Generalises the settings path, build command, console example and the read-only rationale; drops internal knowledge-base links and team roles. Co-Authored-By: Claude Code <noreply@anthropic.com>
Asset Usage Audit
Плагин редактора UE 5.6: показывает, какие ассеты используются на каких уровнях, и какие не используются нигде. Отмеченное галочками выгружается файлами или отчётом.
Задача: OV — «подготовить утилиты для UE по экспорту ассетов, используемых в локациях».
Быстрый старт
Window → Tools → Asset Usage Audit(или в консолиAssetUsageAudit.OpenPanel)- Выбрать локацию в списке Level — это область прогона, а не фильтр результата
- Run Audit
- Отфильтровать по типу, отметить галочками
- Внизу — Export report (JSON+CSV) или Export ticked assets…, открывающая окно выгрузки
Двойной клик по строке показывает ассет в Content Browser — как Browse To в редакторе. Ассет при этом не загружается: строка может быть картой или мешем на сотни мегабайт.
Полный свип по всем 1072 уровням — ~1–3 с. По одному уровню — быстрее.
Интерфейс
Тулбар
| Контрол | Назначение |
|---|---|
| By asset / By level | Направление вопроса. By level строит дерево: уровни-заголовки, под ними ассеты. Без выбранного уровня показывает все уровни сразу — так сравниваются две локации. Галочка на заголовке отмечает всё под ним; частичный выбор рисуется третьим состоянием, а не полной галочкой |
| Level | Область аудита, чеклист с множественным выбором. Пусто = все уровни. Смена помечает результат устаревшим, но не запускает прогон: «All levels» это 1072 уровня |
| Types | Сверху пресеты (StaticMesh, Material, VFX, Sound…), ниже все 136 сырых классов. Пресет разворачивается в подклассы, поэтому Material ловит и MaterialInstanceConstant; пресеты, которых в проекте нет, не показываются. Повторный клик по выбранному пресету снимает его целиком |
| Blueprint class | Подстрока геймплейного класса. Непустое значение скрывает не-блюпринты |
| Verdicts | Пять состояний, все включены по умолчанию |
| References | Как ассет удерживается и через что достигнут |
| Поиск | По имени и пути |
Колонки
Check · Type · Name · Path · Verdict · Levels · Hard · Soft · Provenance · Route
Route — цепочка, объясняющая вердикт:
WP_Main → BP_Pickup_Child_C_UAID_...1381106233 → BP_Pickup_Master → SM_ReaperStation
Вердикт без маршрута для художника бесполезен, поэтому маршрут пишется всегда.
Type показывает класс ассета (Blueprint), геймплейный класс — в тултипе. Это сделано намеренно: если бы в колонке стоял BP_Pickup_Child_C, строка попадала бы под фильтр Blueprint, и таблица противоречила бы сама себе.
Вердикты
Пять состояний, никогда не булево.
| Вердикт | Значение |
|---|---|
UsedOnLevel |
Достижим от карты. Колонки уточняют hard/soft и через какой уровень |
UsedByAssetsOnly |
Есть референсеры, но ни одна цепочка не доходит до карты |
ReferencedFromConfigOrSource |
Найден grep-ом по Config/ и Source/. В провенансе — файл и строка |
Unreferenced |
Ни одной входящей ссылки |
Unknown |
Попадает в слепую зону реестра |
⚠️ Unknown — это не «не используется»
Asset Registry принципиально не видит:
- пути, собранные конкатенацией строк в C++/BP;
OpenLevel(FName);- строки внутри DataTable — уровень зависит от таблицы целиком, поэтому любая строка выглядит используемой;
- FMOD — резолвит события строковыми путями мимо UObject-графа. Аудио систематически попадает в
Unknown.
Инструмент никогда не удаляет и не предлагает удалить. Только отбор и выгрузка.
Смягчение: скан Config/ и Source/ по \/Game([A-Za-z0-9_.\/]+)\b. Без него GameMode, GameInstance и стартовая карта помечались бы мусором — на них не ссылается ни один ассет, только DefaultEngine.ini.
Настройки
Project Settings → <Проект> → Asset Usage Audit. Пишутся в Config/DefaultEditor.ini — файл под контролем версий, настройки общие для команды.
Состояние панели (выбранный уровень, типы, режим выгрузки, чекбокс зависимостей) хранится отдельно — в Saved/Config/.../EditorPerProjectUserSettings.ini, пер-юзерно и вне контроля версий. Сохраняется при закрытии вкладки; аварийное завершение редактора теряет изменения. Строка поиска намеренно не восстанавливается: панель, открывшаяся пустой из-за забытого фильтра, выглядит сломанной.
| Параметр | По умолчанию |
|---|---|
DefaultExportDirectory |
пусто → Saved/AssetUsageAudit |
bOverwriteExistingFiles |
false → индексирование имён |
ExcludedPackagePaths |
3rdParty, StarterContent, StarterBundle, Megascans, MSPresets |
IncludedPackagePaths |
пусто → /Game |
bScanIndirectReferences |
true |
bHideExternalPackages |
true |
bMirrorFolderStructure |
false → плоская папка |
ExchangeFormatByClass |
пусто → встроенные умолчания |
⚠️ Покупной пак не следует исключать вслепую: он может поставлять класс, который DefaultEngine.ini назначает GameMode или GameInstance проекта. Проверяйте ExcludedPackagePaths против конфига.
Консольные команды
AssetUsageAudit.OpenPanel
AssetUsageAudit.Run [/Game/Maps/YourLevel ...]
AssetUsageAudit.FindUnused
Без аргументов Run обходит все уровни. Первый вызов после старта редактора ждёт догрузки Asset Registry.
Blueprint / Python
Категория Asset Usage Audit: RunAudit, GetAssetsUsedOnLevel, GetLevelsUsingAsset, MeasureFullSweepSeconds.
Отчёты
Пишутся парой, Saved/AssetUsageAudit/:
- JSON — вложенные списки уровней, метаданные фильтров
- CSV — UTF-8 с BOM, разделитель
;, многозначные поля через|
BOM обязателен: без него Excel ломает кириллицу. Шапка CSV содержит применённые фильтры, версию движка, длительность и предупреждение про Unknown.
Выгрузка файлов
Все параметры собраны в отдельном окне: кнопка Export ticked assets… внизу панели. Наверху остались только фильтры и Run Audit — режим, раскладка, папка и зависимости трогаются лишь в момент выгрузки, и окно может показать, к чему они приведут.
Окно наследует значения из настроек проекта и из прошлого выбора этого пользователя, но ничего не пишет обратно при отмене.
Раскладка папок
| Вариант | Что делает |
|---|---|
| Flat | Всё в одну папку. Ради этого и существует политика имён |
| Mirror the content tree | Воспроизводит дерево /Game |
| One folder per asset | Каждому отмеченному ассету — своя папка, зависимости рядом |
| Migrate into another Unreal project | Передаёт всё движковому MigratePackages. Единственный вариант, после которого ссылки работают |
⚠️ Копия .uasset не переносит зависимости сама по себе
Ссылки внутри .uasset — это полные имена пакетов (/Game/Art/T_Rock_D), а не относительные пути. Файл резолвится, только если лежит ровно по этому пути от Content/ в целевом проекте.
Отсюда: Flat и One folder per asset дают файлы для людей, а не для движка. Скопированные в проект, они откроются с битыми ссылками. Диалог говорит это прямо в подсказке под настройками.
Для переноса в другой UE-проект есть Migrate. Указывать надо папку Content/ целевого проекта.
⚠️ Папка назначения обязана удовлетворять двум условиям движка, иначе перенос молча не состоится:
- путь оканчивается на
/Content/; - на уровень выше лежит
.uprojectили ровно один.uplugin— из этого движок выводит точку монтирования.
Проверка повторена у нас (AssetUsagePaths::ValidateMigrateDestination) и показывается прямо в окне: кнопка Export гаснет, причина стоит внизу. Так вышло не от аккуратности — первая версия отправляла папку Saved/AssetUsageAudit, движок отвечал does not appear to be a game Content folder только в Output Log, и в панели не происходило ничего. При переключении на Migrate подставленный путь сбрасывается: он заведомо непригоден.
Про Migrate стоит знать:
- Ему передаются только отмеченные пакеты — замыкание он строит сам. Скармливать ему ещё и наше означало бы тот же результат медленнее, с чужими ассетами в его отчёте.
MigratePackagesвозвращаетvoidи отчитывается сам. Поэтому в статусе панели не будет числа скопированных файлов: подделывать его нельзя.- Политика имён к нему неприменима — Migrate обязан положить пакет по исходному пути, иначе теряется весь смысл.
Keep bothвырождается вSkip. - ⚠️ Снятая галочка зависимостей означает
bIgnoreDependencies, а он, по комментарию движка, не переносит OFPA-акторов уровня. Уровень приедет пустым. Панель спрашивает подтверждение отдельно.
Манифест зависимостей
Для раскладок, ломающих ссылки, рядом с файлами пишется AssetUsageAudit.manifest.json — галочка Write dependency manifest, включена по умолчанию.
Содержит: исходное имя пакета каждого файла, фактический путь на диске, был ли ассет отмечен вручную или пришёл зависимостью, и список прямых зависимостей с пометкой, уехали ли они в ту же папку.
⚠️ Фактический путь, а не задуманный. Экспортёр возвращает карту записанного (bRecordWrittenFiles), и манифест строится из неё. Манифест из задуманных путей врал бы ровно в случае сработавшей политики имён — то есть когда он нужнее всего.
Пустой список записанных файлов — ошибка, а не пустой манифест: почти всегда это забытый флаг, а пустой манифест рядом с полной папкой будет принят за правду. По той же причине манифест не пишется под Migrate.
⚠️ One folder per asset пишет больше файлов, чем ассетов. Текстура на сорока мешах копируется в сорок папок — в этом и смысл: папку можно отдать целиком. Диалог подтверждения называет число файлов, а не число ассетов; это разные числа, и путать их дорого.
Галочка Sort dependencies into type subfolders раскладывает зависимости внутри папки ассета по типам — Texture/, Material/, StaticMesh/. Имена берутся из пресетов фильтра типов, а не выдуманы отдельно: в фильтре и на диске должны быть те же слова. Сам ассет остаётся в корне своей папки — он её предмет. Галочка активна только при One folder per asset; при других раскладках она выключена, но видима — исчезающий контрол читается как поломка.
⚠️ Include dependencies — включено по умолчанию
Пакет меша не содержит материалов и текстур, только ссылки на них. Без этой галочки отмеченный SM_Rock приезжает один и открывается розовым; Niagara-система — без спрайтов и модулей.
Поэтому по умолчанию выгружается замыкание: отмеченное плюс всё, на что оно ссылается, транзитивно. Перед записью показывается диалог с реальным числом — «отмечено 40, будет записано 380». Это та цифра, которая останавливает человека, случайно выгружающего пол-проекта.
Границы обхода:
| Запрос | тот же NoRequirements — soft-ссылки ловятся наравне с hard |
/Engine, /Temp |
не копируются — в целевом проекте они уже есть, перезаписать хуже, чем пропустить |
/Script |
не копируются, это код |
| Исключения путей | те же, что у аудита: выгрузка не тянет то, что отчёт игнорирует |
| Отмеченное вручную | проходит мимо фильтров — если человек отметил строку, он получит файл, даже из исключённой папки |
Это не MigratePackages: тот ходит по тому же замыканию, но сам решает, куда класть файлы, спрашивает и не умеет останавливаться на границе папки.
Копия .uasset
Побайтовое копирование, ничего не загружается. Уровень тянет за собой свои OFPA-пакеты — без них выгруженная карта откроется пустой.
⚠️ С источника снимается атрибут read-only. В проектах под VCS, которая держит невытянутые файлы read-only (Perforce и подобные), Windows CopyFile переносит атрибут на копию. Без этого артист получал бы нередактируемую папку, а повторный прогон с политикой Overwrite падал бы на собственном предыдущем выводе. Флаг снимается с обеих сторон: перед перезаписью и после копирования.
Конвертация в обменные форматы
Через UAssetExportTask — меши в FBX, текстуры в PNG, звук в WAV. Загружает каждый ассет, поэтому счёт идёт на минуты, а не на секунды; GC каждые 64 ассета, иначе память кончится раньше экспорта.
Соответствие класса и расширения — в настройках (ExchangeFormatByClass), поиск идёт вверх по иерархии: запись Texture покрывает Texture2D. Пустая карта означает встроенные умолчания.
Blueprint в умолчаниях отсутствует намеренно. Обменного формата у него нет; запись породила бы .t3d-дамп с подписью «экспортировано». Вместо этого класс попадает в счётчик нет настроенного формата.
Причины пропуска разделены на три счётчика — нет формата / нет экспортёра / не загрузился. «17 пропущено» не говорит ничего; «17 без настроенного формата» ведёт прямо в нужную настройку.
Перед стартом показывается диалог с числом реально конвертируемых: узнать об ошибке в выборе режима лучше до нескольких минут заблокированного редактора, а не после.
Импорт из внешней папки
Window → Tools → Import Assets from Folder… — обратное направление: прочитать папку выгруженных .uasset и вернуть выбранное в проект.
Список с галочками, колонки: файл · во что превратится · откуда взят путь · статус.
Откуда берётся целевой путь
Это главная сложность импорта, а не копирование. Ссылка внутри .uasset называет цель полным путём пакета, поэтому файл заработает только если положить его туда, откуда он пришёл. Ошибка здесь не падает громко — она даёт ассет с отсутствующими ссылками, который обнаружат сильно позже.
Четыре источника по убыванию доверия, и колонка показывает, какой сработал:
| Источник | Надёжность |
|---|---|
| Manifest | Мы сами его написали из фактически записанного. Точно |
| Package header | FPackageFileSummary::PackageName — имя, с которым файл сохраняли. Замерено на этом проекте: работает |
| Folder structure | Позиция файла под корнем импорта. Верно для зеркальной выгрузки, догадка для прочих |
| Unresolved | Не восстановить. Импортируется только в явно названную папку, ссылки не сработают |
⚠️ Про Package header есть оговорка в самом движке (AssetHeaderPatcher.cpp:1214): поле сериализуется не всегда, и движок сам предусматривает откат. Поэтому цепочка, а не одна проверка.
Что импорт делать откажется
Импорт — единственная операция инструмента, способная уничтожить работу: запись поверх /Game/Art/SM_Rock заменяет то, что там было, без отмены и без копии.
- Существующий ассет пропускается, пока явно не включена перезапись. Перед перезаписью — отдельное подтверждение.
- Пакет, открытый в редакторе, не перезаписывается никогда, даже с включённой галочкой: подмена файла под загруженным
UPackageоставляет сессию с устаревшими объектами, которые потом сохранятся поверх импорта. - Файлы без восстановимого пути пропускаются, если не названа папка-приёмник.
После копирования выполняется ScanFilesSynchronous — без него файлы лежат на диске и невидимы в Content Browser, что читается как «импорт ничего не сделал».
Удаление отмеченных ассетов
Кнопка Delete ticked assets… внизу панели, рядом с выгрузкой. Работает по тому же набору галочек.
Удаление — единственная операция инструмента, которая уничтожает работу безвозвратно, поэтому проходит через два окна.
Окно 1 — наше
Показывает весь отмеченный набор построчно: имя, тип, путь, сколько ассетов снаружи набора ещё ссылается на строку (в тултипе — их имена), и статус.
- Ссылающиеся ассеты, которые сами удаляются вместе с целью, в счётчик не попадают. Иначе удаление блюпринта вместе с его единственным мешем выглядело бы опасным, хотя оно чистое.
- Референсеры запрашиваются тем же
NoRequirements-запросом, что и аудит: hard-only потерял бы каждую soft-ссылку, а soft-ссылка ломается точно так же — просто в рантайме, а не при загрузке. - Файл, помеченный read-only на диске, отмечается заранее. Под VCS, которая держит невытянутые файлы нередактируемыми, без этой пометки удаление падало бы по одному файлу в середине пачки.
- Кнопка удаления неактивна, пока не поставлена галочка «I understand these files will be removed from the project».
Окно 2 — родное от UE
После подтверждения набор уходит в ObjectTools::DeleteAssets(..., bShowConfirmation=true), и открывается штатное окно Delete Assets движка — со списком ссылок, Force Delete и Replace References.
Оно не имитировано, а вызвано: своя реализация замены ссылок означала бы свою реализацию FAssetDeleteModel, ошибки которой всплыли бы на чужом проекте.
Что удалять запрещено
Отказы показываются в списке с причиной, строку нельзя отметить — молча выкинуть её из набора было бы хуже, чем отказать вслух.
| Отказ | Почему |
|---|---|
Уровень (.umap) |
Уровень — это единица, относительно которой инструмент меряет использование. Удалив его, обесцениваешь каждую другую строку результата. Плюс движок сам отказывается удалять открытую карту, и итог зависел бы от того, какая карта сейчас загружена |
OFPA-пакет (__ExternalActors__, __ExternalObjects__) |
Это не ассет, а размещённый на карте актор. Удалять его мимо level-редактора — значит править карту за его спиной, без его undo |
/Engine, /Script, /Temp |
Не контент этого проекта |
| Уже отсутствует | В реестре нет ассета под этим именем — удалять нечего |
⚠️ Набор для удаления не расширяется зависимостями
В отличие от выгрузки. Выгрузка тянет зависимости, чтобы меш приехал с материалами; удаление, расширенное так же, снесло бы контент, который никто не отмечал, — одна общая текстура утащила бы половину проекта.
Удаляется ровно отмеченное и никогда больше.
⚠️ После удаления результат помечается устаревшим
Удалённые строки убираются из таблицы, но всё, что на них ссылалось, сохраняет прежние счётчики ссылок, а ассет, бывший UsedByAssetsOnly через удалённый блюпринт, теперь имеет другой вердикт. Панель говорит об этом прямо и просит перезапустить аудит, а не выдаёт старые числа за свежий замер.
И главное — то же, что и везде в этом инструменте: Unreferenced не означает «не используется». Реестр не видит путей, собранных строками, содержимого DataTable и событий FMOD. Окно удаления повторяет это предупреждение прямо в тексте.
Архитектура
AssetUsageAuditCore UncookedOnly — весь анализ, ноль UI
AssetUsageAuditEditor Editor — Slate-панель, настройки, команды
AssetUsageAuditTests UncookedOnly — 128 спек
Инвариант Core: не линковать UnrealEd, AssetTools, ToolMenus, Slate, SlateCore, EditorSubsystem. AssetTools editor-only транзитивно через UnrealEd.
Engine в Core разрешён — Runtime-модуль, доступен в коммандлете. Нужен для ULevel::GetExternalActorsPaths.
⚠️ Инварианты, которые нельзя нарушать
Проверено чтением исходников UE 5.6. Без этого инструмент молча даёт неверный ответ.
1. Запрос зависимостей — всегда NoRequirements
AssetUsageAudit::MakeTraversalCategory() // EDependencyCategory::Package
AssetUsageAudit::MakeTraversalQuery() // FDependencyQuery{} — пустой
Два факта движка:
ExternalObjectAndActorDependencyGatherer.cpp:22 выдаёт рёбра карта→внешний актор с маской Game | Build — без Hard. А AssetRegistryInterface.h:95: отсутствие Hard и есть soft-зависимость. Запрос с EDependencyQuery::Hard теряет все 16 117 OFPA-пакетов проекта.
Soft определён как NotHard. Значит Hard | Soft = «требуется Hard И требуется не-Hard» = пустое множество.
Hard/soft — это колонка в отчёте, а не фильтр запроса. Отдельной константы Hard в коде нет — ошибиться негде.
Есть канарейка: если достижимых ассетов меньше четверти от числа OFPA-пакетов, в лог падает предупреждение. Это сигнатура регресса к Hard-запросу.
2. Границу карты пересекать только от уровня или его внешнего пакета
Первая версия проваливалась в любой встреченный World. Замер на WP_Main: 18 136 строк, из них 9 994 приходили через чужую карту:
WP_Main → BP_FirstPersonGameMode → PDA_MenuSystemConfig → L_MainLevel → …
Две трети ответа были содержимым другого уровня. После исправления — 8 644.
Правило: пересечение разрешено, только если источник ребра — сам уровень или его внешний пакет. Это покрывает сублевелы и Level Instance, но не ссылку из дата-ассета. Флаг bTraverseIntoOtherLevels возвращает прежнее поведение.
3. Пути OFPA не собирать строками
Только ULevel::GetExternalActorsPaths / GetExternalObjectsPaths (множественная форма — плагины регистрируют пути делегатами). Content Bundles вставляют /CB/<Guid>/, External Data Layers — /EDL/<UID>/.
Обратное отображение «внешний актор → его карта» не делать: PackageDependencyData.cpp:57-96 намеренно снимает флаг UsedInGame с этого ребра. Идти только вперёд от карты.
4. Blueprint не находится фильтром по классу
У ассета BP_Foo класс всегда /Script/Engine.Blueprint. Геймплейный класс живёт в теге GeneratedClass. Нужны два запроса — как в SAssetAuditBrowser::AddAssetsOfClass.
5. ScanLevelAssets перед обходом уровня
Гейтерер сообщает только те внешние пакеты, которые реестр уже отсканировал. Без этого уровень, который никто не открывал в сессии, отдаёт пустой список — неотличимо от уровня без акторов.
Проектные решения
Чекбоксы — TSet<FName>, не селекция SListView. План предписывал селекцию (Ctrl+A бесплатно), но она пересоздаётся при смене фильтра: пользователь, отметивший 40 ассетов и переключивший тип, потерял бы отметки молча.
Тип ассета и геймплейный класс — разные оси. Смешение давало 2980 записей в выпадающем списке. Разделено: 136 классов в меню, геймплейный класс — текстовым полем.
Смена области не запускает аудит. «All levels» = 1072 уровня; долгий прогон по клику в списке был бы неприятным сюрпризом. Результат помечается устаревшим.
Выгрузка — копирование, не MigratePackages. Migrate тянет всё дерево зависимостей, чего никто не просил, и требует editor-only модуля.
Конвертация живёт в Core, а не в Editor. UExporter и UAssetExportTask — это ENGINE_API в Runtime/Engine, не в UnrealEd. Core уже линкует Engine, так что инвариант не нарушен. Конкретные экспортёры (FBX, PNG) резолвятся рефлексией в рантайме: их отсутствие даёт честное «нет экспортёра», а не ошибку линковки — и открывает дорогу коммандлету.
Сборка и тесты
⚠️ Закрыть редактор — иначе линковка DLL упадёт.
"<UE>/Engine/Build/BatchFiles/Build.bat" \
<Project>Editor Win64 Development \
-project="<abs path>/<Project>.uproject" -waitmutex
"C:/Program Files/Epic Games/UE_5.6/Engine/Binaries/Win64/UnrealEditor-Cmd.exe" \
"<project>.uproject" \
-ExecCmds="Automation RunTests AssetUsageAudit" \
-TestExit="Automation Test Queue Empty" \
-unattended -nopause -nosplash -stdout -abslog="<abs path>"
128 спек, префикс AssetUsageAudit.*.
Три набора работают на настоящем контенте проекта, а не на выдуманных именах: ExporterLive (копирование, коллизии, раскрытие OFPA), ExchangeExport (конвертация) и GraphFidelity (сверка графа с реестром). Субъект они ищут через Asset Registry и берут самый маленький OFPA-уровень — на WP_Main тест копировал бы гигабайты. Если контент не найден, тест пишет предупреждение и не падает, поэтому в логе стоит смотреть на предупреждения: их отсутствие означает, что тесты реально работали с контентом.
GraphFidelity — замена ручной сверке с Reference Viewer
Reference Viewer сам по себе не источник истины: он рисует те же рёбра, что отдаёт Asset Registry. Поэтому «сверить глазами десять ассетов» сведено к воспроизводимой проверке — граф против сырого ответа реестра тем же запросом:
- ни одного выдуманного ребра и ни одного потерянного (кроме отфильтрованных по путям);
- флаг hard совпадает с ответом реестра;
- каждый переход в колонке Route — настоящее ребро, а не склейка;
- ассет сублевела приписан и сублевелу, и родительской карте.
⚠️ Выборка обязана быть по имени пакета, а не по индексу. Индексы зависят от порядка перечисления реестра: граф между прогонами идентичен (84 505 пакетов, 370 722 ребра), а шаг по индексу каждый раз брал разные пакеты, и спека падала через раз. Сейчас проверяется одно и то же: 872 пакета и 3393 цели, число в число во всех прогонах.
⚠️ Hard сравнивать по цели, а не по ребру. Реестр может отдать одну и ту же цель дважды с разными масками — hard и soft одновременно, — и граф честно хранит оба. Проверка «это ребро hard?» против множества «есть ли hard-ребро к этой цели» помечает soft-двойника расхождением.
⚠️ Два флага ломают прогон на этом проекте
| Флаг | Симптом |
|---|---|
-nullrhi |
Fatal error: Null assigned to TNotNull (NotNull.cpp:12). Дефект проекта, не плагина — воспроизводится при полностью отключённых плагинах |
-NoShaderCompile |
Failed to find shader map for default material (MaterialShared.cpp:2905) |
Следствие: headless без GPU на этом проекте недоступен. Это блокирует серверную автоматизацию.
Ещё: тест, намеренно провоцирующий UE_LOG(Error), обязан объявить AddExpectedError — иначе фреймворк считает его упавшим.
Замеры
| Метрика | Значение |
|---|---|
| Пакетов в графе | 62 517 (после исключений; без них — 84 505) |
| Рёбер | 290 170 (без исключений — 370 722) |
| Уровней | 1072 |
| OFPA-пакетов | 16 117 |
| Полный свип | ~1.1 с |
WP_Main |
~1.6 с, 8 644 ассета |
| Все уровни | 33 261 UsedOnLevel |
| Отчёт | 45 167 строк, JSON 21.8 МБ + CSV 10.2 МБ |
Кэш не нужен — вопрос закрыт замером.
Незакрытое
- UI после переделки в дерево руками не проверялся. Компилируется, 121 автотест зелёный, но автотесты не трогают Slate: раскрытие уровней, третье состояние галочки на заголовке и сортировка внутри группы проверены только чтением кода.
- Коммандлет не написан. Упирается в дефект headless выше.
- Конвертация не прогонялась на большом объёме. Тесты покрывают единицы ассетов; поведение GC и времени на тысячах — не замерено.
- Карта форматов узкая. В дефолте только классы, для которых движок реально поставляет
UExporter. Материалы, блюпринты и Niagara не конвертируются никуда — это ограничение движка, а не недоделка, но пользователя нужно об этом предупредить явно. - GameMode может давать заметную долю строк отчёта по главной карте — нужен ли фильтр «исключить достижимое только через GameMode», решать по месту.
- Ассеты сторонних аудио-плагинов попадают в
Unknown— скрывать по умолчанию или помечать, решать по месту.
Контроль версий
Плагин — самостоятельный git-репозиторий. Артефакты сборки (Binaries/, Intermediate/) исключены локальным .gitignore: они пересобираются UBT из исходников и в коммит попадать не должны.
При подключении к проекту как сабмодуль:
git submodule add <url> Plugins/AssetUsageAudit
Связанное
Plugins/AssetsCleaner— ищет неиспользуемые ассеты, но не умеет привязку к уровням