Files
AssetUsageAudit/Source/AssetUsageAuditCore/Private/AssetExportNaming.cpp
T
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

133 lines
4.0 KiB
C++

// MagentaDolphin 2026. Asset Usage Audit.
#include "AssetExportNaming.h"
#include "AssetUsageAuditCoreModule.h"
namespace AssetExportNaming
{
namespace
{
/** Guard against a pathological folder where every candidate is taken. */
constexpr int32 MaxCollisionAttempts = 100000;
bool IsAllDigits(const FString& Value)
{
if (Value.IsEmpty())
{
return false;
}
for (const TCHAR Char : Value)
{
if (!FChar::IsDigit(Char))
{
return false;
}
}
return true;
}
}
void SplitTrailingIndex(const FString& BaseName, FString& OutStem, int32& OutIndex, int32& OutPadWidth)
{
OutStem = BaseName;
OutIndex = INDEX_NONE;
OutPadWidth = 0;
int32 SeparatorPos = INDEX_NONE;
if (!BaseName.FindLastChar(TEXT('_'), SeparatorPos))
{
return;
}
// A leading underscore is part of the name, not a separator: "_1" has no stem.
if (SeparatorPos <= 0)
{
return;
}
const FString Suffix = BaseName.Mid(SeparatorPos + 1);
// "Foo_" has an empty suffix, "SM_Rock_02_v3" has a non-numeric one. Neither is an index.
if (!IsAllDigits(Suffix))
{
return;
}
OutStem = BaseName.Left(SeparatorPos);
OutIndex = FCString::Atoi(*Suffix);
OutPadWidth = Suffix.Len();
}
FString ComposeIndexedName(const FString& Stem, int32 Index, int32 PadWidth)
{
if (Index == INDEX_NONE)
{
return Stem;
}
FString Digits = FString::FromInt(Index);
// Preserve the original zero padding, but never truncate: Foo_09 -> Foo_10, Foo_99 -> Foo_100.
while (Digits.Len() < PadWidth)
{
Digits.InsertAt(0, TEXT('0'));
}
return FString::Printf(TEXT("%s_%s"), *Stem, *Digits);
}
FString ResolveCollision(const FString& DesiredFileName, EExportCollisionPolicy Policy, FExistsPredicate Exists)
{
if (Policy == EExportCollisionPolicy::Overwrite)
{
return DesiredFileName;
}
if (!Exists(DesiredFileName))
{
return DesiredFileName;
}
// Split off the extension so the index lands before it: SM_Rock.uasset -> SM_Rock_1.uasset.
// Note this splits on the LAST dot, so a name like "M_Glass.v2.uasset" keeps "M_Glass.v2" as the base.
FString BaseName = DesiredFileName;
FString Extension;
int32 DotPos = INDEX_NONE;
if (DesiredFileName.FindLastChar(TEXT('.'), DotPos) && DotPos > 0)
{
BaseName = DesiredFileName.Left(DotPos);
Extension = DesiredFileName.Mid(DotPos); // includes the dot
}
FString Stem;
int32 Index = INDEX_NONE;
int32 PadWidth = 0;
SplitTrailingIndex(BaseName, Stem, Index, PadWidth);
// No trailing index yet: start at _1. Otherwise increment what is already there,
// rather than appending a second suffix and producing Foo_1_1.
int32 NextIndex = (Index == INDEX_NONE) ? 1 : Index + 1;
const int32 EffectivePadWidth = (Index == INDEX_NONE) ? 1 : PadWidth;
for (int32 Attempt = 0; Attempt < MaxCollisionAttempts; ++Attempt, ++NextIndex)
{
const FString Candidate = ComposeIndexedName(Stem, NextIndex, EffectivePadWidth) + Extension;
if (!Exists(Candidate))
{
return Candidate;
}
}
// Every candidate taken. Returning the desired name would silently overwrite, which is the
// one outcome the Index policy exists to prevent - so fail loudly instead.
UE_LOG(LogAssetUsageAudit, Error,
TEXT("Could not find a free filename for '%s' after %d attempts. Export of this asset will be skipped."),
*DesiredFileName, MaxCollisionAttempts);
return FString();
}
}