feat: import assets from an external folder
Reverse direction of the export: read a folder of .uasset files, recover each one's original package path, and bring the chosen ones back. Recovering the path is the hard part - a reference inside a .uasset names its target by full package path, so a file only works when restored to where it came from. Four sources are tried and the one used is shown per row: our manifest, the package header, the folder structure, or nothing. The package header carries the name on this project's assets, measured, so a flat export without a manifest still restores correctly. The engine notes the field is not always written, hence the chain rather than one check. Import refuses to destroy work: existing assets are skipped unless overwrite is explicitly on, and a package loaded in the editor is never replaced. Also verifies Migrate on real data for the first time - asset and its dependency land at the correct package paths. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// NextGenium 2026. Asset Usage Audit.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class IAssetRegistry;
|
||||
|
||||
/**
|
||||
* Reads a folder of exported .uasset files and works out where each one would have to go.
|
||||
*
|
||||
* The hard part is not copying files, it is knowing the package path to copy them to. A reference
|
||||
* inside a .uasset names its target by full package path, so an imported asset only resolves when
|
||||
* it is restored to the path it came from. Getting that path wrong does not fail loudly - it
|
||||
* produces an asset that loads with missing references, which is discovered much later.
|
||||
*
|
||||
* Four sources are tried in order, and the one used is reported per file so the UI can show it:
|
||||
*
|
||||
* 1. Our own manifest. Authoritative: we wrote it from what the exporter actually wrote.
|
||||
* 2. The package header. FPackageFileSummary carries "the package name the file was last saved
|
||||
* with" - but the engine's own AssetHeaderPatcher notes it is not always serialised and falls
|
||||
* back when it is empty or "None", so this cannot be trusted blindly either.
|
||||
* 3. The folder structure under the import root, which is exactly right for a MirrorTree export
|
||||
* and a guess for anything else.
|
||||
* 4. Nothing. The file can still be imported, but only into a folder the user names, and its
|
||||
* references will not resolve. Said plainly rather than papered over.
|
||||
*/
|
||||
namespace AssetImportScanner
|
||||
{
|
||||
/** Where a candidate's target package path came from. Shown per row; not an implementation detail. */
|
||||
enum class EPackageNameSource : uint8
|
||||
{
|
||||
/** From AssetUsageAudit.manifest.json written beside the files. */
|
||||
Manifest,
|
||||
|
||||
/** From FPackageFileSummary::PackageName inside the .uasset. */
|
||||
PackageHeader,
|
||||
|
||||
/** Inferred from the file's position under the import root. Correct for a mirrored export. */
|
||||
FolderStructure,
|
||||
|
||||
/** Not recoverable. Importing this file cannot restore its references. */
|
||||
Unresolved
|
||||
};
|
||||
|
||||
ASSETUSAGEAUDITCORE_API const TCHAR* LexToString(EPackageNameSource Source);
|
||||
|
||||
struct FCandidate
|
||||
{
|
||||
/** Absolute path of the file in the source folder. */
|
||||
FString SourceFile;
|
||||
|
||||
/** Path relative to the import root, for display. */
|
||||
FString RelativePath;
|
||||
|
||||
/** Package it would be restored to. None when Unresolved. */
|
||||
FName TargetPackage;
|
||||
|
||||
EPackageNameSource NameSource = EPackageNameSource::Unresolved;
|
||||
|
||||
/**
|
||||
* A package of that name already exists in this project.
|
||||
*
|
||||
* Never imported silently: overwriting is how an import tool destroys someone's work, and
|
||||
* the file on disk gives no hint that it was about to happen.
|
||||
*/
|
||||
bool bTargetExists = false;
|
||||
|
||||
/** Packages this asset referenced at export time. Only known from a manifest. */
|
||||
TArray<FName> Dependencies;
|
||||
|
||||
/**
|
||||
* Dependencies that are neither in this folder nor already in the project.
|
||||
*
|
||||
* A non-zero count means the asset will import and then open with something missing.
|
||||
*/
|
||||
int32 MissingDependencies = 0;
|
||||
};
|
||||
|
||||
struct FOptions
|
||||
{
|
||||
/** Folder to read. Scanned recursively. */
|
||||
FString SourceDirectory;
|
||||
};
|
||||
|
||||
struct ASSETUSAGEAUDITCORE_API FStats
|
||||
{
|
||||
bool bManifestFound = false;
|
||||
|
||||
int32 FilesFound = 0;
|
||||
int32 FromManifest = 0;
|
||||
int32 FromPackageHeader = 0;
|
||||
int32 FromFolderStructure = 0;
|
||||
int32 Unresolved = 0;
|
||||
|
||||
/** Candidates whose target already exists in the project. */
|
||||
int32 WouldOverwrite = 0;
|
||||
|
||||
FString Summarise() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scan a folder for importable packages.
|
||||
*
|
||||
* Reads only package headers, never loads an asset: a folder can hold gigabytes, and the list
|
||||
* has to appear immediately for the user to tick through.
|
||||
*/
|
||||
ASSETUSAGEAUDITCORE_API TArray<FCandidate> Scan(
|
||||
IAssetRegistry& Registry,
|
||||
const FOptions& Options,
|
||||
FStats& OutStats);
|
||||
|
||||
/**
|
||||
* The package name a .uasset was saved with, or empty when the file does not carry one.
|
||||
*
|
||||
* Empty is a normal answer, not an error: the field is not always serialised. Callers must have
|
||||
* a fallback rather than treating an empty result as a corrupt file.
|
||||
*/
|
||||
ASSETUSAGEAUDITCORE_API FString ReadPackageNameFromFile(const FString& FilePath);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// NextGenium 2026. Asset Usage Audit.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AssetImportScanner.h"
|
||||
|
||||
class IAssetRegistry;
|
||||
|
||||
/**
|
||||
* Copies scanned files into the project at their recovered package paths.
|
||||
*
|
||||
* A byte copy is enough: a .uasset is self-contained and its references are resolved by package
|
||||
* path, so putting the file where the path says produces a working asset without loading anything.
|
||||
* That is also why the scanner's job - deciding the path - is the hard half and this is the easy one.
|
||||
*
|
||||
* What this refuses to do matters more than what it does. Importing is the one operation here that
|
||||
* can destroy work: writing over /Game/Art/SM_Rock replaces whatever the project had under that
|
||||
* name, with no undo and no trace. So an existing target is skipped unless overwriting was asked
|
||||
* for explicitly, and a package already loaded in the editor is never written over at all.
|
||||
*/
|
||||
namespace AssetImporter
|
||||
{
|
||||
struct FOptions
|
||||
{
|
||||
/**
|
||||
* Replace assets that already exist at the target path.
|
||||
*
|
||||
* Off by default and deliberately awkward to turn on: the scanner reports the count up
|
||||
* front so the choice is made knowingly rather than discovered afterwards.
|
||||
*/
|
||||
bool bOverwriteExisting = false;
|
||||
|
||||
/**
|
||||
* Where to put files whose package path could not be recovered, e.g. "/Game/Imported".
|
||||
*
|
||||
* Empty means skip them. They can be imported, but their references will not resolve, so
|
||||
* putting them somewhere by default would quietly fill the project with broken assets.
|
||||
*/
|
||||
FString UnresolvedDestinationPath;
|
||||
|
||||
/** Called as (Done, Total). Return false to stop; files already written are kept. */
|
||||
TFunction<bool(int32, int32)> OnProgress;
|
||||
};
|
||||
|
||||
struct ASSETUSAGEAUDITCORE_API FResult
|
||||
{
|
||||
bool bSuccess = false;
|
||||
|
||||
int32 FilesImported = 0;
|
||||
|
||||
/** Target existed and overwriting was not requested. */
|
||||
int32 SkippedExisting = 0;
|
||||
|
||||
/** No recoverable package path and no fallback folder was given. */
|
||||
int32 SkippedUnresolved = 0;
|
||||
|
||||
/**
|
||||
* Target package is loaded in this editor session.
|
||||
*
|
||||
* Counted separately because it is not a user choice to make: replacing the file under a
|
||||
* loaded package leaves the editor holding stale objects that will be saved back over the
|
||||
* new file. The asset has to be closed first.
|
||||
*/
|
||||
int32 SkippedLoaded = 0;
|
||||
|
||||
bool bCancelled = false;
|
||||
|
||||
TArray<FString> Errors;
|
||||
|
||||
/** Absolute paths written, for the Asset Registry rescan that makes them visible. */
|
||||
TArray<FString> ImportedFiles;
|
||||
|
||||
FString Summarise() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Import the given candidates.
|
||||
*
|
||||
* Only candidates the caller wants should be passed in - this applies no filtering of its own
|
||||
* beyond the safety rules above.
|
||||
*/
|
||||
ASSETUSAGEAUDITCORE_API FResult Import(
|
||||
IAssetRegistry& Registry,
|
||||
const TArray<AssetImportScanner::FCandidate>& Candidates,
|
||||
const FOptions& Options);
|
||||
}
|
||||
Reference in New Issue
Block a user