Compare commits

...

9 Commits

Author SHA1 Message Date
MagentaDolphin d4161cdc77 fix: asset paths cannot contain spaces in test fixture
The anonymisation pass replaced a pack name with a phrase containing
spaces, and the scanner splits package paths on whitespace, so the
expectation became /Game/a. Uses a single-token folder name instead.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 18:57:14 +07:00
MagentaDolphin 63c4054aef docs: correct test counts (169 cases in 16 spec suites)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 18:47:20 +07:00
MagentaDolphin a0a061656e docs: anonymise remaining project names in test fixtures
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 18:41:51 +07:00
MagentaDolphin 640e940ab4 docs: anonymise project asset names in source comments
Measured figures are kept; only the studio-specific level and asset
names are generalised, and the Perforce rationale is reworded to
describe the class of setups rather than this one project.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 18:39:39 +07:00
MagentaDolphin e92306c797 docs: anonymise project asset names in examples
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 17:44:44 +07:00
MagentaDolphin 0363b07b65 docs: remove studio-specific paths and references from README
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>
2026-09-07 17:44:01 +07:00
MagentaDolphin f3c82d3815 fix: correct authorship attribution in deletion feature files
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 17:35:50 +07:00
MagentaDolphin 430fc18d5f Merge branch 'master' into wip/asset-deletion 2026-09-07 17:35:49 +07:00
MagentaDolphin aeb4a1067c fix: correct authorship attribution to MagentaDolphin
The plugin was authored outside studio work but carried NextGenium
attribution in the .uplugin descriptor and in every source header.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 17:35:48 +07:00
69 changed files with 159 additions and 170 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"FriendlyName": "Asset Usage Audit", "FriendlyName": "Asset Usage Audit",
"Description": "Reports which assets are used on which levels, resolving usage transitively through Blueprints and other assets, and handling hard/soft references and One File Per Actor. Exports selected assets and usage reports.", "Description": "Reports which assets are used on which levels, resolving usage transitively through Blueprints and other assets, and handling hard/soft references and One File Per Actor. Exports selected assets and usage reports.",
"Category": "Editor", "Category": "Editor",
"CreatedBy": "Next-Genium", "CreatedBy": "MagentaDolphin",
"CanContainContent": false, "CanContainContent": false,
"EnabledByDefault": true, "EnabledByDefault": true,
"Modules": [ "Modules": [
+23 -34
View File
@@ -40,7 +40,7 @@
**Route** — цепочка, объясняющая вердикт: **Route** — цепочка, объясняющая вердикт:
``` ```
WP_Main → BP_Pickup_Child_C_UAID_...1381106233 → BP_Pickup_Master → SM_ReaperStation WP_Example → BP_Child_C_UAID_... → BP_Master → SM_Station
``` ```
Вердикт без маршрута для художника бесполезен, поэтому маршрут пишется всегда. Вердикт без маршрута для художника бесполезен, поэтому маршрут пишется всегда.
@@ -71,13 +71,13 @@ Asset Registry принципиально не видит:
**Инструмент никогда не удаляет и не предлагает удалить.** Только отбор и выгрузка. **Инструмент никогда не удаляет и не предлагает удалить.** Только отбор и выгрузка.
Смягчение: скан `Config/` и `Source/` по `\/Game([A-Za-z0-9_.\/]+)\b`. Без него `BP_FirstPersonGameMode`, `BP_MenuSystemGameInstance` и `RefinedMenuMap` помечались бы мусором — на них не ссылается ни один ассет, только `DefaultEngine.ini`. Смягчение: скан `Config/` и `Source/` по `\/Game([A-Za-z0-9_.\/]+)\b`. Без него GameMode, GameInstance и стартовая карта помечались бы мусором — на них не ссылается ни один ассет, только `DefaultEngine.ini`.
--- ---
## Настройки ## Настройки
`Project Settings → 14Overmind → Asset Usage Audit`. Пишутся в `Config/DefaultEditor.ini` — файл Perforce-трекается, настройки видны команде. `Project Settings → <Проект> → Asset Usage Audit`. Пишутся в `Config/DefaultEditor.ini` — файл под контролем версий, настройки общие для команды.
Состояние панели (выбранный уровень, типы, режим выгрузки, чекбокс зависимостей) хранится **отдельно** — в `Saved/Config/.../EditorPerProjectUserSettings.ini`, пер-юзерно и вне контроля версий. Сохраняется при закрытии вкладки; аварийное завершение редактора теряет изменения. Строка поиска намеренно не восстанавливается: панель, открывшаяся пустой из-за забытого фильтра, выглядит сломанной. Состояние панели (выбранный уровень, типы, режим выгрузки, чекбокс зависимостей) хранится **отдельно** — в `Saved/Config/.../EditorPerProjectUserSettings.ini`, пер-юзерно и вне контроля версий. Сохраняется при закрытии вкладки; аварийное завершение редактора теряет изменения. Строка поиска намеренно не восстанавливается: панель, открывшаяся пустой из-за забытого фильтра, выглядит сломанной.
@@ -92,7 +92,7 @@ Asset Registry принципиально не видит:
| `bMirrorFolderStructure` | `false` → плоская папка | | `bMirrorFolderStructure` | `false` → плоская папка |
| `ExchangeFormatByClass` | пусто → встроенные умолчания | | `ExchangeFormatByClass` | пусто → встроенные умолчания |
⚠️ `Content/MenuSystemPro` **намеренно не исключён**, хотя это покупной пак: он поставляет `BP_MenuSystemGameInstance`, который `DefaultEngine.ini` назначает GameInstance проекта. ⚠️ Покупной пак **не следует исключать вслепую**: он может поставлять класс, который `DefaultEngine.ini` назначает GameMode или GameInstance проекта. Проверяйте `ExcludedPackagePaths` против конфига.
--- ---
@@ -100,7 +100,7 @@ Asset Registry принципиально не видит:
``` ```
AssetUsageAudit.OpenPanel AssetUsageAudit.OpenPanel
AssetUsageAudit.Run [/Game/Space/Maps/WP_Main ...] AssetUsageAudit.Run [/Game/Maps/YourLevel ...]
AssetUsageAudit.FindUnused AssetUsageAudit.FindUnused
``` ```
@@ -196,7 +196,7 @@ BOM обязателен: без него Excel ломает кириллицу.
Побайтовое копирование, ничего не загружается. Уровень тянет за собой свои OFPA-пакеты — без них выгруженная карта откроется пустой. Побайтовое копирование, ничего не загружается. Уровень тянет за собой свои OFPA-пакеты — без них выгруженная карта откроется пустой.
⚠️ **С источника снимается атрибут read-only.** Проект Perforce-primary, неоткрытые файлы на диске read-only, а Windows `CopyFile` переносит атрибут на копию. Без этого артист получал бы нередактируемую папку, а повторный прогон с политикой `Overwrite` падал бы на собственном предыдущем выводе. Флаг снимается с обеих сторон: перед перезаписью и после копирования. ⚠️ **С источника снимается атрибут read-only.** В проектах под VCS, которая держит невытянутые файлы read-only (Perforce и подобные), Windows `CopyFile` переносит атрибут на копию. Без этого артист получал бы нередактируемую папку, а повторный прогон с политикой `Overwrite` падал бы на собственном предыдущем выводе. Флаг снимается с обеих сторон: перед перезаписью и после копирования.
### Конвертация в обменные форматы ### Конвертация в обменные форматы
@@ -257,7 +257,7 @@ BOM обязателен: без него Excel ломает кириллицу.
- Ссылающиеся ассеты, которые сами удаляются вместе с целью, **в счётчик не попадают**. Иначе удаление блюпринта вместе с его единственным мешем выглядело бы опасным, хотя оно чистое. - Ссылающиеся ассеты, которые сами удаляются вместе с целью, **в счётчик не попадают**. Иначе удаление блюпринта вместе с его единственным мешем выглядело бы опасным, хотя оно чистое.
- Референсеры запрашиваются тем же `NoRequirements`-запросом, что и аудит: hard-only потерял бы каждую soft-ссылку, а soft-ссылка ломается точно так же — просто в рантайме, а не при загрузке. - Референсеры запрашиваются тем же `NoRequirements`-запросом, что и аудит: hard-only потерял бы каждую soft-ссылку, а soft-ссылка ломается точно так же — просто в рантайме, а не при загрузке.
- Файл, помеченный **read-only на диске**, отмечается заранее. Проект Perforce-primary, невытянутый файл нередактируемый, и без этой пометки удаление падало бы по одному файлу в середине пачки. - Файл, помеченный **read-only на диске**, отмечается заранее. Под VCS, которая держит невытянутые файлы нередактируемыми, без этой пометки удаление падало бы по одному файлу в середине пачки.
- Кнопка удаления неактивна, пока не поставлена галочка «I understand these files will be removed from the project». - Кнопка удаления неактивна, пока не поставлена галочка «I understand these files will be removed from the project».
### Окно 2 — родное от UE ### Окно 2 — родное от UE
@@ -296,7 +296,7 @@ BOM обязателен: без него Excel ломает кириллицу.
``` ```
AssetUsageAuditCore UncookedOnly — весь анализ, ноль UI AssetUsageAuditCore UncookedOnly — весь анализ, ноль UI
AssetUsageAuditEditor Editor — Slate-панель, настройки, команды AssetUsageAuditEditor Editor — Slate-панель, настройки, команды
AssetUsageAuditTests UncookedOnly — 128 спек AssetUsageAuditTests UncookedOnly — 16 spec-сьютов, 169 кейсов
``` ```
**Инвариант Core:** не линковать `UnrealEd`, `AssetTools`, `ToolMenus`, `Slate`, `SlateCore`, `EditorSubsystem`. `AssetTools` editor-only транзитивно через `UnrealEd`. **Инвариант Core:** не линковать `UnrealEd`, `AssetTools`, `ToolMenus`, `Slate`, `SlateCore`, `EditorSubsystem`. `AssetTools` editor-only транзитивно через `UnrealEd`.
@@ -328,10 +328,10 @@ Hard/soft — это **колонка в отчёте**, а не фильтр з
### 2. Границу карты пересекать только от уровня или его внешнего пакета ### 2. Границу карты пересекать только от уровня или его внешнего пакета
Первая версия проваливалась в любой встреченный World. Замер на `WP_Main`: **18 136** строк, из них **9 994** приходили через чужую карту: Первая версия проваливалась в любой встреченный World. Замер на `WP_Example`: **18 136** строк, из них **9 994** приходили через чужую карту:
``` ```
WP_Main → BP_FirstPersonGameMode → PDA_MenuSystemConfig → L_MainLevel → … WP_Example → BP_GameMode → PDA_MenuConfig → L_Other → …
``` ```
Две трети ответа были содержимым другого уровня. После исправления — **8 644**. Две трети ответа были содержимым другого уровня. После исправления — **8 644**.
@@ -373,9 +373,9 @@ WP_Main → BP_FirstPersonGameMode → PDA_MenuSystemConfig → L_MainLevel →
⚠️ Закрыть редактор — иначе линковка DLL упадёт. ⚠️ Закрыть редактор — иначе линковка DLL упадёт.
```bash ```bash
"C:/Program Files/Epic Games/UE_5.6/Engine/Build/BatchFiles/Build.bat" \ "<UE>/Engine/Build/BatchFiles/Build.bat" \
SpaceEditor Win64 Development \ <Project>Editor Win64 Development \
-project="D:\Work\NG\ng_MagentaDolphin_space\overmind\14Overmind.uproject" -waitmutex -project="<abs path>/<Project>.uproject" -waitmutex
``` ```
```bash ```bash
@@ -386,9 +386,9 @@ WP_Main → BP_FirstPersonGameMode → PDA_MenuSystemConfig → L_MainLevel →
-unattended -nopause -nosplash -stdout -abslog="<abs path>" -unattended -nopause -nosplash -stdout -abslog="<abs path>"
``` ```
**128 спек**, префикс `AssetUsageAudit.*`. **169 кейсов в 16 spec-сьютах**, префикс `AssetUsageAudit.*`.
Три набора работают на **настоящем контенте проекта**, а не на выдуманных именах: `ExporterLive` (копирование, коллизии, раскрытие OFPA), `ExchangeExport` (конвертация) и `GraphFidelity` (сверка графа с реестром). Субъект они ищут через Asset Registry и берут **самый маленький** OFPA-уровень — на `WP_Main` тест копировал бы гигабайты. Если контент не найден, тест пишет предупреждение и не падает, поэтому в логе стоит смотреть на предупреждения: их отсутствие означает, что тесты реально работали с контентом. Три набора работают на **настоящем контенте проекта**, а не на выдуманных именах: `ExporterLive` (копирование, коллизии, раскрытие OFPA), `ExchangeExport` (конвертация) и `GraphFidelity` (сверка графа с реестром). Субъект они ищут через Asset Registry и берут **самый маленький** OFPA-уровень — на `WP_Example` тест копировал бы гигабайты. Если контент не найден, тест пишет предупреждение и не падает, поэтому в логе стоит смотреть на предупреждения: их отсутствие означает, что тесты реально работали с контентом.
### `GraphFidelity` — замена ручной сверке с Reference Viewer ### `GraphFidelity` — замена ручной сверке с Reference Viewer
@@ -425,7 +425,7 @@ Reference Viewer сам по себе не источник истины: он
| Уровней | 1072 | | Уровней | 1072 |
| OFPA-пакетов | 16 117 | | OFPA-пакетов | 16 117 |
| Полный свип | ~1.1 с | | Полный свип | ~1.1 с |
| `WP_Main` | ~1.6 с, 8 644 ассета | | `WP_Example` | ~1.6 с, 8 644 ассета |
| Все уровни | 33 261 `UsedOnLevel` | | Все уровни | 33 261 `UsedOnLevel` |
| Отчёт | 45 167 строк, JSON 21.8 МБ + CSV 10.2 МБ | | Отчёт | 45 167 строк, JSON 21.8 МБ + CSV 10.2 МБ |
@@ -435,37 +435,26 @@ Reference Viewer сам по себе не источник истины: он
## Незакрытое ## Незакрытое
- **UI после переделки в дерево руками не проверялся.** Компилируется, 121 автотест зелёный, но автотесты не трогают Slate: раскрытие уровней, третье состояние галочки на заголовке и сортировка внутри группы проверены только чтением кода. - **UI после переделки в дерево руками не проверялся.** Компилируется, но автотесты не трогают Slate: раскрытие уровней, третье состояние галочки на заголовке и сортировка внутри группы проверены только чтением кода.
- **Коммандлет не написан.** Упирается в дефект headless выше. - **Коммандлет не написан.** Упирается в дефект headless выше.
- **Конвертация не прогонялась на большом объёме.** Тесты покрывают единицы ассетов; поведение GC и времени на тысячах — не замерено. - **Конвертация не прогонялась на большом объёме.** Тесты покрывают единицы ассетов; поведение GC и времени на тысячах — не замерено.
- **Карта форматов узкая.** В дефолте только классы, для которых движок реально поставляет `UExporter`. Материалы, блюпринты и Niagara не конвертируются никуда — это ограничение движка, а не недоделка, но LA/3D нужно об этом сказать явно. - **Карта форматов узкая.** В дефолте только классы, для которых движок реально поставляет `UExporter`. Материалы, блюпринты и Niagara не конвертируются никуда — это ограничение движка, а не недоделка, но пользователя нужно об этом предупредить явно.
- `BP_FirstPersonGameMode` даёт 29% строк `WP_Main` — нужен ли фильтр «исключить достижимое только через GameMode», решать LA/3D. - GameMode может давать заметную долю строк отчёта по главной карте — нужен ли фильтр «исключить достижимое только через GameMode», решать по месту.
- FMOD в `Unknown` — скрывать по умолчанию или помечать, решать аудио-отделу. - Ассеты сторонних аудио-плагинов попадают в `Unknown` — скрывать по умолчанию или помечать, решать по месту.
- Плагин лежит в git-части репозитория (`Plugins/**` вайтлистится в `.gitignore` и исключается в `.p4ignore`). Вынос в сабмодуль — после согласования.
--- ---
## Контроль версий ## Контроль версий
Плагин — **самостоятельный git-репозиторий** внутри `Plugins/`, как остальные 26 плагинов проекта. Удалённого сервера пока нет: репозиторий и выделенный remote ещё не согласованы. Плагин — **самостоятельный git-репозиторий**. Артефакты сборки (`Binaries/`, `Intermediate/`) исключены локальным `.gitignore`: они пересобираются UBT из исходников и в коммит попадать не должны.
Из этого следуют две вещи: При подключении к проекту как сабмодуль:
- в `.gitmodules` записи **нет** и быть не должно, пока нет URL. Запись без рабочего адреса уронит `git submodule update --init` в `SetupProject.bat` у всех, кто дёрнет проект;
- родительский репозиторий видит папку как `?? Plugins/AssetUsageAudit/`. Так и задумано: коммитить её содержимое в родителя нельзя, иначе при появлении сабмодуля будут две копии одних файлов.
**История живёт только на машине разработчика.** Как только появится remote:
```bash ```bash
git -C Plugins/AssetUsageAudit remote add origin <url>
git -C Plugins/AssetUsageAudit push -u origin master
git submodule add <url> Plugins/AssetUsageAudit git submodule add <url> Plugins/AssetUsageAudit
``` ```
Артефакты сборки (`Binaries/`, `Intermediate/` — 231 МБ) исключены локальным `.gitignore`. В Perforce плагин не попадает: `Plugins/**` исключён в `.p4ignore`.
## Связанное ## Связанное
- `.docs/architecture/asset-usage-audit.md` — заметка в базе знаний - `Plugins/AssetsCleaner` — ищет неиспользуемые ассеты, но **не умеет привязку к уровням**
- `.docs/guides/build-and-run.md` — сборка и прогон тестов
- `Plugins/AssetsCleaner` — уже включён, ищет неиспользуемые ассеты, но **не умеет привязку к уровням**
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetDeletionScan.h" #include "AssetDeletionScan.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetDependencyClosure.h" #include "AssetDependencyClosure.h"
@@ -252,8 +252,8 @@ namespace AssetDependencyClosure
// comes from a level or from one of its external actor packages - the shape a // comes from a level or from one of its external actor packages - the shape a
// streaming sublevel or a Level Instance actually has. A World named by an ordinary // streaming sublevel or a Level Instance actually has. A World named by an ordinary
// asset is a different location that happens to be mentioned, and following it // asset is a different location that happens to be mentioned, and following it
// exports someone else's map: measured on WP_Main, the chain // exports someone else's map: measured on WP_Example, the chain
// BP_FirstPersonGameMode -> PDA_MenuSystemConfig -> L_MainLevel accounts for 9994 // BP_GameMode -> PDA_MenuConfig -> L_Other accounts for 9994
// packages. FLevelUsageResolver applies the same rule; the two must not disagree // packages. FLevelUsageResolver applies the same rule; the two must not disagree
// about the same graph. // about the same graph.
if (IsLevelPackage(Dependency)) if (IsLevelPackage(Dependency))
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetExportLayout.h" #include "AssetExportLayout.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetExportManifest.h" #include "AssetExportManifest.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetExportNaming.h" #include "AssetExportNaming.h"
#include "AssetUsageAuditCoreModule.h" #include "AssetUsageAuditCoreModule.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetImportScanner.h" #include "AssetImportScanner.h"
@@ -170,7 +170,7 @@ namespace AssetImportScanner
/** /**
* Package path implied by a file's position under the import root. * Package path implied by a file's position under the import root.
* *
* Correct for a MirrorTree export, which writes Game/Space/Art/SM_Rock.uasset, and a guess * Correct for a MirrorTree export, which writes Game/Art/SM_Rock.uasset, and a guess
* for anything else. Returns None when the result would not be a valid package name. * for anything else. Returns None when the result would not be a valid package name.
*/ */
FName PackageFromFolderStructure(const FString& RelativePath) FName PackageFromFolderStructure(const FString& RelativePath)
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetImporter.h" #include "AssetImporter.h"
@@ -146,7 +146,7 @@ namespace AssetImporter
continue; continue;
} }
// This project is Perforce-primary, so an existing target is very likely read-only. // Perforce-primary projects keep unopened files read-only, so an existing target is very likely read-only.
// Without clearing the flag the copy fails with an error that reads like a permissions // Without clearing the flag the copy fails with an error that reads like a permissions
// problem rather than "the file is not checked out". // problem rather than "the file is not checked out".
if (bTargetOnDisk && FileManager.IsReadOnly(*DestinationPath)) if (bTargetOnDisk && FileManager.IsReadOnly(*DestinationPath))
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetTypeFilter.h" #include "AssetTypeFilter.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditTypes.h" #include "AssetUsageAuditTypes.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditor.h" #include "AssetUsageAuditor.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageExporter.h" #include "AssetUsageExporter.h"
@@ -536,7 +536,7 @@ FAssetUsageExporter::FResult FAssetUsageExporter::CopyExpandedPackages(const TAr
const bool bWasPresent = FileManager.FileExists(*DestinationPath); const bool bWasPresent = FileManager.FileExists(*DestinationPath);
// This repository is Perforce-primary, so the source .uasset is very often read-only on // Perforce-primary repositories keep unopened files read-only, so the source .uasset is very often read-only on
// disk. Windows CopyFile carries that attribute across, which would make the export folder // disk. Windows CopyFile carries that attribute across, which would make the export folder
// read-only too - and then an Overwrite-policy re-export fails on its own previous output. // read-only too - and then an Overwrite-policy re-export fails on its own previous output.
// Clear the flag on both ends of the problem: before writing over an existing file, and // Clear the flag on both ends of the problem: before writing over an existing file, and
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageGraph.h" #include "AssetUsageGraph.h"
#include "AssetUsageAuditCoreModule.h" #include "AssetUsageAuditCoreModule.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsagePaths.h" #include "AssetUsagePaths.h"
#include "HAL/FileManager.h" #include "HAL/FileManager.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageReportWriter.h" #include "AssetUsageReportWriter.h"
#include "AssetUsageAuditCoreModule.h" #include "AssetUsageAuditCoreModule.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "IndirectReferenceScanner.h" #include "IndirectReferenceScanner.h"
#include "AssetUsageAuditCoreModule.h" #include "AssetUsageAuditCoreModule.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "LevelUsageResolver.h" #include "LevelUsageResolver.h"
@@ -243,8 +243,8 @@ void FLevelUsageResolver::TraverseLevel(int32 LevelIndex, const FLevelUsageResol
} }
// Reaching another World is NOT automatically a sublevel. An earlier version assumed it // Reaching another World is NOT automatically a sublevel. An earlier version assumed it
// was, and on WP_Main that assumption pulled the whole of L_MainLevel in through // was, and on WP_Example that assumption pulled the whole of L_Other in through
// BP_FirstPersonGameMode -> PDA_MenuSystemConfig: 9994 of 18136 rows belonged to a // BP_GameMode -> PDA_MenuConfig: 9994 of 18136 rows belonged to a
// different map. A crossing only counts when the reference comes from the level package // different map. A crossing only counts when the reference comes from the level package
// itself or from one of its external actor packages, which is the shape a streaming // itself or from one of its external actor packages, which is the shape a streaming
// sublevel or a Level Instance actually has. // sublevel or a Level Instance actually has.
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -73,8 +73,8 @@ namespace AssetDependencyClosure
* Maps reached through an ordinary asset rather than through a level, and therefore not * Maps reached through an ordinary asset rather than through a level, and therefore not
* followed - nor included. * followed - nor included.
* *
* The same rule FLevelUsageResolver applies, and for the same measured reason: on WP_Main * The same rule FLevelUsageResolver applies, and for the same measured reason: on WP_Example
* the chain BP_FirstPersonGameMode -> PDA_MenuSystemConfig -> L_MainLevel drags in 9994 * the chain BP_GameMode -> PDA_MenuConfig -> L_Other drags in 9994
* packages belonging to a different map. Without this the export dialog would quote an * packages belonging to a different map. Without this the export dialog would quote an
* honest number for a wrong set. * honest number for a wrong set.
* *
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -139,7 +139,7 @@ enum class EExportCollisionPolicy : uint8
*/ */
struct ASSETUSAGEAUDITCORE_API FAssetUsageRow struct ASSETUSAGEAUDITCORE_API FAssetUsageRow
{ {
/** Package name, e.g. /Game/Space/Art/SM_Rock. */ /** Package name, e.g. /Game/Art/SM_Rock. */
FName PackageName; FName PackageName;
/** Asset name without path. */ /** Asset name without path. */
@@ -154,7 +154,7 @@ struct ASSETUSAGEAUDITCORE_API FAssetUsageRow
*/ */
FTopLevelAssetPath GeneratedClassPath; FTopLevelAssetPath GeneratedClassPath;
/** Path relative to the project root, e.g. Content/Space/Art/SM_Rock.uasset. */ /** Path relative to the project root, e.g. Content/Art/SM_Rock.uasset. */
FString PathFromProjectRoot; FString PathFromProjectRoot;
EAssetUsageVerdict Verdict = EAssetUsageVerdict::Unknown; EAssetUsageVerdict Verdict = EAssetUsageVerdict::Unknown;
@@ -172,7 +172,7 @@ struct ASSETUSAGEAUDITCORE_API FAssetUsageRow
/** /**
* Human-readable chain explaining the verdict, e.g. * Human-readable chain explaining the verdict, e.g.
* "WP_Main -> __ExternalActors__/.../A2B -> BP_Rock -> SM_Rock". * "WP_Example -> __ExternalActors__/.../A2B -> BP_Rock -> SM_Rock".
* A verdict without a route is unactionable for an artist. * A verdict without a route is unactionable for an artist.
*/ */
FString Route; FString Route;
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -51,7 +51,7 @@ namespace AssetUsagePaths
/** /**
* Package name to a path relative to the project root, as the report requires. * Package name to a path relative to the project root, as the report requires.
* "/Game/Space/Art/SM_Rock" -> "Content/Space/Art/SM_Rock.uasset" * "/Game/Art/SM_Rock" -> "Content/Art/SM_Rock.uasset"
* *
* @param bIsLevel Chooses the .umap extension over .uasset. * @param bIsLevel Chooses the .umap extension over .uasset.
* @return Empty for packages with no project-relative form, e.g. /Script or /Engine. * @return Empty for packages with no project-relative form, e.g. /Script or /Engine.
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -9,7 +9,7 @@
*/ */
struct FIndirectReference struct FIndirectReference
{ {
/** Package name the literal resolved to, e.g. /Game/Space/Core/GameModes/BP_FirstPersonGameMode. */ /** Package name the literal resolved to, e.g. /Game/Core/GameModes/BP_GameMode. */
FName PackageName; FName PackageName;
/** Absolute path of the file the literal was found in. */ /** Absolute path of the file the literal was found in. */
@@ -28,7 +28,7 @@ struct FIndirectReference
* Finds asset paths referenced from text rather than from a package. * Finds asset paths referenced from text rather than from a package.
* *
* This exists because of a concrete, measured failure mode on this project: Config/DefaultEngine.ini * This exists because of a concrete, measured failure mode on this project: Config/DefaultEngine.ini
* names BP_FirstPersonGameMode, BP_MenuSystemGameInstance, RefinedMenuMap and Gyms_Geoda as plain * names BP_GameMode, BP_MenuGameInstance, RefinedMenuMap and Gyms_Geoda as plain
* strings. No asset references them, so the Asset Registry reports zero referencers and a naive * strings. No asset references them, so the Asset Registry reports zero referencers and a naive
* audit calls the project's GameMode unused. * audit calls the project's GameMode unused.
* *
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -37,8 +37,8 @@ struct FLevelUsageResolveOptions
* *
* Off by default, and that default is load-bearing. A map does not only reference its own * Off by default, and that default is load-bearing. A map does not only reference its own
* sublevels: anything it can reach may name an unrelated map, and on this project it does. * sublevels: anything it can reach may name an unrelated map, and on this project it does.
* WP_Main -> BP_FirstPersonGameMode -> PDA_MenuSystemConfig -> L_MainLevel drags in the whole * WP_Example -> BP_GameMode -> PDA_MenuConfig -> L_Other drags in the whole
* of L_MainLevel, which measured at 9994 of WP_Main's 18136 rows - two thirds of the answer * of L_Other, which measured at 9994 of WP_Example's 18136 rows - two thirds of the answer
* was another level's content. * was another level's content.
* *
* With this off, a foreign map is still reported as referenced, but its contents are attributed * With this off, a foreign map is still reported as referenced, but its contents are attributed
@@ -72,7 +72,7 @@ struct FLevelUsageStats
* Foreign maps referenced but deliberately not expanded into. * Foreign maps referenced but deliberately not expanded into.
* *
* A non-zero value here is the amount of another level's content that would otherwise have * A non-zero value here is the amount of another level's content that would otherwise have
* been attributed to this one. Worth surfacing: on WP_Main it was two thirds of the report. * been attributed to this one. Worth surfacing: on WP_Example it was two thirds of the report.
*/ */
int32 ForeignLevelsNotExpanded = 0; int32 ForeignLevelsNotExpanded = 0;
double ScanLevelAssetsSeconds = 0.0; double ScanLevelAssetsSeconds = 0.0;
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
using UnrealBuildTool; using UnrealBuildTool;
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditEditorModule.h" #include "AssetUsageAuditEditorModule.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditLibrary.h" #include "AssetUsageAuditLibrary.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditSettings.h" #include "AssetUsageAuditSettings.h"
@@ -11,8 +11,8 @@ UAssetUsageAuditSettings::UAssetUsageAuditSettings()
// third-party, so leaving these in makes the first run unreadable for an artist looking for // third-party, so leaving these in makes the first run unreadable for an artist looking for
// their own content. // their own content.
// //
// Deliberately NOT excluded, despite being a bought pack: Content/MenuSystemPro. It supplies // Deliberately NOT excluded, despite being a bought pack: Content/a purchased UI pack. It supplies
// BP_MenuSystemGameInstance, which DefaultEngine.ini names as the project's GameInstance. // BP_MenuGameInstance, which DefaultEngine.ini names as the project's GameInstance.
// Excluding it would hide a load-bearing asset from every report. // Excluding it would hide a load-bearing asset from every report.
ExcludedPackagePaths = { ExcludedPackagePaths = {
TEXT("Content/3rdParty"), TEXT("Content/3rdParty"),
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditUserSettings.h" #include "AssetUsageAuditUserSettings.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "SAssetDeleteDialog.h" #include "SAssetDeleteDialog.h"
@@ -178,7 +178,7 @@ public:
return SNew(STextBlock) return SNew(STextBlock)
.Text(LOCTEXT("ReadOnly", "read-only on disk")) .Text(LOCTEXT("ReadOnly", "read-only on disk"))
.ColorAndOpacity(FSlateColor(CautionColor)) .ColorAndOpacity(FSlateColor(CautionColor))
.ToolTipText(LOCTEXT("ReadOnlyTip", "The file is not writable. This project is Perforce-primary, so an unopened file is read-only and the delete will fail on it. Check it out first.")); .ToolTipText(LOCTEXT("ReadOnlyTip", "The file is not writable. Perforce-primary projects keep unopened files read-only, so an unopened file is read-only and the delete will fail on it. Check it out first."));
} }
return SNew(STextBlock) return SNew(STextBlock)
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "SAssetExportDialog.h" #include "SAssetExportDialog.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "SAssetImportDialog.h" #include "SAssetImportDialog.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "SAssetUsageAuditPanel.h" #include "SAssetUsageAuditPanel.h"
@@ -375,7 +375,7 @@ void SAssetUsageAuditPanel::Construct(const FArguments& InArgs)
} }
// Name the level when there is one, count them when there are several: // Name the level when there is one, count them when there are several:
// a button reading "WP_Main" is worth more than one reading "1 level". // a button reading "WP_Example" is worth more than one reading "1 level".
if (LevelFilters.Num() == 1) if (LevelFilters.Num() == 1)
{ {
return FText::FromString(FPackageName::GetShortName(*LevelFilters.CreateConstIterator())); return FText::FromString(FPackageName::GetShortName(*LevelFilters.CreateConstIterator()));
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -25,7 +25,7 @@ public:
/** /**
* Run a full audit and write JSON + CSV reports. * Run a full audit and write JSON + CSV reports.
* *
* @param LevelPackageNames Levels to analyse, e.g. "/Game/Space/Maps/WP_Main". Empty means all. * @param LevelPackageNames Levels to analyse, e.g. "/Game/Maps/WP_Example". Empty means all.
* @param OutputDirectory Destination. Empty uses the configured default. * @param OutputDirectory Destination. Empty uses the configured default.
* @param OutReportPath Absolute path of the JSON report on success. * @param OutReportPath Absolute path of the JSON report on success.
* @return true when both reports were written. * @return true when both reports were written.
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -10,7 +10,7 @@
/** /**
* Team-shared configuration, written to Config/DefaultEditor.ini. * Team-shared configuration, written to Config/DefaultEditor.ini.
* *
* That file is Perforce-tracked (only Plugins/** is p4-ignored), so the exclusion list and the * That file is normally under version control, so the exclusion list and the
* type presets are reviewable and shared. Per-user state - last filter, column layout - belongs * type presets are reviewable and shared. Per-user state - last filter, column layout - belongs
* in UAssetUsageAuditUserSettings instead, which stays in Saved/Config. * in UAssetUsageAuditUserSettings instead, which stays in Saved/Config.
*/ */
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#pragma once #pragma once
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
using UnrealBuildTool; using UnrealBuildTool;
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "Modules/ModuleManager.h" #include "Modules/ModuleManager.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetDeletionScan.h" #include "AssetDeletionScan.h"
@@ -399,7 +399,7 @@ void AssetDeletionScanSpec::Define()
return; return;
} }
// This project is Perforce-primary, so read-only is the normal state of an unopened // Perforce-primary projects keep unopened files read-only, so read-only is the normal state of an unopened
// file. Reporting it up front is the difference between a warning and a delete that // file. Reporting it up front is the difference between a warning and a delete that
// fails halfway through the selection. // fails halfway through the selection.
TestEqual(TEXT("matches the filesystem"), TestEqual(TEXT("matches the filesystem"),
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetDependencyClosure.h" #include "AssetDependencyClosure.h"
@@ -232,8 +232,8 @@ void AssetDependencyClosureSpec::Define()
It("does not follow a map that an ordinary asset merely mentions", [this]() It("does not follow a map that an ordinary asset merely mentions", [this]()
{ {
// The defect this guards against: exporting WP_Main pulled in the whole of L_MainLevel // The defect this guards against: exporting WP_Example pulled in the whole of L_Other
// through BP_FirstPersonGameMode -> PDA_MenuSystemConfig, 9994 packages belonging to a // through BP_GameMode -> PDA_MenuConfig, 9994 packages belonging to a
// different location. FLevelUsageResolver already refused that crossing; the closure // different location. FLevelUsageResolver already refused that crossing; the closure
// did not, so the two disagreed about the same graph. // did not, so the two disagreed about the same graph.
IAssetRegistry& Registry = ReadyRegistry(); IAssetRegistry& Registry = ReadyRegistry();
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageExporter.h" #include "AssetUsageExporter.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetExportLayout.h" #include "AssetExportLayout.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetExportManifest.h" #include "AssetExportManifest.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetExportNaming.h" #include "AssetExportNaming.h"
#include "AssetUsageAuditTypes.h" #include "AssetUsageAuditTypes.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetImportScanner.h" #include "AssetImportScanner.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageExporter.h" #include "AssetUsageExporter.h"
#include "AssetExportNaming.h" #include "AssetExportNaming.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageExporter.h" #include "AssetUsageExporter.h"
@@ -176,7 +176,7 @@ BEGIN_DEFINE_SPEC(AssetUsageExporterLiveSpec,
/** /**
* The OFPA level with the fewest external actors. * The OFPA level with the fewest external actors.
* *
* Smallest on purpose: WP_Main owns roughly 16000 external packages and copying those in a test * Smallest on purpose: WP_Example owns roughly 16000 external packages and copying those in a test
* would move gigabytes. Search stops as soon as something small enough turns up. * would move gigabytes. Search stops as soon as something small enough turns up.
*/ */
static FName FindSmallestOfpaLevel(int32& OutExternalCount) static FName FindSmallestOfpaLevel(int32& OutExternalCount)
@@ -254,7 +254,7 @@ void AssetUsageExporterLiveSpec::Define()
It("leaves the exported file writable even when the source is read-only", [this]() It("leaves the exported file writable even when the source is read-only", [this]()
{ {
// This project is Perforce-primary, so unopened files are read-only on disk and Windows // Perforce-primary projects keep unopened files read-only, so unopened files are read-only on disk and Windows
// CopyFile carries the attribute across. A read-only export folder is useless to an // CopyFile carries the attribute across. A read-only export folder is useless to an
// artist and breaks the exporter's own Overwrite policy on the next run. // artist and breaks the exporter's own Overwrite policy on the next run.
const FName Package = FindOrdinaryAssetPackage(); const FName Package = FindOrdinaryAssetPackage();
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageGraph.h" #include "AssetUsageGraph.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsagePaths.h" #include "AssetUsagePaths.h"
#include "Misc/AutomationTest.h" #include "Misc/AutomationTest.h"
@@ -16,27 +16,27 @@ void AssetUsagePathsSpec::Define()
{ {
It("detects an OFPA external actor package", [this]() It("detects an OFPA external actor package", [this]()
{ {
// Real shape from this project: /Game/__ExternalActors__/Space/Maps/WP_Main/8/AB/<22 chars> // Real shape from a live project: /Game/__ExternalActors__/Maps/WP_Example/8/AB/<22 chars>
TestTrue(TEXT("external actor"), TestTrue(TEXT("external actor"),
AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/__ExternalActors__/Space/Maps/WP_Main/8/AB/QWERTYUIOPASDFGHJKLZXC")))); AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/__ExternalActors__/Maps/WP_Example/8/AB/QWERTYUIOPASDFGHJKLZXC"))));
}); });
It("detects an external objects package", [this]() It("detects an external objects package", [this]()
{ {
TestTrue(TEXT("external object"), TestTrue(TEXT("external object"),
AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/__ExternalObjects__/Space/Maps/L_3i_Hub/1/2A/ABC")))); AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/__ExternalObjects__/Maps/L_3i_Hub/1/2A/ABC"))));
}); });
It("does not flag an ordinary content package", [this]() It("does not flag an ordinary content package", [this]()
{ {
TestFalse(TEXT("ordinary asset"), TestFalse(TEXT("ordinary asset"),
AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/Space/Art/SM_Rock")))); AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/Art/SM_Rock"))));
}); });
It("does not flag a level that merely lives next to external actors", [this]() It("does not flag a level that merely lives next to external actors", [this]()
{ {
TestFalse(TEXT("the level itself is not external"), TestFalse(TEXT("the level itself is not external"),
AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/Space/Maps/WP_Main")))); AssetUsagePaths::IsExternalPackage(FName(TEXT("/Game/Maps/WP_Example"))));
}); });
}); });
@@ -44,7 +44,7 @@ void AssetUsagePathsSpec::Define()
{ {
It("flags /Script packages", [this]() It("flags /Script packages", [this]()
{ {
TestTrue(TEXT("script"), AssetUsagePaths::IsScriptPackage(FName(TEXT("/Script/Space")))); TestTrue(TEXT("script"), AssetUsagePaths::IsScriptPackage(FName(TEXT("/Script/MyGame"))));
}); });
It("does not flag content that starts with the same letters", [this]() It("does not flag content that starts with the same letters", [this]()
@@ -123,7 +123,7 @@ void AssetUsagePathsSpec::Define()
{ {
const TArray<FString> Excluded; const TArray<FString> Excluded;
TestFalse(TEXT("nothing excluded"), TestFalse(TEXT("nothing excluded"),
AssetUsagePaths::IsPathExcluded(FName(TEXT("/Game/Space/Art/SM_Rock")), Excluded)); AssetUsagePaths::IsPathExcluded(FName(TEXT("/Game/Art/SM_Rock")), Excluded));
}); });
}); });
@@ -132,21 +132,21 @@ void AssetUsagePathsSpec::Define()
It("converts an asset package", [this]() It("converts an asset package", [this]()
{ {
TestEqual(TEXT("asset"), TestEqual(TEXT("asset"),
AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Game/Space/Art/SM_Rock")), false), AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Game/Art/SM_Rock")), false),
FString(TEXT("Content/Space/Art/SM_Rock.uasset"))); FString(TEXT("Content/Art/SM_Rock.uasset")));
}); });
It("uses the umap extension for levels", [this]() It("uses the umap extension for levels", [this]()
{ {
TestEqual(TEXT("level"), TestEqual(TEXT("level"),
AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Game/Space/Maps/WP_Main")), true), AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Game/Maps/WP_Example")), true),
FString(TEXT("Content/Space/Maps/WP_Main.umap"))); FString(TEXT("Content/Maps/WP_Example.umap")));
}); });
It("returns empty for script and engine packages rather than inventing a path", [this]() It("returns empty for script and engine packages rather than inventing a path", [this]()
{ {
TestTrue(TEXT("script"), TestTrue(TEXT("script"),
AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Script/Space")), false).IsEmpty()); AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Script/MyGame")), false).IsEmpty());
TestTrue(TEXT("engine"), TestTrue(TEXT("engine"),
AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Engine/BasicShapes/Cube")), false).IsEmpty()); AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Engine/BasicShapes/Cube")), false).IsEmpty());
}); });
@@ -154,8 +154,8 @@ void AssetUsagePathsSpec::Define()
It("converts an external actor package, which is still under /Game", [this]() It("converts an external actor package, which is still under /Game", [this]()
{ {
TestEqual(TEXT("external actor"), TestEqual(TEXT("external actor"),
AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Game/__ExternalActors__/Space/Maps/WP_Main/8/AB/ABC")), false), AssetUsagePaths::ToProjectRelativePath(FName(TEXT("/Game/__ExternalActors__/Maps/WP_Example/8/AB/ABC")), false),
FString(TEXT("Content/__ExternalActors__/Space/Maps/WP_Main/8/AB/ABC.uasset"))); FString(TEXT("Content/__ExternalActors__/Maps/WP_Example/8/AB/ABC.uasset")));
}); });
}); });
} }
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageReportWriter.h" #include "AssetUsageReportWriter.h"
#include "AssetUsageAuditor.h" #include "AssetUsageAuditor.h"
@@ -17,7 +17,7 @@ namespace
FAssetUsageAuditResult Result; FAssetUsageAuditResult Result;
Result.Header.GeneratedAt = FDateTime(2026, 9, 2, 12, 0, 0); Result.Header.GeneratedAt = FDateTime(2026, 9, 2, 12, 0, 0);
Result.Header.ProjectName = TEXT("Space"); Result.Header.ProjectName = TEXT("MyProject");
Result.Header.EngineVersion = TEXT("5.6.1"); Result.Header.EngineVersion = TEXT("5.6.1");
Result.Header.ToolVersion = TEXT("0.1"); Result.Header.ToolVersion = TEXT("0.1");
Result.Header.LevelsScanned = 2; Result.Header.LevelsScanned = 2;
@@ -25,22 +25,22 @@ namespace
Result.Header.AppliedFilters.Add(TEXT("Types: StaticMesh")); Result.Header.AppliedFilters.Add(TEXT("Types: StaticMesh"));
FAssetUsageRow& Used = Result.Rows.AddDefaulted_GetRef(); FAssetUsageRow& Used = Result.Rows.AddDefaulted_GetRef();
Used.PackageName = FName(TEXT("/Game/Space/Art/SM_Rock")); Used.PackageName = FName(TEXT("/Game/Art/SM_Rock"));
Used.AssetName = FName(TEXT("SM_Rock")); Used.AssetName = FName(TEXT("SM_Rock"));
Used.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.StaticMesh")); Used.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.StaticMesh"));
Used.PathFromProjectRoot = TEXT("Content/Space/Art/SM_Rock.uasset"); Used.PathFromProjectRoot = TEXT("Content/Art/SM_Rock.uasset");
Used.Verdict = EAssetUsageVerdict::UsedOnLevel; Used.Verdict = EAssetUsageVerdict::UsedOnLevel;
Used.Levels = { FName(TEXT("/Game/Space/Maps/WP_Main")), FName(TEXT("/Game/Space/Maps/L_3i_Hub")) }; Used.Levels = { FName(TEXT("/Game/Maps/WP_Example")), FName(TEXT("/Game/Maps/L_3i_Hub")) };
Used.HardReferenceCount = 3; Used.HardReferenceCount = 3;
Used.SoftReferenceCount = 1; Used.SoftReferenceCount = 1;
Used.Provenance = EAssetUsageProvenance::HardReference | EAssetUsageProvenance::ExternalActor; Used.Provenance = EAssetUsageProvenance::HardReference | EAssetUsageProvenance::ExternalActor;
Used.Route = TEXT("WP_Main -> BP_Rock -> SM_Rock"); Used.Route = TEXT("WP_Example -> BP_Rock -> SM_Rock");
FAssetUsageRow& Unused = Result.Rows.AddDefaulted_GetRef(); FAssetUsageRow& Unused = Result.Rows.AddDefaulted_GetRef();
Unused.PackageName = FName(TEXT("/Game/Space/Art/SM_Orphan")); Unused.PackageName = FName(TEXT("/Game/Art/SM_Orphan"));
Unused.AssetName = FName(TEXT("SM_Orphan")); Unused.AssetName = FName(TEXT("SM_Orphan"));
Unused.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.StaticMesh")); Unused.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.StaticMesh"));
Unused.PathFromProjectRoot = TEXT("Content/Space/Art/SM_Orphan.uasset"); Unused.PathFromProjectRoot = TEXT("Content/Art/SM_Orphan.uasset");
Unused.Verdict = EAssetUsageVerdict::Unreferenced; Unused.Verdict = EAssetUsageVerdict::Unreferenced;
return Result; return Result;
@@ -123,7 +123,7 @@ void AssetUsageReportWriterSpec::Define()
const FString Csv = FAssetUsageReportWriter::BuildCsv(Sample, Options); const FString Csv = FAssetUsageReportWriter::BuildCsv(Sample, Options);
TestTrue(TEXT("levels joined"), TestTrue(TEXT("levels joined"),
Csv.Contains(TEXT("/Game/Space/Maps/WP_Main|/Game/Space/Maps/L_3i_Hub"))); Csv.Contains(TEXT("/Game/Maps/WP_Example|/Game/Maps/L_3i_Hub")));
}); });
It("carries the verdict verbatim", [this]() It("carries the verdict verbatim", [this]()
@@ -172,7 +172,7 @@ void AssetUsageReportWriterSpec::Define()
TestTrue(TEXT("header object"), Root->TryGetObjectField(TEXT("header"), Header)); TestTrue(TEXT("header object"), Root->TryGetObjectField(TEXT("header"), Header));
if (Header) if (Header)
{ {
TestEqual(TEXT("project"), (*Header)->GetStringField(TEXT("project")), FString(TEXT("Space"))); TestEqual(TEXT("project"), (*Header)->GetStringField(TEXT("project")), FString(TEXT("MyProject")));
TestEqual(TEXT("levelsScanned"), (*Header)->GetIntegerField(TEXT("levelsScanned")), 2); TestEqual(TEXT("levelsScanned"), (*Header)->GetIntegerField(TEXT("levelsScanned")), 2);
} }
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditor.h" #include "AssetUsageAuditor.h"
#include "AssetUsageGraph.h" #include "AssetUsageGraph.h"
@@ -45,7 +45,7 @@ void AssetUsageVerdictSpec::Define()
It("reports ReferencedFromConfigOrSource when only a text literal names it", [this]() It("reports ReferencedFromConfigOrSource when only a text literal names it", [this]()
{ {
// This is the BP_FirstPersonGameMode case: no asset references it, only // This is the BP_GameMode case: no asset references it, only
// Config/DefaultEngine.ini does. Reporting it Unreferenced would be a real defect. // Config/DefaultEngine.ini does. Reporting it Unreferenced would be a real defect.
const EAssetUsageVerdict Verdict = FAssetUsageAuditor::ClassifyVerdict(false, false, true, false); const EAssetUsageVerdict Verdict = FAssetUsageAuditor::ClassifyVerdict(false, false, true, false);
@@ -126,7 +126,7 @@ void AssetUsageVerdictSpec::Define()
{ {
// A level depends on the whole table, so every row looks used even when none is. // A level depends on the whole table, so every row looks used even when none is.
FAssetUsageNode Node; FAssetUsageNode Node;
Node.PackageName = FName(TEXT("/Game/Space/Data/DT_Weapons")); Node.PackageName = FName(TEXT("/Game/Data/DT_Weapons"));
Node.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.DataTable")); Node.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.DataTable"));
TestTrue(TEXT("DataTable is a blind spot"), FAssetUsageAuditor::IsRegistryBlindSpot(Node)); TestTrue(TEXT("DataTable is a blind spot"), FAssetUsageAuditor::IsRegistryBlindSpot(Node));
@@ -135,7 +135,7 @@ void AssetUsageVerdictSpec::Define()
It("does not flag an ordinary static mesh", [this]() It("does not flag an ordinary static mesh", [this]()
{ {
FAssetUsageNode Node; FAssetUsageNode Node;
Node.PackageName = FName(TEXT("/Game/Space/Art/SM_Rock")); Node.PackageName = FName(TEXT("/Game/Art/SM_Rock"));
Node.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.StaticMesh")); Node.ClassPath = FTopLevelAssetPath(TEXT("/Script/Engine.StaticMesh"));
TestFalse(TEXT("StaticMesh is not a blind spot"), FAssetUsageAuditor::IsRegistryBlindSpot(Node)); TestFalse(TEXT("StaticMesh is not a blind spot"), FAssetUsageAuditor::IsRegistryBlindSpot(Node));
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "IndirectReferenceScanner.h" #include "IndirectReferenceScanner.h"
#include "Misc/AutomationTest.h" #include "Misc/AutomationTest.h"
@@ -28,70 +28,70 @@ void IndirectReferenceScannerSpec::Define()
It("finds the GameMode from DefaultEngine.ini and strips the _C class suffix", [this]() It("finds the GameMode from DefaultEngine.ini and strips the _C class suffix", [this]()
{ {
const TArray<FString> Found = Extract( const TArray<FString> Found = Extract(
TEXT("GlobalDefaultGameMode=/Game/Space/Core/GameModes/BP_FirstPersonGameMode.BP_FirstPersonGameMode_C")); TEXT("GlobalDefaultGameMode=/Game/Core/GameModes/BP_GameMode.BP_GameMode_C"));
TestEqual(TEXT("one hit"), Found.Num(), 1); TestEqual(TEXT("one hit"), Found.Num(), 1);
if (Found.Num() == 1) if (Found.Num() == 1)
{ {
TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Space/Core/GameModes/BP_FirstPersonGameMode"))); TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Core/GameModes/BP_GameMode")));
} }
}); });
It("finds the GameInstance from MenuSystemPro", [this]() It("finds the GameInstance from a purchased pack", [this]()
{ {
const TArray<FString> Found = Extract( const TArray<FString> Found = Extract(
TEXT("GameInstanceClass=/Game/MenuSystemPro/Blueprints/GameFramework/BP_MenuSystemGameInstance.BP_MenuSystemGameInstance_C")); TEXT("GameInstanceClass=/Game/UIPack/Blueprints/GameFramework/BP_MenuGameInstance.BP_MenuGameInstance_C"));
TestEqual(TEXT("one hit"), Found.Num(), 1); TestEqual(TEXT("one hit"), Found.Num(), 1);
if (Found.Num() == 1) if (Found.Num() == 1)
{ {
TestEqual(TEXT("package"), Found[0], TestEqual(TEXT("package"), Found[0],
FString(TEXT("/Game/MenuSystemPro/Blueprints/GameFramework/BP_MenuSystemGameInstance"))); FString(TEXT("/Game/UIPack/Blueprints/GameFramework/BP_MenuGameInstance")));
} }
}); });
It("finds a map reference with an object suffix", [this]() It("finds a map reference with an object suffix", [this]()
{ {
const TArray<FString> Found = Extract(TEXT("EditorStartupMap=/Game/Space/Maps/Gyms/Gyms_Geoda.Gyms_Geoda")); const TArray<FString> Found = Extract(TEXT("EditorStartupMap=/Game/Maps/Gyms/Gyms_Geoda.Gyms_Geoda"));
TestEqual(TEXT("one hit"), Found.Num(), 1); TestEqual(TEXT("one hit"), Found.Num(), 1);
if (Found.Num() == 1) if (Found.Num() == 1)
{ {
TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Space/Maps/Gyms/Gyms_Geoda"))); TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Maps/Gyms/Gyms_Geoda")));
} }
}); });
It("finds a bare package path with no object suffix", [this]() It("finds a bare package path with no object suffix", [this]()
{ {
const TArray<FString> Found = Extract(TEXT("GameDefaultMap=/Game/Space/Maps/RefinedMenuMap")); const TArray<FString> Found = Extract(TEXT("GameDefaultMap=/Game/Maps/RefinedMenuMap"));
TestEqual(TEXT("one hit"), Found.Num(), 1); TestEqual(TEXT("one hit"), Found.Num(), 1);
if (Found.Num() == 1) if (Found.Num() == 1)
{ {
TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Space/Maps/RefinedMenuMap"))); TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Maps/RefinedMenuMap")));
} }
}); });
It("strips surrounding quotes and parentheses from an ini struct value", [this]() It("strips surrounding quotes and parentheses from an ini struct value", [this]()
{ {
const TArray<FString> Found = Extract(TEXT("+DirectoriesToAlwaysCook=(Path=\"/Game/Space/Art\")")); const TArray<FString> Found = Extract(TEXT("+DirectoriesToAlwaysCook=(Path=\"/Game/Art\")"));
TestEqual(TEXT("one hit"), Found.Num(), 1); TestEqual(TEXT("one hit"), Found.Num(), 1);
if (Found.Num() == 1) if (Found.Num() == 1)
{ {
TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Space/Art"))); TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Art")));
} }
}); });
It("handles a C++ literal wrapped in TEXT()", [this]() It("handles a C++ literal wrapped in TEXT()", [this]()
{ {
const TArray<FString> Found = Extract( const TArray<FString> Found = Extract(
TEXT("static const FSoftObjectPath Path(TEXT(\"/Game/Space/Art/SM_Rock\"));")); TEXT("static const FSoftObjectPath Path(TEXT(\"/Game/Art/SM_Rock\"));"));
TestEqual(TEXT("one hit"), Found.Num(), 1); TestEqual(TEXT("one hit"), Found.Num(), 1);
if (Found.Num() == 1) if (Found.Num() == 1)
{ {
TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Space/Art/SM_Rock"))); TestEqual(TEXT("package"), Found[0], FString(TEXT("/Game/Art/SM_Rock")));
} }
}); });
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsagePaths.h" #include "AssetUsagePaths.h"
@@ -1,4 +1,4 @@
// NextGenium 2026. Asset Usage Audit. // MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsageAuditTypes.h" #include "AssetUsageAuditTypes.h"
#include "AssetUsagePaths.h" #include "AssetUsagePaths.h"