1802 lines
61 KiB
C++
1802 lines
61 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/*=============================================================================
|
|
UObjectHash.cpp: Unreal object name hashes
|
|
=============================================================================*/
|
|
|
|
#include "Misc/PackageName.h"
|
|
#include "Containers/StringView.h"
|
|
#include "GenericPlatform/GenericPlatformFile.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Stats/Stats.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Misc/App.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/PackageFileSummary.h"
|
|
#include "UObject/Linker.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
#include "Internationalization/PackageLocalizationManager.h"
|
|
#include "HAL/CriticalSection.h"
|
|
#include "HAL/ThreadHeartBeat.h"
|
|
#include "Misc/AutomationTest.h"
|
|
#include "Misc/PathViews.h"
|
|
#include "Misc/ScopeRWLock.h"
|
|
#include "Misc/StringBuilder.h"
|
|
#include "IO/IoDispatcher.h"
|
|
#include "ProfilingDebugging/LoadTimeTracker.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogPackageName, Log, All);
|
|
|
|
FString FPackageName::AssetPackageExtension = TEXT(".uasset");
|
|
FString FPackageName::MapPackageExtension = TEXT(".umap");
|
|
FString FPackageName::TextAssetPackageExtension = TEXT(".utxt");
|
|
FString FPackageName::TextMapPackageExtension = TEXT(".utxtmap");
|
|
static FRWLock ContentMountPointCriticalSection;
|
|
|
|
/** Event that is triggered when a new content path is mounted */
|
|
FPackageName::FOnContentPathMountedEvent FPackageName::OnContentPathMountedEvent;
|
|
|
|
/** Event that is triggered when a content path is dismounted */
|
|
FPackageName::FOnContentPathDismountedEvent FPackageName::OnContentPathDismountedEvent;
|
|
|
|
/** Delegate used to check whether a package exist without using the filesystem. */
|
|
FPackageName::FDoesPackageExistOverride FPackageName::DoesPackageExistOverrideDelegate;
|
|
|
|
namespace PackageNameConstants
|
|
{
|
|
// Minimum theoretical package name length ("/A/B") is 4
|
|
const int32 MinPackageNameLength = 4;
|
|
} // namespace PackageNameConstants
|
|
|
|
bool FPackageName::IsShortPackageName(FStringView PossiblyLongName)
|
|
{
|
|
// Long names usually have / as first character so check from the front
|
|
for (TCHAR Char: PossiblyLongName)
|
|
{
|
|
if (Char == '/')
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FPackageName::IsShortPackageName(const FString& PossiblyLongName)
|
|
{
|
|
return IsShortPackageName(FStringView(PossiblyLongName));
|
|
}
|
|
|
|
bool FPackageName::IsShortPackageName(FName PossiblyLongName)
|
|
{
|
|
// Only get "plain" part of the name. The number suffix, e.g. "_123", can't contain slashes.
|
|
TCHAR Buffer[NAME_SIZE];
|
|
uint32 Len = PossiblyLongName.GetPlainNameString(Buffer);
|
|
return IsShortPackageName(FStringView(Buffer, static_cast<int32>(Len)));
|
|
}
|
|
|
|
FString FPackageName::GetShortName(const FString& LongName)
|
|
{
|
|
// Get everything after the last slash
|
|
int32 IndexOfLastSlash = INDEX_NONE;
|
|
LongName.FindLastChar('/', IndexOfLastSlash);
|
|
return LongName.Mid(IndexOfLastSlash + 1);
|
|
}
|
|
|
|
FString FPackageName::GetShortName(const UPackage* Package)
|
|
{
|
|
check(Package != NULL);
|
|
return GetShortName(Package->GetName());
|
|
}
|
|
|
|
FString FPackageName::GetShortName(const FName& LongName)
|
|
{
|
|
return GetShortName(LongName.ToString());
|
|
}
|
|
|
|
FString FPackageName::GetShortName(const TCHAR* LongName)
|
|
{
|
|
return GetShortName(FString(LongName));
|
|
}
|
|
|
|
FName FPackageName::GetShortFName(const FString& LongName)
|
|
{
|
|
return GetShortFName(*LongName);
|
|
}
|
|
|
|
FName FPackageName::GetShortFName(const FName& LongName)
|
|
{
|
|
TCHAR LongNameStr[FName::StringBufferSize];
|
|
LongName.ToString(LongNameStr);
|
|
|
|
if (const TCHAR* Slash = FCString::Strrchr(LongNameStr, '/'))
|
|
{
|
|
return FName(Slash + 1);
|
|
}
|
|
|
|
return LongName;
|
|
}
|
|
|
|
FName FPackageName::GetShortFName(const TCHAR* LongName)
|
|
{
|
|
if (LongName == nullptr)
|
|
{
|
|
return FName();
|
|
}
|
|
|
|
if (const TCHAR* Slash = FCString::Strrchr(LongName, '/'))
|
|
{
|
|
return FName(Slash + 1);
|
|
}
|
|
|
|
return FName(LongName);
|
|
}
|
|
|
|
bool FPackageName::TryConvertGameRelativePackagePathToLocalPath(FStringView RelativePackagePath, FString& OutLocalPath)
|
|
{
|
|
if (RelativePackagePath.StartsWith(TEXT("/"), ESearchCase::CaseSensitive))
|
|
{
|
|
// If this starts with /, this includes a root like /engine
|
|
FString AbsolutePackagePath(RelativePackagePath);
|
|
if (FPackageName::TryConvertLongPackageNameToFilename(AbsolutePackagePath, OutLocalPath))
|
|
{
|
|
return true;
|
|
}
|
|
// Workaround a problem with TryConvertLongPackageNameToFilename: If the PackagePath is a content root itself (/Some/Content/Root)
|
|
// and is missing a terminating /, it will not match the existing content root which does have the / (/Some/Content/Root/)
|
|
if (!AbsolutePackagePath.EndsWith(TEXT("/")))
|
|
{
|
|
AbsolutePackagePath = AbsolutePackagePath + TEXT("/");
|
|
return FPackageName::TryConvertLongPackageNameToFilename(AbsolutePackagePath, OutLocalPath);
|
|
}
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// This is relative to /game
|
|
const FString AbsoluteGameContentDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
|
|
OutLocalPath = AbsoluteGameContentDir / FString(RelativePackagePath);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
struct FPathPair
|
|
{
|
|
// The virtual path (e.g., "/Engine/")
|
|
const FString RootPath;
|
|
|
|
// The physical relative path (e.g., "../../../Engine/Content/")
|
|
const FString ContentPath;
|
|
|
|
bool operator==(const FPathPair& Other) const
|
|
{
|
|
return RootPath == Other.RootPath && ContentPath == Other.ContentPath;
|
|
}
|
|
|
|
// Construct a path pair
|
|
FPathPair(const FString& InRootPath, const FString& InContentPath)
|
|
: RootPath(InRootPath), ContentPath(InContentPath)
|
|
{
|
|
}
|
|
};
|
|
|
|
struct FLongPackagePathsSingleton
|
|
{
|
|
FString ConfigRootPath;
|
|
FString EngineRootPath;
|
|
FString GameRootPath;
|
|
FString ScriptRootPath;
|
|
FString ExtraRootPath;
|
|
FString MemoryRootPath;
|
|
FString TempRootPath;
|
|
TArray<FString> MountPointRootPaths;
|
|
|
|
FString EngineContentPath;
|
|
FString ContentPathShort;
|
|
FString EngineShadersPath;
|
|
FString EngineShadersPathShort;
|
|
FString GameContentPath;
|
|
FString GameConfigPath;
|
|
FString GameScriptPath;
|
|
FString GameExtraPath;
|
|
FString GameSavedPath;
|
|
FString GameContentPathRebased;
|
|
FString GameConfigPathRebased;
|
|
FString GameScriptPathRebased;
|
|
FString GameExtraPathRebased;
|
|
FString GameSavedPathRebased;
|
|
|
|
//@TODO: Can probably consolidate these into a single array, if it weren't for EngineContentPathShort
|
|
TArray<FPathPair> ContentRootToPath;
|
|
TArray<FPathPair> ContentPathToRoot;
|
|
|
|
// singleton
|
|
static FLongPackagePathsSingleton& Get()
|
|
{
|
|
static FLongPackagePathsSingleton Singleton;
|
|
return Singleton;
|
|
}
|
|
|
|
void GetValidLongPackageRoots(TArray<FString>& OutRoots, bool bIncludeReadOnlyRoots) const
|
|
{
|
|
OutRoots.Add(EngineRootPath);
|
|
OutRoots.Add(GameRootPath);
|
|
|
|
{
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
OutRoots += MountPointRootPaths;
|
|
}
|
|
|
|
if (bIncludeReadOnlyRoots)
|
|
{
|
|
OutRoots.Add(ConfigRootPath);
|
|
OutRoots.Add(ScriptRootPath);
|
|
OutRoots.Add(ExtraRootPath);
|
|
OutRoots.Add(MemoryRootPath);
|
|
OutRoots.Add(TempRootPath);
|
|
}
|
|
}
|
|
|
|
// Given a content path ensure it is consistent, specifically with FileManager relative paths
|
|
static FString ProcessContentMountPoint(const FString& ContentPath)
|
|
{
|
|
FString MountPath = ContentPath;
|
|
|
|
// If a relative path is passed, convert to an absolute path
|
|
if (FPaths::IsRelative(MountPath))
|
|
{
|
|
MountPath = FPaths::ConvertRelativePathToFull(ContentPath);
|
|
|
|
// Revert to original path if unable to convert to full path
|
|
if (MountPath.Len() <= 1)
|
|
{
|
|
MountPath = ContentPath;
|
|
UE_LOG(LogPackageName, Warning, TEXT("Unable to convert mount point relative path: %s"), *ContentPath);
|
|
}
|
|
}
|
|
|
|
// Convert to a relative path using the FileManager
|
|
return IFileManager::Get().ConvertToRelativePath(*MountPath);
|
|
}
|
|
|
|
// This will insert a mount point at the head of the search chain (so it can overlap an existing mount point and win)
|
|
void InsertMountPoint(const FString& RootPath, const FString& ContentPath)
|
|
{
|
|
// Make sure the content path is stored as a relative path, consistent with the other paths we have
|
|
FString RelativeContentPath = ProcessContentMountPoint(ContentPath);
|
|
|
|
// Make sure the path ends in a trailing path separator. We are expecting that in the InternalFilenameToLongPackageName code.
|
|
if (!RelativeContentPath.EndsWith(TEXT("/"), ESearchCase::CaseSensitive))
|
|
{
|
|
RelativeContentPath += TEXT("/");
|
|
}
|
|
|
|
FPathPair Pair(RootPath, RelativeContentPath);
|
|
{
|
|
FWriteScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
ContentRootToPath.Insert(Pair, 0);
|
|
ContentPathToRoot.Insert(Pair, 0);
|
|
MountPointRootPaths.Add(RootPath);
|
|
}
|
|
|
|
// Let subscribers know that a new content path was mounted
|
|
FPackageName::OnContentPathMounted().Broadcast(RootPath, RelativeContentPath);
|
|
}
|
|
|
|
// This will remove a previously inserted mount point
|
|
void RemoveMountPoint(const FString& RootPath, const FString& ContentPath)
|
|
{
|
|
// Make sure the content path is stored as a relative path, consistent with the other paths we have
|
|
FString RelativeContentPath = ProcessContentMountPoint(ContentPath);
|
|
|
|
// Make sure the path ends in a trailing path separator. We are expecting that in the InternalFilenameToLongPackageName code.
|
|
if (!RelativeContentPath.EndsWith(TEXT("/"), ESearchCase::CaseSensitive))
|
|
{
|
|
RelativeContentPath += TEXT("/");
|
|
}
|
|
|
|
bool bFirePathDismountedDelegate = false;
|
|
{
|
|
FWriteScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
if (MountPointRootPaths.Remove(RootPath) > 0)
|
|
{
|
|
FPathPair Pair(RootPath, RelativeContentPath);
|
|
ContentRootToPath.Remove(Pair);
|
|
ContentPathToRoot.Remove(Pair);
|
|
MountPointRootPaths.Remove(RootPath);
|
|
|
|
// Let subscribers know that a new content path was unmounted
|
|
bFirePathDismountedDelegate = true;
|
|
}
|
|
}
|
|
|
|
if (bFirePathDismountedDelegate)
|
|
{
|
|
FPackageName::OnContentPathDismounted().Broadcast(RootPath, RelativeContentPath);
|
|
}
|
|
}
|
|
|
|
// Checks whether the specific root path is a valid mount point.
|
|
bool MountPointExists(const FString& RootPath)
|
|
{
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
return MountPointRootPaths.Contains(RootPath);
|
|
}
|
|
|
|
private:
|
|
FLongPackagePathsSingleton()
|
|
{
|
|
ConfigRootPath = TEXT("/Config/");
|
|
EngineRootPath = TEXT("/Engine/");
|
|
GameRootPath = TEXT("/Game/");
|
|
ScriptRootPath = TEXT("/Script/");
|
|
ExtraRootPath = TEXT("/Extra/");
|
|
MemoryRootPath = TEXT("/Memory/");
|
|
TempRootPath = TEXT("/Temp/");
|
|
|
|
EngineContentPath = FPaths::EngineContentDir();
|
|
ContentPathShort = TEXT("../../Content/");
|
|
EngineShadersPath = FPaths::EngineDir() / TEXT("Shaders/");
|
|
EngineShadersPathShort = TEXT("../../Shaders/");
|
|
GameContentPath = FPaths::ProjectContentDir();
|
|
GameConfigPath = FPaths::ProjectConfigDir();
|
|
GameScriptPath = FPaths::ProjectDir() / TEXT("Script/");
|
|
GameExtraPath = FPaths::ProjectDir() / TEXT("Extra/");
|
|
GameSavedPath = FPaths::ProjectSavedDir();
|
|
|
|
FString RebasedGameDir = FString::Printf(TEXT("../../../%s/"), FApp::GetProjectName());
|
|
|
|
GameContentPathRebased = RebasedGameDir / TEXT("Content/");
|
|
GameConfigPathRebased = RebasedGameDir / TEXT("Config/");
|
|
GameScriptPathRebased = RebasedGameDir / TEXT("Script/");
|
|
GameExtraPathRebased = RebasedGameDir / TEXT("Extra/");
|
|
GameSavedPathRebased = RebasedGameDir / TEXT("Saved/");
|
|
|
|
FWriteScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
|
|
ContentPathToRoot.Empty(13);
|
|
ContentPathToRoot.Emplace(EngineRootPath, EngineContentPath);
|
|
if (FPaths::IsSamePath(GameContentPath, ContentPathShort))
|
|
{
|
|
ContentPathToRoot.Emplace(GameRootPath, ContentPathShort);
|
|
}
|
|
else
|
|
{
|
|
ContentPathToRoot.Emplace(EngineRootPath, ContentPathShort);
|
|
}
|
|
ContentPathToRoot.Emplace(EngineRootPath, EngineShadersPath);
|
|
ContentPathToRoot.Emplace(EngineRootPath, EngineShadersPathShort);
|
|
ContentPathToRoot.Emplace(GameRootPath, GameContentPath);
|
|
ContentPathToRoot.Emplace(ScriptRootPath, GameScriptPath);
|
|
ContentPathToRoot.Emplace(TempRootPath, GameSavedPath);
|
|
ContentPathToRoot.Emplace(GameRootPath, GameContentPathRebased);
|
|
ContentPathToRoot.Emplace(ScriptRootPath, GameScriptPathRebased);
|
|
ContentPathToRoot.Emplace(TempRootPath, GameSavedPathRebased);
|
|
ContentPathToRoot.Emplace(ConfigRootPath, GameConfigPath);
|
|
ContentPathToRoot.Emplace(ExtraRootPath, GameExtraPath);
|
|
ContentPathToRoot.Emplace(ExtraRootPath, GameExtraPathRebased);
|
|
|
|
ContentRootToPath.Empty(11);
|
|
ContentRootToPath.Emplace(EngineRootPath, EngineContentPath);
|
|
ContentRootToPath.Emplace(EngineRootPath, EngineShadersPath);
|
|
ContentRootToPath.Emplace(GameRootPath, GameContentPath);
|
|
ContentRootToPath.Emplace(ScriptRootPath, GameScriptPath);
|
|
ContentRootToPath.Emplace(TempRootPath, GameSavedPath);
|
|
ContentRootToPath.Emplace(GameRootPath, GameContentPathRebased);
|
|
ContentRootToPath.Emplace(ScriptRootPath, GameScriptPathRebased);
|
|
ContentRootToPath.Emplace(ExtraRootPath, GameExtraPath);
|
|
ContentRootToPath.Emplace(ExtraRootPath, GameExtraPathRebased);
|
|
ContentRootToPath.Emplace(TempRootPath, GameSavedPathRebased);
|
|
ContentRootToPath.Emplace(ConfigRootPath, GameConfigPathRebased);
|
|
|
|
// Allow the plugin manager to mount new content paths by exposing access through a delegate. PluginManager is
|
|
// a Core class, but content path functionality is added at the CoreUObject level.
|
|
IPluginManager::Get().SetRegisterMountPointDelegate(IPluginManager::FRegisterMountPointDelegate::CreateStatic(&FPackageName::RegisterMountPoint));
|
|
IPluginManager::Get().SetUnRegisterMountPointDelegate(IPluginManager::FRegisterMountPointDelegate::CreateStatic(&FPackageName::UnRegisterMountPoint));
|
|
}
|
|
};
|
|
|
|
void FPackageName::InternalFilenameToLongPackageName(FStringView InFilename, FStringBuilderBase& OutPackageName)
|
|
{
|
|
const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get();
|
|
FString Filename(InFilename);
|
|
FPaths::NormalizeFilename(Filename);
|
|
|
|
// Convert to relative path if it's not already a long package name
|
|
bool bIsValidLongPackageName = false;
|
|
{
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
for (const auto& Pair: Paths.ContentRootToPath)
|
|
{
|
|
if (Filename.StartsWith(Pair.RootPath))
|
|
{
|
|
bIsValidLongPackageName = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bIsValidLongPackageName)
|
|
{
|
|
Filename = IFileManager::Get().ConvertToRelativePath(*Filename);
|
|
if (InFilename.Len() > 0 && InFilename[InFilename.Len() - 1] == '/')
|
|
{
|
|
// If InFilename ends in / but converted doesn't, add the / back
|
|
bool bEndsInSlash = Filename.Len() > 0 && Filename[Filename.Len() - 1] == '/';
|
|
|
|
if (!bEndsInSlash)
|
|
{
|
|
Filename += TEXT("/");
|
|
}
|
|
}
|
|
}
|
|
|
|
FStringView Result = FPathViews::GetBaseFilenameWithPath(Filename);
|
|
|
|
{
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
for (const auto& Pair: Paths.ContentPathToRoot)
|
|
{
|
|
if (Result.StartsWith(Pair.ContentPath))
|
|
{
|
|
OutPackageName << Pair.RootPath << Result.RightChop(Pair.ContentPath.Len());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
OutPackageName << Result;
|
|
}
|
|
|
|
bool FPackageName::TryConvertFilenameToLongPackageName(const FString& InFilename, FString& OutPackageName, FString* OutFailureReason)
|
|
{
|
|
TStringBuilder<256> LongPackageNameBuilder;
|
|
InternalFilenameToLongPackageName(InFilename, LongPackageNameBuilder);
|
|
FStringView LongPackageName = LongPackageNameBuilder.ToString();
|
|
|
|
// we don't support loading packages from outside of well defined places
|
|
int32 CharacterIndex;
|
|
const bool bContainsDot = LongPackageName.FindChar(TEXT('.'), CharacterIndex);
|
|
const bool bContainsBackslash = LongPackageName.FindChar(TEXT('\\'), CharacterIndex);
|
|
const bool bContainsColon = LongPackageName.FindChar(TEXT(':'), CharacterIndex);
|
|
|
|
if (!(bContainsDot || bContainsBackslash || bContainsColon))
|
|
{
|
|
OutPackageName = LongPackageName;
|
|
return true;
|
|
}
|
|
|
|
// if the package name resolution failed and a relative path was provided, convert to an absolute path
|
|
// as content may be mounted in a different relative path to the one given
|
|
if (FPaths::IsRelative(InFilename))
|
|
{
|
|
FString AbsPath = FPaths::ConvertRelativePathToFull(InFilename);
|
|
if (!FPaths::IsRelative(AbsPath) && AbsPath.Len() > 1)
|
|
{
|
|
if (TryConvertFilenameToLongPackageName(AbsPath, OutPackageName, nullptr))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (OutFailureReason != nullptr)
|
|
{
|
|
FString InvalidChars;
|
|
if (bContainsDot)
|
|
{
|
|
InvalidChars += TEXT(".");
|
|
}
|
|
if (bContainsBackslash)
|
|
{
|
|
InvalidChars += TEXT("\\");
|
|
}
|
|
if (bContainsColon)
|
|
{
|
|
InvalidChars += TEXT(":");
|
|
}
|
|
*OutFailureReason = FString::Printf(TEXT("FilenameToLongPackageName failed to convert '%s'. Attempt result was '%.*s', but the path contains illegal characters '%s'"), *InFilename, LongPackageName.Len(), LongPackageName.GetData(), *InvalidChars);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
FString FPackageName::FilenameToLongPackageName(const FString& InFilename)
|
|
{
|
|
FString FailureReason;
|
|
FString Result;
|
|
if (!TryConvertFilenameToLongPackageName(InFilename, Result, &FailureReason))
|
|
{
|
|
UE_LOG(LogPackageName, Fatal, TEXT("%s"), *FailureReason);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
bool FPackageName::TryConvertLongPackageNameToFilename(const FString& InLongPackageName, FString& OutFilename, const FString& InExtension)
|
|
{
|
|
const auto& Paths = FLongPackagePathsSingleton::Get();
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
for (const auto& Pair: Paths.ContentRootToPath)
|
|
{
|
|
if (InLongPackageName.StartsWith(Pair.RootPath))
|
|
{
|
|
OutFilename = Pair.ContentPath + InLongPackageName.Mid(Pair.RootPath.Len()) + InExtension;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// This is not a long package name or the root folder is not handled in the above cases
|
|
return false;
|
|
}
|
|
|
|
bool FPackageName::ConvertRootPathToContentPath(const FString& RootPath, FString& OutContentPath)
|
|
{
|
|
const auto& Paths = FLongPackagePathsSingleton::Get();
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
for (const auto& Pair: Paths.ContentRootToPath)
|
|
{
|
|
if (RootPath.StartsWith(Pair.RootPath))
|
|
{
|
|
OutContentPath = Pair.ContentPath;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// This is not a long package name or the root folder is not handled in the above cases
|
|
return false;
|
|
}
|
|
|
|
FString FPackageName::LongPackageNameToFilename(const FString& InLongPackageName, const FString& InExtension)
|
|
{
|
|
FString Result;
|
|
if (!TryConvertLongPackageNameToFilename(InLongPackageName, Result, InExtension))
|
|
{
|
|
UE_LOG(LogPackageName, Fatal, TEXT("LongPackageNameToFilename failed to convert '%s'. Path does not map to any roots."), *InLongPackageName);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
FString FPackageName::GetLongPackagePath(const FString& InLongPackageName)
|
|
{
|
|
int32 IndexOfLastSlash = INDEX_NONE;
|
|
if (InLongPackageName.FindLastChar('/', IndexOfLastSlash))
|
|
{
|
|
return InLongPackageName.Left(IndexOfLastSlash);
|
|
}
|
|
else
|
|
{
|
|
return InLongPackageName;
|
|
}
|
|
}
|
|
|
|
bool FPackageName::SplitLongPackageName(const FString& InLongPackageName, FString& OutPackageRoot, FString& OutPackagePath, FString& OutPackageName, const bool bStripRootLeadingSlash)
|
|
{
|
|
const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get();
|
|
|
|
const bool bIncludeReadOnlyRoots = true;
|
|
TArray<FString> ValidRoots;
|
|
Paths.GetValidLongPackageRoots(ValidRoots, bIncludeReadOnlyRoots);
|
|
|
|
// Check to see whether our package came from a valid root
|
|
OutPackageRoot.Empty();
|
|
for (auto RootIt = ValidRoots.CreateConstIterator(); RootIt; ++RootIt)
|
|
{
|
|
const FString& PackageRoot = *RootIt;
|
|
if (InLongPackageName.StartsWith(PackageRoot))
|
|
{
|
|
OutPackageRoot = PackageRoot / "";
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (OutPackageRoot.IsEmpty() || InLongPackageName.Len() <= OutPackageRoot.Len())
|
|
{
|
|
// Path is not part of a valid root, or the path given is too short to continue; splitting failed
|
|
return false;
|
|
}
|
|
|
|
// Use the standard path functions to get the rest
|
|
const FString RemainingPackageName = InLongPackageName.Mid(OutPackageRoot.Len());
|
|
OutPackagePath = FPaths::GetPath(RemainingPackageName) / "";
|
|
OutPackageName = FPaths::GetCleanFilename(RemainingPackageName);
|
|
|
|
if (bStripRootLeadingSlash && OutPackageRoot.StartsWith(TEXT("/"), ESearchCase::CaseSensitive))
|
|
{
|
|
OutPackageRoot.RemoveAt(0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FPackageName::SplitFullObjectPath(const FString& InFullObjectPath, FString& OutClassName, FString& OutPackageName, FString& OutObjectName, FString& OutSubObjectName)
|
|
{
|
|
FString Sanitized = InFullObjectPath.TrimStartAndEnd();
|
|
const TCHAR* Cur = *Sanitized;
|
|
|
|
auto ExtractBeforeDelim = [&Cur](TCHAR Delim, FString& OutString)
|
|
{
|
|
const TCHAR* Start = Cur;
|
|
while (*Cur != '\0' && *Cur != Delim)
|
|
{
|
|
++Cur;
|
|
}
|
|
|
|
OutString = FString(Cur - Start, Start);
|
|
|
|
if (*Cur == Delim)
|
|
{
|
|
++Cur;
|
|
}
|
|
};
|
|
|
|
ExtractBeforeDelim(' ', OutClassName);
|
|
ExtractBeforeDelim('.', OutPackageName);
|
|
ExtractBeforeDelim(':', OutObjectName);
|
|
ExtractBeforeDelim('\0', OutSubObjectName);
|
|
}
|
|
|
|
FString FPackageName::GetLongPackageAssetName(const FString& InLongPackageName)
|
|
{
|
|
return GetShortName(InLongPackageName);
|
|
}
|
|
|
|
bool FPackageName::DoesPackageNameContainInvalidCharacters(FStringView InLongPackageName, FText* OutReason /*= NULL*/)
|
|
{
|
|
// See if the name contains invalid characters.
|
|
TStringBuilder<32> MatchedInvalidChars;
|
|
for (const TCHAR* InvalidCharacters = INVALID_LONGPACKAGE_CHARACTERS; *InvalidCharacters; ++InvalidCharacters)
|
|
{
|
|
FStringView::SizeType OutIndex;
|
|
if (InLongPackageName.FindChar(*InvalidCharacters, OutIndex))
|
|
{
|
|
MatchedInvalidChars += *InvalidCharacters;
|
|
}
|
|
}
|
|
if (MatchedInvalidChars.Len())
|
|
{
|
|
if (OutReason)
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("IllegalNameCharacters"), FText::FromString(FString(MatchedInvalidChars)));
|
|
*OutReason = FText::Format(NSLOCTEXT("Core", "PackageNameContainsInvalidCharacters", "Name may not contain the following characters: '{IllegalNameCharacters}'"), Args);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FPackageName::IsValidLongPackageName(const FString& InLongPackageName, bool bIncludeReadOnlyRoots /*= false*/, FText* OutReason /*= NULL*/)
|
|
{
|
|
// All package names must contain a leading slash, root, slash and name, at minimum theoretical length ("/A/B") is 4
|
|
if (InLongPackageName.Len() < PackageNameConstants::MinPackageNameLength)
|
|
{
|
|
if (OutReason)
|
|
{
|
|
*OutReason = FText::Format(NSLOCTEXT("Core", "LongPackageNames_PathTooShort", "Path should be no less than {0} characters long."), FText::AsNumber(PackageNameConstants::MinPackageNameLength));
|
|
}
|
|
return false;
|
|
}
|
|
// Package names start with a leading slash.
|
|
if (InLongPackageName[0] != '/')
|
|
{
|
|
if (OutReason)
|
|
{
|
|
*OutReason = NSLOCTEXT("Core", "LongPackageNames_PathWithNoStartingSlash", "Path should start with a '/'");
|
|
}
|
|
return false;
|
|
}
|
|
// Package names do not end with a trailing slash.
|
|
if (InLongPackageName[InLongPackageName.Len() - 1] == '/')
|
|
{
|
|
if (OutReason)
|
|
{
|
|
*OutReason = NSLOCTEXT("Core", "LongPackageNames_PathWithTrailingSlash", "Path may not end with a '/'");
|
|
}
|
|
return false;
|
|
}
|
|
// Check for invalid characters
|
|
if (DoesPackageNameContainInvalidCharacters(InLongPackageName, OutReason))
|
|
{
|
|
return false;
|
|
}
|
|
// Check valid roots
|
|
const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get();
|
|
TArray<FString> ValidRoots;
|
|
bool bValidRoot = false;
|
|
Paths.GetValidLongPackageRoots(ValidRoots, bIncludeReadOnlyRoots);
|
|
for (int32 RootIdx = 0; RootIdx < ValidRoots.Num(); ++RootIdx)
|
|
{
|
|
const FString& Root = ValidRoots[RootIdx];
|
|
if (InLongPackageName.StartsWith(Root))
|
|
{
|
|
bValidRoot = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bValidRoot && OutReason)
|
|
{
|
|
if (ValidRoots.Num() == 0)
|
|
{
|
|
*OutReason = NSLOCTEXT("Core", "LongPackageNames_NoValidRoots", "No valid roots exist!");
|
|
}
|
|
else
|
|
{
|
|
FString ValidRootsString = TEXT("");
|
|
if (ValidRoots.Num() == 1)
|
|
{
|
|
ValidRootsString = FString::Printf(TEXT("'%s'"), *ValidRoots[0]);
|
|
}
|
|
else
|
|
{
|
|
for (int32 RootIdx = 0; RootIdx < ValidRoots.Num(); ++RootIdx)
|
|
{
|
|
if (RootIdx < ValidRoots.Num() - 1)
|
|
{
|
|
ValidRootsString += FString::Printf(TEXT("'%s', "), *ValidRoots[RootIdx]);
|
|
}
|
|
else
|
|
{
|
|
ValidRootsString += FString::Printf(TEXT("or '%s'"), *ValidRoots[RootIdx]);
|
|
}
|
|
}
|
|
}
|
|
*OutReason = FText::Format(NSLOCTEXT("Core", "LongPackageNames_InvalidRoot", "Path does not start with a valid root. Path must begin with: {0}"), FText::FromString(ValidRootsString));
|
|
}
|
|
}
|
|
return bValidRoot;
|
|
}
|
|
|
|
bool FPackageName::IsValidObjectPath(const FString& InObjectPath, FText* OutReason)
|
|
{
|
|
FString PackageName;
|
|
FString RemainingObjectPath;
|
|
|
|
// Check for package delimiter
|
|
int32 ObjectDelimiterIdx;
|
|
if (InObjectPath.FindChar('.', ObjectDelimiterIdx))
|
|
{
|
|
if (ObjectDelimiterIdx == InObjectPath.Len() - 1)
|
|
{
|
|
if (OutReason)
|
|
{
|
|
*OutReason = NSLOCTEXT("Core", "ObjectPath_EndWithPeriod", "Object Path may not end with .");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
PackageName = InObjectPath.Mid(0, ObjectDelimiterIdx);
|
|
RemainingObjectPath = InObjectPath.Mid(ObjectDelimiterIdx + 1);
|
|
}
|
|
else
|
|
{
|
|
PackageName = InObjectPath;
|
|
}
|
|
|
|
if (!IsValidLongPackageName(PackageName, true, OutReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (RemainingObjectPath.Len() > 0)
|
|
{
|
|
FText PathContext = NSLOCTEXT("Core", "ObjectPathContext", "Object Path");
|
|
if (!FName::IsValidXName(RemainingObjectPath, INVALID_OBJECTPATH_CHARACTERS, OutReason, &PathContext))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
TCHAR LastChar = RemainingObjectPath[RemainingObjectPath.Len() - 1];
|
|
if (LastChar == '.' || LastChar == ':')
|
|
{
|
|
if (OutReason)
|
|
{
|
|
*OutReason = NSLOCTEXT("Core", "ObjectPath_PathWithTrailingSeperator", "Object Path may not end with : or .");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int32 SlashIndex;
|
|
if (RemainingObjectPath.FindChar('/', SlashIndex))
|
|
{
|
|
if (OutReason)
|
|
{
|
|
*OutReason = NSLOCTEXT("Core", "ObjectPath_SlashAfterPeriod", "Object Path may not have / after first .");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FPackageName::IsValidPath(const FString& InPath)
|
|
{
|
|
const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get();
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
for (const FPathPair& Pair: Paths.ContentRootToPath)
|
|
{
|
|
if (InPath.StartsWith(Pair.RootPath))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// The root folder is not handled in the above cases
|
|
return false;
|
|
}
|
|
|
|
void FPackageName::RegisterMountPoint(const FString& RootPath, const FString& ContentPath)
|
|
{
|
|
FLongPackagePathsSingleton::Get().InsertMountPoint(RootPath, ContentPath);
|
|
}
|
|
|
|
void FPackageName::UnRegisterMountPoint(const FString& RootPath, const FString& ContentPath)
|
|
{
|
|
FLongPackagePathsSingleton::Get().RemoveMountPoint(RootPath, ContentPath);
|
|
}
|
|
|
|
bool FPackageName::MountPointExists(const FString& RootPath)
|
|
{
|
|
return FLongPackagePathsSingleton::Get().MountPointExists(RootPath);
|
|
}
|
|
|
|
FName FPackageName::GetPackageMountPoint(const FString& InPackagePath, bool InWithoutSlashes)
|
|
{
|
|
FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get();
|
|
|
|
TArray<FString> MountPoints;
|
|
Paths.GetValidLongPackageRoots(MountPoints, true);
|
|
|
|
int32 WithoutSlashes = InWithoutSlashes ? 1 : 0;
|
|
for (auto RootIt = MountPoints.CreateConstIterator(); RootIt; ++RootIt)
|
|
{
|
|
if (InPackagePath.StartsWith(*RootIt))
|
|
{
|
|
return FName(*RootIt->Mid(WithoutSlashes, RootIt->Len() - (2 * WithoutSlashes)));
|
|
}
|
|
}
|
|
|
|
return FName();
|
|
}
|
|
|
|
FString FPackageName::ConvertToLongScriptPackageName(const TCHAR* InShortName)
|
|
{
|
|
if (IsShortPackageName(FString(InShortName)))
|
|
{
|
|
return FString::Printf(TEXT("/Script/%s"), InShortName);
|
|
}
|
|
else
|
|
{
|
|
return InShortName;
|
|
}
|
|
}
|
|
|
|
// Short to long script package name map.
|
|
static TMap<FName, FName> ScriptPackageNames;
|
|
|
|
// @todo: This stuff needs to be eliminated as soon as we can make sure that no legacy short package names
|
|
// are in use when referencing class names in UObject module "class packages"
|
|
void FPackageName::RegisterShortPackageNamesForUObjectModules()
|
|
{
|
|
// @todo: Ideally we'd only be processing UObject modules, not every module, but we have
|
|
// no way of knowing which modules may contain UObjects (without say, having UBT save a manifest.)
|
|
// @todo: This stuff is a bomb waiting to explode. Because short package names can
|
|
// take precedent over other object names, modules can reserve names for other types!
|
|
TArray<FName> AllModuleNames;
|
|
FModuleManager::Get().FindModules(TEXT("*"), AllModuleNames);
|
|
for (TArray<FName>::TConstIterator ModuleNameIt(AllModuleNames); ModuleNameIt; ++ModuleNameIt)
|
|
{
|
|
ScriptPackageNames.Add(*ModuleNameIt, *ConvertToLongScriptPackageName(*ModuleNameIt->ToString()));
|
|
}
|
|
}
|
|
|
|
FName* FPackageName::FindScriptPackageName(FName InShortName)
|
|
{
|
|
return ScriptPackageNames.Find(InShortName);
|
|
}
|
|
|
|
bool FPackageName::FindPackageFileWithoutExtension(const FString& InPackageFilename, FString& OutFilename, bool InAllowTextFormats)
|
|
{
|
|
auto& FileManager = IFileManager::Get();
|
|
|
|
{
|
|
static const FString* PackageExtensions[] =
|
|
{
|
|
&AssetPackageExtension,
|
|
&MapPackageExtension};
|
|
|
|
// Loop through all known extensions and check if the file exists
|
|
|
|
for (int32 ExtensionIndex = 0; ExtensionIndex < UE_ARRAY_COUNT(PackageExtensions); ++ExtensionIndex)
|
|
{
|
|
FString PackageFilename = InPackageFilename + *PackageExtensions[ExtensionIndex];
|
|
if (FileManager.FileExists(*PackageFilename))
|
|
{
|
|
// The package exists so exit. From now on InPackageFilename can be equal to OutFilename so
|
|
// don't attempt to use it anymore (case where &InPackageFilename == &OutFilename).
|
|
OutFilename = MoveTemp(PackageFilename);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if WITH_TEXT_ARCHIVE_SUPPORT
|
|
if (InAllowTextFormats)
|
|
{
|
|
static const FString* TextPackageExtensions[] =
|
|
{
|
|
&TextAssetPackageExtension,
|
|
&TextMapPackageExtension};
|
|
|
|
for (int32 ExtensionIndex = 0; ExtensionIndex < UE_ARRAY_COUNT(TextPackageExtensions); ++ExtensionIndex)
|
|
{
|
|
FString PackageFilename = InPackageFilename + *TextPackageExtensions[ExtensionIndex];
|
|
if (FileManager.FileExists(*PackageFilename))
|
|
{
|
|
// The package exists so exit. From now on InPackageFilename can be equal to OutFilename so
|
|
// don't attempt to use it anymore (case where &InPackageFilename == &OutFilename).
|
|
OutFilename = MoveTemp(PackageFilename);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FPackageName::FixPackageNameCase(FString& LongPackageName, FStringView Extension)
|
|
{
|
|
// Find the matching long package root
|
|
const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get();
|
|
FReadScopeLock ScopeLock(ContentMountPointCriticalSection);
|
|
for (const FPathPair& Pair: Paths.ContentRootToPath)
|
|
{
|
|
if (LongPackageName.StartsWith(Pair.RootPath))
|
|
{
|
|
FString RelativePackageName = LongPackageName.Mid(Pair.RootPath.Len());
|
|
FString FileName = Pair.ContentPath / RelativePackageName;
|
|
|
|
int ExtensionLen = Extension.Len();
|
|
if (Extension.Len() > 0 && Extension[0] != '.')
|
|
{
|
|
FileName.AppendChar('.');
|
|
ExtensionLen++;
|
|
}
|
|
|
|
FileName += Extension;
|
|
|
|
FString CorrectFileName = IFileManager::Get().GetFilenameOnDisk(*FileName);
|
|
if (CorrectFileName.Len() >= RelativePackageName.Len() + ExtensionLen)
|
|
{
|
|
FString NewRelativePackageName = CorrectFileName.Mid(CorrectFileName.Len() - RelativePackageName.Len() - ExtensionLen, RelativePackageName.Len());
|
|
if (NewRelativePackageName == RelativePackageName)
|
|
{
|
|
LongPackageName.RemoveAt(Pair.RootPath.Len(), LongPackageName.Len() - Pair.RootPath.Len());
|
|
LongPackageName.Append(*NewRelativePackageName);
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FPackageName::DoesPackageExist(const FString& LongPackageName, const FGuid* Guid, FString* OutFilename, bool InAllowTextFormats)
|
|
{
|
|
SCOPED_LOADTIMER(FPackageName_DoesPackageExist);
|
|
|
|
bool bFoundFile = false;
|
|
|
|
// Make sure passing filename as LongPackageName is supported.
|
|
FString PackageName;
|
|
FText Reason;
|
|
|
|
if (!FPackageName::TryConvertFilenameToLongPackageName(LongPackageName, PackageName))
|
|
{
|
|
verify(!FPackageName::IsValidLongPackageName(LongPackageName, true, &Reason));
|
|
UE_LOG(LogPackageName, Error, TEXT("Illegal call to DoesPackageExist: '%s' is not a standard unreal filename or a long path name. Reason: %s"), *LongPackageName, *Reason.ToString());
|
|
ensureMsgf(false, TEXT("Illegal call to DoesPackageExist: '%s' is not a standard unreal filename or a long path name. Reason: %s"), *LongPackageName, *Reason.ToString());
|
|
return false;
|
|
}
|
|
// Once we have the real Package Name, we can exit early if it's a script package - they exist only in memory.
|
|
if (IsScriptPackage(PackageName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (IsMemoryPackage(PackageName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!FPackageName::IsValidLongPackageName(PackageName, true, &Reason))
|
|
{
|
|
UE_LOG(LogPackageName, Error, TEXT("DoesPackageExist: DoesPackageExist FAILED: '%s' is not a standard unreal filename or a long path name. Reason: %s"), *LongPackageName, *Reason.ToString());
|
|
return false;
|
|
}
|
|
|
|
// Used when I/O dispatcher is enabled
|
|
if (DoesPackageExistOverrideDelegate.IsBound())
|
|
{
|
|
if (DoesPackageExistOverrideDelegate.Execute(FName(*PackageName)))
|
|
{
|
|
if (OutFilename)
|
|
{
|
|
*OutFilename = LongPackageNameToFilename(PackageName, TEXT(""));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Try to find uncooked packages on disk when I/O store is enabled in editor builds
|
|
#if !WITH_IOSTORE_IN_EDITOR
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
// Convert to filename (no extension yet).
|
|
FString Filename = LongPackageNameToFilename(PackageName, TEXT(""));
|
|
|
|
// Find the filename (with extension).
|
|
bFoundFile = FindPackageFileWithoutExtension(Filename, Filename, InAllowTextFormats);
|
|
|
|
// On consoles, we don't support package downloading, so no need to waste any extra cycles/disk io dealing with it
|
|
if (!FPlatformProperties::RequiresCookedData() && bFoundFile && Guid != NULL)
|
|
{
|
|
// @todo: If we could get to list of linkers here, it would be faster to check
|
|
// then to open the file and read it
|
|
FArchive* PackageReader = IFileManager::Get().CreateFileReader(*Filename);
|
|
// This had better open
|
|
check(PackageReader != NULL);
|
|
|
|
// Read in the package summary
|
|
FPackageFileSummary Summary;
|
|
*PackageReader << Summary;
|
|
|
|
// Compare Guids
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
if (Summary.Guid != *Guid)
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
{
|
|
bFoundFile = false;
|
|
}
|
|
|
|
// Close package file
|
|
delete PackageReader;
|
|
}
|
|
|
|
if (OutFilename && bFoundFile)
|
|
{
|
|
*OutFilename = Filename;
|
|
}
|
|
return bFoundFile;
|
|
}
|
|
|
|
bool FPackageName::SearchForPackageOnDisk(const FString& PackageName, FString* OutLongPackageName, FString* OutFilename)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FPackageName::SearchForPackageOnDisk);
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("FPackageName::SearchForPackageOnDisk"), STAT_PackageName_SearchForPackageOnDisk, STATGROUP_LoadTime);
|
|
|
|
// This function may take a long time to complete, so suspend heartbeat measure while we're here
|
|
FSlowHeartBeatScope SlowHeartBeatScope;
|
|
|
|
bool bResult = false;
|
|
double StartTime = FPlatformTime::Seconds();
|
|
if (FPackageName::IsShortPackageName(PackageName) == false)
|
|
{
|
|
// If this is long package name, revert to using DoesPackageExist because it's a lot faster.
|
|
FString Filename;
|
|
if (DoesPackageExist(PackageName, NULL, &Filename))
|
|
{
|
|
if (OutLongPackageName)
|
|
{
|
|
*OutLongPackageName = PackageName;
|
|
}
|
|
if (OutFilename)
|
|
{
|
|
*OutFilename = Filename;
|
|
}
|
|
bResult = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Attempt to find package by its short name by searching in the known content paths.
|
|
TArray<FString> Paths;
|
|
{
|
|
TArray<FString> RootContentPaths;
|
|
FPackageName::QueryRootContentPaths(RootContentPaths);
|
|
for (TArray<FString>::TConstIterator RootPathIt(RootContentPaths); RootPathIt; ++RootPathIt)
|
|
{
|
|
const FString& RootPath = *RootPathIt;
|
|
const FString& ContentFolder = FPackageName::LongPackageNameToFilename(RootPath, TEXT(""));
|
|
Paths.Add(ContentFolder);
|
|
}
|
|
}
|
|
|
|
const FString PackageWildcard = (PackageName.Find(TEXT("."), ESearchCase::CaseSensitive) != INDEX_NONE ? PackageName : PackageName + TEXT(".*"));
|
|
TArray<FString> Results;
|
|
|
|
for (int32 PathIndex = 0; PathIndex < Paths.Num() && !bResult; ++PathIndex)
|
|
{
|
|
// Search directly on disk. Very slow!
|
|
IFileManager::Get().FindFilesRecursive(Results, *Paths[PathIndex], *PackageWildcard, true, false);
|
|
|
|
for (int32 FileIndex = 0; FileIndex < Results.Num(); ++FileIndex)
|
|
{
|
|
FString Filename(Results[FileIndex]);
|
|
if (IsPackageFilename(Results[FileIndex]))
|
|
{
|
|
// Convert to long package name.
|
|
FString LongPackageName;
|
|
if (TryConvertFilenameToLongPackageName(Filename, LongPackageName))
|
|
{
|
|
if (OutLongPackageName)
|
|
{
|
|
if (bResult)
|
|
{
|
|
UE_LOG(LogPackageName, Warning, TEXT("Found ambiguous long package name for '%s'. Returning '%s', but could also be '%s'."), *PackageName, **OutLongPackageName, *LongPackageName);
|
|
}
|
|
else
|
|
{
|
|
*OutLongPackageName = LongPackageName;
|
|
}
|
|
}
|
|
if (OutFilename)
|
|
{
|
|
FPaths::MakeStandardFilename(Filename);
|
|
if (bResult)
|
|
{
|
|
UE_LOG(LogPackageName, Warning, TEXT("Found ambiguous file name for '%s'. Returning '%s', but could also be '%s'."), *PackageName, **OutFilename, *Filename);
|
|
}
|
|
else
|
|
{
|
|
*OutFilename = Filename;
|
|
}
|
|
}
|
|
bResult = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
float ThisTime = FPlatformTime::Seconds() - StartTime;
|
|
|
|
if (bResult)
|
|
{
|
|
UE_LOG(LogPackageName, Log, TEXT("SearchForPackageOnDisk took %7.3fs to resolve %s."), ThisTime, *PackageName);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageName, Log, TEXT("SearchForPackageOnDisk took %7.3fs, but failed to resolve %s."), ThisTime, *PackageName);
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
bool FPackageName::TryConvertShortPackagePathToLongInObjectPath(const FString& ObjectPath, FString& ConvertedObjectPath)
|
|
{
|
|
FString PackagePath;
|
|
FString ObjectName;
|
|
|
|
int32 DotPosition = ObjectPath.Find(TEXT("."), ESearchCase::CaseSensitive);
|
|
if (DotPosition != INDEX_NONE)
|
|
{
|
|
PackagePath = ObjectPath.Mid(0, DotPosition);
|
|
ObjectName = ObjectPath.Mid(DotPosition + 1);
|
|
}
|
|
else
|
|
{
|
|
PackagePath = ObjectPath;
|
|
}
|
|
|
|
FString LongPackagePath;
|
|
if (!SearchForPackageOnDisk(PackagePath, &LongPackagePath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ConvertedObjectPath = FString::Printf(TEXT("%s.%s"), *LongPackagePath, *ObjectName);
|
|
return true;
|
|
}
|
|
|
|
FString FPackageName::GetNormalizedObjectPath(const FString& ObjectPath)
|
|
{
|
|
if (!ObjectPath.IsEmpty() && FPackageName::IsShortPackageName(ObjectPath))
|
|
{
|
|
FString LongPath;
|
|
|
|
UE_LOG(LogPackageName, Warning, TEXT("Asset path \"%s\" is in short form, which is unsupported and -- even if valid -- resolving it will be really slow."), *ObjectPath);
|
|
UE_LOG(LogPackageName, Warning, TEXT("Please consider resaving package in order to speed-up loading."));
|
|
|
|
if (!FPackageName::TryConvertShortPackagePathToLongInObjectPath(ObjectPath, LongPath))
|
|
{
|
|
UE_LOG(LogPackageName, Warning, TEXT("Asset path \"%s\" could not be resolved."), *ObjectPath);
|
|
}
|
|
|
|
return LongPath;
|
|
}
|
|
else
|
|
{
|
|
return ObjectPath;
|
|
}
|
|
}
|
|
|
|
FString FPackageName::GetDelegateResolvedPackagePath(const FString& InSourcePackagePath)
|
|
{
|
|
if (FCoreDelegates::PackageNameResolvers.Num() > 0)
|
|
{
|
|
bool WasResolved = false;
|
|
|
|
// If the path is /Game/Path/Foo.Foo only worry about resolving the /Game/Path/Foo
|
|
FString PathName = InSourcePackagePath;
|
|
FString ObjectName;
|
|
int32 DotIndex = INDEX_NONE;
|
|
|
|
if (PathName.FindChar('.', DotIndex))
|
|
{
|
|
ObjectName = PathName.Mid(DotIndex + 1);
|
|
PathName.LeftInline(DotIndex, false);
|
|
}
|
|
|
|
for (auto Delegate: FCoreDelegates::PackageNameResolvers)
|
|
{
|
|
FString ResolvedPath;
|
|
if (Delegate.Execute(PathName, ResolvedPath))
|
|
{
|
|
UE_LOG(LogPackageName, Display, TEXT("Package '%s' was resolved to '%s'"), *PathName, *ResolvedPath);
|
|
PathName = ResolvedPath;
|
|
WasResolved = true;
|
|
}
|
|
}
|
|
|
|
if (WasResolved)
|
|
{
|
|
// If package was passed in with an object, add that back on by deriving it from the package name
|
|
if (ObjectName.Len())
|
|
{
|
|
int32 LastSlashIndex = INDEX_NONE;
|
|
if (PathName.FindLastChar('/', LastSlashIndex))
|
|
{
|
|
ObjectName = PathName.Mid(LastSlashIndex + 1);
|
|
}
|
|
|
|
PathName += TEXT(".");
|
|
PathName += ObjectName;
|
|
}
|
|
|
|
return PathName;
|
|
}
|
|
}
|
|
|
|
return InSourcePackagePath;
|
|
}
|
|
|
|
FString FPackageName::GetSourcePackagePath(const FString& InLocalizedPackagePath)
|
|
{
|
|
// This function finds the start and end point of the "/L10N/<culture>" part of the path so that it can be removed
|
|
auto GetL10NTrimRange = [](const FString& InPath, int32& OutL10NStart, int32& OutL10NLength)
|
|
{
|
|
const TCHAR* CurChar = *InPath;
|
|
|
|
// Must start with a slash
|
|
if (*CurChar++ != TEXT('/'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Find the end of the first part of the path, eg /Game/
|
|
while (*CurChar && *CurChar++ != TEXT('/'))
|
|
{
|
|
}
|
|
if (!*CurChar)
|
|
{
|
|
// Found end-of-string
|
|
return false;
|
|
}
|
|
|
|
if (FCString::Strnicmp(CurChar, TEXT("L10N/"), 5) == 0) // StartsWith "L10N/"
|
|
{
|
|
CurChar -= 1; // -1 because we need to eat the slash before L10N
|
|
OutL10NStart = (CurChar - *InPath);
|
|
OutL10NLength = 6; // "/L10N/"
|
|
|
|
// Walk to the next slash as that will be the end of the culture code
|
|
CurChar += OutL10NLength;
|
|
while (*CurChar && *CurChar++ != TEXT('/'))
|
|
{
|
|
++OutL10NLength;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else if (FCString::Stricmp(CurChar, TEXT("L10N")) == 0) // Is "L10N"
|
|
{
|
|
CurChar -= 1; // -1 because we need to eat the slash before L10N
|
|
OutL10NStart = (CurChar - *InPath);
|
|
OutL10NLength = 5; // "/L10N"
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
FString SourcePackagePath = InLocalizedPackagePath;
|
|
|
|
int32 L10NStart = INDEX_NONE;
|
|
int32 L10NLength = 0;
|
|
if (GetL10NTrimRange(SourcePackagePath, L10NStart, L10NLength))
|
|
{
|
|
SourcePackagePath.RemoveAt(L10NStart, L10NLength);
|
|
}
|
|
|
|
return SourcePackagePath;
|
|
}
|
|
|
|
FString FPackageName::GetLocalizedPackagePath(const FString& InSourcePackagePath)
|
|
{
|
|
const FName LocalizedPackageName = FPackageLocalizationManager::Get().FindLocalizedPackageName(*InSourcePackagePath);
|
|
return (LocalizedPackageName.IsNone()) ? InSourcePackagePath : LocalizedPackageName.ToString();
|
|
}
|
|
|
|
FString FPackageName::GetLocalizedPackagePath(const FString& InSourcePackagePath, const FString& InCultureName)
|
|
{
|
|
const FName LocalizedPackageName = FPackageLocalizationManager::Get().FindLocalizedPackageNameForCulture(*InSourcePackagePath, InCultureName);
|
|
return (LocalizedPackageName.IsNone()) ? InSourcePackagePath : LocalizedPackageName.ToString();
|
|
}
|
|
|
|
FString FPackageName::PackageFromPath(const TCHAR* InPathName)
|
|
{
|
|
FString PackageName;
|
|
if (FPackageName::TryConvertFilenameToLongPackageName(InPathName, PackageName))
|
|
{
|
|
return PackageName;
|
|
}
|
|
else
|
|
{
|
|
// Not a valid package filename
|
|
return InPathName;
|
|
}
|
|
}
|
|
|
|
bool FPackageName::IsTextPackageExtension(const TCHAR* Ext)
|
|
{
|
|
return IsTextAssetPackageExtension(Ext) || IsTextMapPackageExtension(Ext);
|
|
}
|
|
|
|
bool FPackageName::IsTextAssetPackageExtension(const TCHAR* Ext)
|
|
{
|
|
if (*Ext != TEXT('.'))
|
|
{
|
|
return (TextAssetPackageExtension.EndsWith(Ext));
|
|
}
|
|
else
|
|
{
|
|
return (TextAssetPackageExtension == Ext);
|
|
}
|
|
}
|
|
|
|
bool FPackageName::IsTextMapPackageExtension(const TCHAR* Ext)
|
|
{
|
|
if (*Ext != TEXT('.'))
|
|
{
|
|
return (TextMapPackageExtension.EndsWith(Ext));
|
|
}
|
|
else
|
|
{
|
|
return (TextMapPackageExtension == Ext);
|
|
}
|
|
}
|
|
|
|
bool FPackageName::IsPackageExtension(const TCHAR* Ext)
|
|
{
|
|
return IsAssetPackageExtension(Ext) || IsMapPackageExtension(Ext);
|
|
}
|
|
|
|
bool FPackageName::IsAssetPackageExtension(const TCHAR* Ext)
|
|
{
|
|
if (*Ext != TEXT('.'))
|
|
{
|
|
return (AssetPackageExtension.EndsWith(Ext));
|
|
}
|
|
else
|
|
{
|
|
return (AssetPackageExtension == Ext);
|
|
}
|
|
}
|
|
|
|
bool FPackageName::IsMapPackageExtension(const TCHAR* Ext)
|
|
{
|
|
if (*Ext != TEXT('.'))
|
|
{
|
|
return (MapPackageExtension.EndsWith(Ext));
|
|
}
|
|
else
|
|
{
|
|
return (MapPackageExtension == Ext);
|
|
}
|
|
}
|
|
|
|
bool FPackageName::FindPackagesInDirectory(TArray<FString>& OutPackages, const FString& RootDir)
|
|
{
|
|
UE_CLOG(FIoDispatcher::IsInitialized(), LogPackageName, Error, TEXT("Can't search for packages using the filesystem when I/O dispatcher is enabled"));
|
|
|
|
FString LocalPathToRootDir;
|
|
if (!FPackageName::TryConvertLongPackageNameToFilename(RootDir / TEXT(""), LocalPathToRootDir))
|
|
{
|
|
LocalPathToRootDir = RootDir;
|
|
}
|
|
LocalPathToRootDir = FPaths::ConvertRelativePathToFull(MoveTemp(LocalPathToRootDir));
|
|
|
|
// Find all files in RootDir, then filter by extension (we have two package extensions so they can't
|
|
// be included in the search wildcard.
|
|
TArray<FString> AllFiles;
|
|
IFileManager::Get().FindFilesRecursive(AllFiles, *LocalPathToRootDir, TEXT("*.*"), true, false);
|
|
// Keep track if any package has been found. Can't rely only on OutPackages.Num() > 0 as it may not be empty.
|
|
const int32 PreviousPackagesCount = OutPackages.Num();
|
|
for (int32 FileIndex = 0; FileIndex < AllFiles.Num(); FileIndex++)
|
|
{
|
|
const FString& Filename = AllFiles[FileIndex];
|
|
if (IsPackageFilename(Filename))
|
|
{
|
|
OutPackages.Add(Filename);
|
|
}
|
|
}
|
|
return OutPackages.Num() > PreviousPackagesCount;
|
|
}
|
|
|
|
bool FPackageName::FindPackagesInDirectories(TArray<FString>& OutPackages, const TArrayView<const FString>& RootDirs)
|
|
{
|
|
TSet<FString> Packages;
|
|
TArray<FString> DirPackages;
|
|
for (const FString& RootDir: RootDirs)
|
|
{
|
|
DirPackages.Reset();
|
|
FindPackagesInDirectory(DirPackages, RootDir);
|
|
for (FString& DirPackage: DirPackages)
|
|
{
|
|
Packages.Add(MoveTemp(DirPackage));
|
|
}
|
|
}
|
|
OutPackages.Reserve(Packages.Num() + OutPackages.Num());
|
|
for (FString& Package: Packages)
|
|
{
|
|
OutPackages.Add(MoveTemp(Package));
|
|
}
|
|
return Packages.Num() > 0;
|
|
}
|
|
|
|
void FPackageName::IteratePackagesInDirectory(const FString& RootDir, const FPackageNameVisitor& Callback)
|
|
{
|
|
class FPackageVisitor: public IPlatformFile::FDirectoryVisitor
|
|
{
|
|
public:
|
|
const FPackageNameVisitor& Callback;
|
|
explicit FPackageVisitor(const FPackageNameVisitor& InCallback)
|
|
: Callback(InCallback)
|
|
{
|
|
}
|
|
virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override
|
|
{
|
|
bool Result = true;
|
|
if (!bIsDirectory && IsPackageFilename(FilenameOrDirectory))
|
|
{
|
|
Result = Callback(FilenameOrDirectory);
|
|
}
|
|
return Result;
|
|
}
|
|
};
|
|
|
|
FPackageVisitor PackageVisitor(Callback);
|
|
IFileManager::Get().IterateDirectoryRecursively(*RootDir, PackageVisitor);
|
|
}
|
|
|
|
void FPackageName::IteratePackagesInDirectory(const FString& RootDir, const FPackageNameStatVisitor& Callback)
|
|
{
|
|
class FPackageStatVisitor: public IPlatformFile::FDirectoryStatVisitor
|
|
{
|
|
public:
|
|
const FPackageNameStatVisitor& Callback;
|
|
explicit FPackageStatVisitor(const FPackageNameStatVisitor& InCallback)
|
|
: Callback(InCallback)
|
|
{
|
|
}
|
|
virtual bool Visit(const TCHAR* FilenameOrDirectory, const FFileStatData& StatData) override
|
|
{
|
|
bool Result = true;
|
|
if (!StatData.bIsDirectory && IsPackageFilename(FilenameOrDirectory))
|
|
{
|
|
Result = Callback(FilenameOrDirectory, StatData);
|
|
}
|
|
return Result;
|
|
}
|
|
};
|
|
|
|
FPackageStatVisitor PackageVisitor(Callback);
|
|
IFileManager::Get().IterateDirectoryStatRecursively(*RootDir, PackageVisitor);
|
|
}
|
|
|
|
void FPackageName::QueryRootContentPaths(TArray<FString>& OutRootContentPaths, bool bIncludeReadOnlyRoots, bool bWithoutLeadingSlashes, bool bWithoutTrailingSlashes)
|
|
{
|
|
const FLongPackagePathsSingleton& Paths = FLongPackagePathsSingleton::Get();
|
|
Paths.GetValidLongPackageRoots(OutRootContentPaths, bIncludeReadOnlyRoots);
|
|
|
|
if (bWithoutTrailingSlashes || bWithoutLeadingSlashes)
|
|
{
|
|
for (FString& It: OutRootContentPaths)
|
|
{
|
|
if (bWithoutTrailingSlashes && It.Len() > 1 && It[It.Len() - 1] == TEXT('/'))
|
|
{
|
|
It.RemoveAt(It.Len() - 1, /*Count*/ 1, /*bAllowShrinking*/ false);
|
|
}
|
|
|
|
if (bWithoutLeadingSlashes && It.Len() > 1 && It[0] == TEXT('/'))
|
|
{
|
|
It.RemoveAt(0, /*Count*/ 1, /*bAllowShrinking*/ false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FPackageName::EnsureContentPathsAreRegistered()
|
|
{
|
|
SCOPED_BOOT_TIMING("FPackageName::EnsureContentPathsAreRegistered");
|
|
FLongPackagePathsSingleton::Get();
|
|
}
|
|
|
|
bool FPackageName::ParseExportTextPath(const FString& InExportTextPath, FString* OutClassName, FString* OutObjectPath)
|
|
{
|
|
if (InExportTextPath.Split(TEXT("'"), OutClassName, OutObjectPath, ESearchCase::CaseSensitive))
|
|
{
|
|
if (OutObjectPath)
|
|
{
|
|
FString& OutObjectPathRef = *OutObjectPath;
|
|
if (OutObjectPathRef.EndsWith(TEXT("'"), ESearchCase::CaseSensitive))
|
|
{
|
|
OutObjectPathRef.LeftChopInline(1, false);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
template <class T>
|
|
bool ParseExportTextPathImpl(const T& InExportTextPath, T* OutClassName, T* OutObjectPath)
|
|
{
|
|
int32 Index;
|
|
if (InExportTextPath.FindChar('\'', Index))
|
|
{
|
|
if (OutClassName)
|
|
{
|
|
*OutClassName = InExportTextPath.Left(Index);
|
|
}
|
|
|
|
if (OutObjectPath)
|
|
{
|
|
*OutObjectPath = InExportTextPath.Mid(Index + 1);
|
|
OutObjectPath->RemoveSuffix(InExportTextPath.EndsWith('\''));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FPackageName::ParseExportTextPath(FWideStringView InExportTextPath, FWideStringView* OutClassName, FWideStringView* OutObjectPath)
|
|
{
|
|
return ParseExportTextPathImpl(InExportTextPath, OutClassName, OutObjectPath);
|
|
}
|
|
|
|
bool FPackageName::ParseExportTextPath(FAnsiStringView InExportTextPath, FAnsiStringView* OutClassName, FAnsiStringView* OutObjectPath)
|
|
{
|
|
return ParseExportTextPathImpl(InExportTextPath, OutClassName, OutObjectPath);
|
|
}
|
|
|
|
bool FPackageName::ParseExportTextPath(const TCHAR* InExportTextPath, FStringView* OutClassName, FStringView* OutObjectPath)
|
|
{
|
|
return ParseExportTextPath(FStringView(InExportTextPath), OutClassName, OutObjectPath);
|
|
}
|
|
|
|
template <class T>
|
|
T ExportTextPathToObjectPathImpl(const T& InExportTextPath)
|
|
{
|
|
T ObjectPath;
|
|
if (FPackageName::ParseExportTextPath(InExportTextPath, nullptr, &ObjectPath))
|
|
{
|
|
return ObjectPath;
|
|
}
|
|
|
|
// Could not parse the export text path. Could already be an object path, just return it back.
|
|
return InExportTextPath;
|
|
}
|
|
|
|
FWideStringView FPackageName::ExportTextPathToObjectPath(FWideStringView InExportTextPath)
|
|
{
|
|
return ExportTextPathToObjectPathImpl(InExportTextPath);
|
|
}
|
|
|
|
FAnsiStringView FPackageName::ExportTextPathToObjectPath(FAnsiStringView InExportTextPath)
|
|
{
|
|
return ExportTextPathToObjectPathImpl(InExportTextPath);
|
|
}
|
|
|
|
FString FPackageName::ExportTextPathToObjectPath(const FString& InExportTextPath)
|
|
{
|
|
return ExportTextPathToObjectPathImpl(InExportTextPath);
|
|
}
|
|
|
|
FString FPackageName::ExportTextPathToObjectPath(const TCHAR* InExportTextPath)
|
|
{
|
|
return ExportTextPathToObjectPath(FString(InExportTextPath));
|
|
}
|
|
|
|
template <class T>
|
|
T ObjectPathToPackageNameImpl(const T& InObjectPath)
|
|
{
|
|
// Check for package delimiter
|
|
int32 ObjectDelimiterIdx;
|
|
if (InObjectPath.FindChar('.', ObjectDelimiterIdx))
|
|
{
|
|
return InObjectPath.Mid(0, ObjectDelimiterIdx);
|
|
}
|
|
|
|
// No object delimiter. The path must refer to the package name directly.
|
|
return InObjectPath;
|
|
}
|
|
|
|
FWideStringView FPackageName::ObjectPathToPackageName(FWideStringView InObjectPath)
|
|
{
|
|
return ObjectPathToPackageNameImpl(InObjectPath);
|
|
}
|
|
|
|
FAnsiStringView FPackageName::ObjectPathToPackageName(FAnsiStringView InObjectPath)
|
|
{
|
|
return ObjectPathToPackageNameImpl(InObjectPath);
|
|
}
|
|
|
|
FString FPackageName::ObjectPathToPackageName(const FString& InObjectPath)
|
|
{
|
|
return ObjectPathToPackageNameImpl(InObjectPath);
|
|
}
|
|
|
|
template <class T>
|
|
T ObjectPathToObjectNameImpl(const T& InObjectPath)
|
|
{
|
|
// Check for a subobject
|
|
int32 SubObjectDelimiterIdx;
|
|
if (InObjectPath.FindChar(':', SubObjectDelimiterIdx))
|
|
{
|
|
return InObjectPath.Mid(SubObjectDelimiterIdx + 1);
|
|
}
|
|
|
|
// Check for a top level object
|
|
int32 ObjectDelimiterIdx;
|
|
if (InObjectPath.FindChar('.', ObjectDelimiterIdx))
|
|
{
|
|
return InObjectPath.Mid(ObjectDelimiterIdx + 1);
|
|
}
|
|
|
|
// No object or subobject delimiters. The path must refer to the object name directly (i.e. a package).
|
|
return InObjectPath;
|
|
}
|
|
|
|
FString FPackageName::ObjectPathToObjectName(const FString& InObjectPath)
|
|
{
|
|
return ObjectPathToObjectNameImpl(InObjectPath);
|
|
}
|
|
|
|
FWideStringView FPackageName::ObjectPathToObjectName(FWideStringView InObjectPath)
|
|
{
|
|
return ObjectPathToObjectNameImpl(InObjectPath);
|
|
}
|
|
|
|
bool FPackageName::IsExtraPackage(FStringView InPackageName)
|
|
{
|
|
return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().ExtraRootPath);
|
|
}
|
|
|
|
bool FPackageName::IsScriptPackage(FStringView InPackageName)
|
|
{
|
|
return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().ScriptRootPath);
|
|
}
|
|
|
|
bool FPackageName::IsMemoryPackage(FStringView InPackageName)
|
|
{
|
|
return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().MemoryRootPath);
|
|
}
|
|
|
|
bool FPackageName::IsTempPackage(FStringView InPackageName)
|
|
{
|
|
return InPackageName.StartsWith(FLongPackagePathsSingleton::Get().TempRootPath);
|
|
}
|
|
|
|
bool FPackageName::IsLocalizedPackage(FStringView InPackageName)
|
|
{
|
|
// Minimum valid package name length is "/A/L10N"
|
|
if (InPackageName.Len() < 7)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const TCHAR* CurChar = InPackageName.GetData();
|
|
const TCHAR* EndChar = InPackageName.GetData() + InPackageName.Len();
|
|
|
|
// Must start with a slash
|
|
if (CurChar == EndChar || *CurChar++ != TEXT('/'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Find the end of the first part of the path, eg /Game/
|
|
while (CurChar != EndChar && *CurChar++ != TEXT('/'))
|
|
{
|
|
}
|
|
if (CurChar == EndChar)
|
|
{
|
|
// Found end-of-string
|
|
return false;
|
|
}
|
|
|
|
// Are we part of the L10N folder?
|
|
FStringView Remaining(CurChar, EndChar - CurChar);
|
|
// Is "L10N" or StartsWith "L10N/"
|
|
return Remaining.StartsWith(TEXT("L10N"_SV), ESearchCase::IgnoreCase) && (Remaining.Len() == 4 || Remaining[4] == '/');
|
|
}
|
|
|
|
#if WITH_DEV_AUTOMATION_TESTS
|
|
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FPackageNameTests, "System.Core.Misc.PackageNames", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::SmokeFilter)
|
|
|
|
bool FPackageNameTests::RunTest(const FString& Parameters)
|
|
{
|
|
// Localized paths tests
|
|
{
|
|
auto TestIsLocalizedPackage = [&](const FString& InPath, const bool InExpected)
|
|
{
|
|
const bool bResult = FPackageName::IsLocalizedPackage(InPath);
|
|
if (bResult != InExpected)
|
|
{
|
|
AddError(FString::Printf(TEXT("Path '%s' failed FPackageName::IsLocalizedPackage (got '%d', expected '%d')."), *InPath, bResult, InExpected));
|
|
}
|
|
};
|
|
|
|
TestIsLocalizedPackage(TEXT("/Game"), false);
|
|
TestIsLocalizedPackage(TEXT("/Game/MyAsset"), false);
|
|
TestIsLocalizedPackage(TEXT("/Game/L10N"), true);
|
|
TestIsLocalizedPackage(TEXT("/Game/L10N/en"), true);
|
|
TestIsLocalizedPackage(TEXT("/Game/L10N/en/MyAsset"), true);
|
|
}
|
|
|
|
// Source path tests
|
|
{
|
|
auto TestGetSourcePackagePath = [this](const FString& InPath, const FString& InExpected)
|
|
{
|
|
const FString Result = FPackageName::GetSourcePackagePath(InPath);
|
|
if (Result != InExpected)
|
|
{
|
|
AddError(FString::Printf(TEXT("Path '%s' failed FPackageName::GetSourcePackagePath (got '%s', expected '%s')."), *InPath, *Result, *InExpected));
|
|
}
|
|
};
|
|
|
|
TestGetSourcePackagePath(TEXT("/Game"), TEXT("/Game"));
|
|
TestGetSourcePackagePath(TEXT("/Game/MyAsset"), TEXT("/Game/MyAsset"));
|
|
TestGetSourcePackagePath(TEXT("/Game/L10N"), TEXT("/Game"));
|
|
TestGetSourcePackagePath(TEXT("/Game/L10N/en"), TEXT("/Game"));
|
|
TestGetSourcePackagePath(TEXT("/Game/L10N/en/MyAsset"), TEXT("/Game/MyAsset"));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endif // WITH_DEV_AUTOMATION_TESTS
|