// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "Misc/ConfigCacheIni.h" #include "UObject/ErrorException.h" #include "UObject/ObjectMacros.h" #include "UObject/Class.h" #include "UObject/Package.h" #include "UObject/MetaData.h" #include "UObject/PropertyPortFlags.h" #include "UObject/UObjectThreadContext.h" #include "UObject/DevObjectVersion.h" #include "UObject/CoreObjectVersion.h" #include "UObject/CoreRedirects.h" #include "UObject/LinkerLoad.h" DEFINE_LOG_CATEGORY_STATIC(LogEnum, Log, All); /*----------------------------------------------------------------------------- UEnum implementation. -----------------------------------------------------------------------------*/ TMap UEnum::AllEnumNames; UEnum::UEnum(const FObjectInitializer& ObjectInitializer) : UField(ObjectInitializer), CppType(), CppForm(ECppForm::Regular), EnumDisplayNameFn(nullptr) { } void UEnum::Serialize(FArchive& Ar) { Ar.UsingCustomVersion(FCoreObjectVersion::GUID); Super::Serialize(Ar); if (Ar.IsLoading()) { if (Ar.UE4Ver() < VER_UE4_TIGHTLY_PACKED_ENUMS) { TArray TempNames; Ar << TempNames; int64 Value = 0; for (const FName& TempName: TempNames) { Names.Emplace(TempName, Value++); } } else if (Ar.CustomVer(FCoreObjectVersion::GUID) < FCoreObjectVersion::EnumProperties) { TArray> OldNames; Ar << OldNames; Names.Reset(OldNames.Num()); for (const TPair& OldName: OldNames) { Names.Emplace(OldName.Key, OldName.Value); } } else { Ar << Names; } } else { Ar << Names; } if (Ar.UE4Ver() < VER_UE4_ENUM_CLASS_SUPPORT) { bool bIsNamespace; Ar << bIsNamespace; CppForm = bIsNamespace ? ECppForm::Namespaced : ECppForm::Regular; } else { uint8 EnumTypeByte = (uint8)CppForm; Ar << EnumTypeByte; CppForm = (ECppForm)EnumTypeByte; } if (Ar.IsLoading() || Ar.IsSaving()) { // We're duplicating this enum. if ((Ar.GetPortFlags() & PPF_Duplicate) // and we're loading it from already serialized base. && Ar.IsLoading()) { // Rename enum names to reflect new class. RenameNamesAfterDuplication(); } AddNamesToMasterList(); } } void UEnum::BeginDestroy() { RemoveNamesFromMasterList(); Super::BeginDestroy(); } FString UEnum::GetBaseEnumNameOnDuplication() const { // Last name is always fully qualified, in form EnumName::Prefix_MAX. FString BaseEnumName = GetNameByIndex(Names.Num() - 1).ToString(); // Double check we have a fully qualified name. const int32 DoubleColonPos = BaseEnumName.Find(TEXT("::"), ESearchCase::CaseSensitive); check(DoubleColonPos != INDEX_NONE); // Get actual base name. BaseEnumName.LeftChopInline(BaseEnumName.Len() - DoubleColonPos, false); return BaseEnumName; } void UEnum::RenameNamesAfterDuplication() { if (Names.Num() != 0) { // Get name of base enum, from which we're duplicating. FString BaseEnumName = GetBaseEnumNameOnDuplication(); // Get name of duplicated enum. FString ThisName = GetName(); // Replace all usages of base class name to the duplicated one. for (TPair& Kvp: Names) { FString NameString = Kvp.Key.ToString(); NameString.ReplaceInline(*BaseEnumName, *ThisName); Kvp.Key = FName(*NameString); } } } int64 UEnum::ResolveEnumerator(FArchive& Ar, int64 EnumeratorIndex) const { return EnumeratorIndex; } FString UEnum::GenerateFullEnumName(const TCHAR* InEnumName) const { if (GetCppForm() == ECppForm::Regular || IsFullEnumName(InEnumName)) { return InEnumName; } return FString::Printf(TEXT("%s::%s"), *GetName(), InEnumName); } FName UEnum::GetNameByIndex(int32 Index) const { if (Names.IsValidIndex(Index)) { return Names[Index].Key; } return NAME_None; } FName UEnum::GetNameByValue(int64 InValue) const { for (const TPair& Kvp: Names) { if (Kvp.Value == InValue) { return Kvp.Key; } } return NAME_None; } int32 UEnum::GetIndexByName(const FName InName, EGetByNameFlags Flags) const { ENameCase ComparisonMethod = !!(Flags & EGetByNameFlags::CaseSensitive) ? ENameCase::CaseSensitive : ENameCase::IgnoreCase; // First try the fast path const int32 Count = Names.Num(); for (int32 Counter = 0; Counter < Count; ++Counter) { if (Names[Counter].Key.IsEqual(InName, ComparisonMethod)) { return Counter; } } // Otherwise see if it is in the redirect table int32 Result = GetIndexByNameString(InName.ToString(), Flags); return Result; } int64 UEnum::GetValueByName(FName InName, EGetByNameFlags Flags) const { // This handles redirects const int32 EnumIndex = GetIndexByName(InName, Flags); if (EnumIndex != INDEX_NONE) { return GetValueByIndex(EnumIndex); } return INDEX_NONE; } int64 UEnum::GetMaxEnumValue() const { int32 NamesNum = Names.Num(); if (NamesNum == 0) { return 0; } int64 MaxValue = Names[0].Value; for (int32 i = 0; i < NamesNum; ++i) { int64 CurrentValue = Names[i].Value; if (CurrentValue > MaxValue) { MaxValue = CurrentValue; } } return MaxValue; } bool UEnum::IsValidEnumValue(int64 InValue) const { int32 NamesNum = Names.Num(); for (int32 i = 0; i < NamesNum; ++i) { int64 CurrentValue = Names[i].Value; if (CurrentValue == InValue) { return true; } } return false; } bool UEnum::IsValidEnumName(FName InName) const { int32 NamesNum = Names.Num(); for (int32 i = 0; i < NamesNum; ++i) { FName CurrentName = Names[i].Key; if (CurrentName == InName) { return true; } } return false; } void UEnum::AddNamesToMasterList() { for (TPair Kvp: Names) { UEnum* Enum = AllEnumNames.FindRef(Kvp.Key); if (Enum == nullptr || Enum->HasAnyFlags(RF_NewerVersionExists)) { AllEnumNames.Add(Kvp.Key, this); } else if (Enum != this && Enum->GetOutermost() != GetTransientPackage()) { UE_LOG(LogEnum, Warning, TEXT("Enum name collision: '%s' is in both '%s' and '%s'"), *Kvp.Key.ToString(), *GetPathName(), *Enum->GetPathName()); } } } void UEnum::RemoveNamesFromMasterList() { for (TPair Kvp: Names) { UEnum* Enum = AllEnumNames.FindRef(Kvp.Key); if (Enum == this) { AllEnumNames.Remove(Kvp.Key); } } } FString UEnum::GenerateEnumPrefix() const { FString Prefix; if (Names.Num() > 0) { Names[0].Key.ToString(Prefix); // For each item in the enumeration, trim the prefix as much as necessary to keep it a prefix. // This ensures that once all items have been processed, a common prefix will have been constructed. // This will be the longest common prefix since as little as possible is trimmed at each step. for (int32 NameIdx = 1; NameIdx < Names.Num(); NameIdx++) { FString EnumItemName = Names[NameIdx].Key.ToString(); // Find the length of the longest common prefix of Prefix and EnumItemName. int32 PrefixIdx = 0; while (PrefixIdx < Prefix.Len() && PrefixIdx < EnumItemName.Len() && Prefix[PrefixIdx] == EnumItemName[PrefixIdx]) { PrefixIdx++; } // Trim the prefix to the length of the common prefix. Prefix.LeftInline(PrefixIdx, false); } // Find the index of the rightmost underscore in the prefix. int32 UnderscoreIdx = Prefix.Find(TEXT("_"), ESearchCase::CaseSensitive, ESearchDir::FromEnd); // If an underscore was found, trim the prefix so only the part before the rightmost underscore is included. if (UnderscoreIdx > 0) { Prefix.LeftInline(UnderscoreIdx, false); } else { // no underscores in the common prefix - this probably indicates that the names // for this enum are not using Epic's notation, so just empty the prefix so that // the max item will use the full name of the enum Prefix.Empty(); } } // If no common prefix was found, or if the enum does not contain any entries, // use the name of the enumeration instead. if (Prefix.Len() == 0) { Prefix = GetName(); } return Prefix; } FString UEnum::GetNameStringByIndex(int32 InIndex) const { if (Names.IsValidIndex(InIndex)) { FName EnumEntryName = GetNameByIndex(InIndex); if (CppForm == ECppForm::Regular) { return EnumEntryName.ToString(); } // Strip the namespace from the name. FString EnumNameString(EnumEntryName.ToString()); int32 ScopeIndex = EnumNameString.Find(TEXT("::"), ESearchCase::CaseSensitive); if (ScopeIndex != INDEX_NONE) { return EnumNameString.Mid(ScopeIndex + 2); } } return FString(); } FString UEnum::GetNameStringByValue(int64 Value) const { int32 Index = GetIndexByValue(Value); return GetNameStringByIndex(Index); } bool UEnum::FindNameStringByValue(FString& Out, int64 InValue) const { int32 Index = GetIndexByValue(InValue); if (Index == INDEX_NONE) { return false; } Out = GetNameStringByIndex(Index); return true; } FText UEnum::GetDisplayNameTextByIndex(int32 NameIndex) const { FString RawName = GetNameStringByIndex(NameIndex); if (RawName.IsEmpty()) { return FText::GetEmpty(); } #if WITH_EDITOR FText LocalizedDisplayName; // In the editor, use metadata and localization to look up names static const FString Namespace = TEXT("UObjectDisplayNames"); const FString Key = GetFullGroupName(false) + TEXT(".") + RawName; FString NativeDisplayName; if (HasMetaData(TEXT("DisplayName"), NameIndex)) { NativeDisplayName = GetMetaData(TEXT("DisplayName"), NameIndex); } else { NativeDisplayName = FName::NameToDisplayString(RawName, false); } if (!(FText::FindText(Namespace, Key, /*OUT*/ LocalizedDisplayName, &NativeDisplayName))) { LocalizedDisplayName = FText::FromString(NativeDisplayName); } if (!LocalizedDisplayName.IsEmpty()) { return LocalizedDisplayName; } #endif if (EnumDisplayNameFn) { return (*EnumDisplayNameFn)(NameIndex); } return FText::FromString(GetNameStringByIndex(NameIndex)); } FText UEnum::GetDisplayNameTextByValue(int64 Value) const { int32 Index = GetIndexByValue(Value); return GetDisplayNameTextByIndex(Index); } bool UEnum::FindDisplayNameTextByValue(FText& Out, int64 Value) const { int32 Index = GetIndexByValue(Value); if (Index == INDEX_NONE) { return false; } Out = GetDisplayNameTextByIndex(Index); return true; } FString UEnum::GetAuthoredNameStringByIndex(int32 InIndex) const { return GetNameStringByIndex(InIndex); } FString UEnum::GetAuthoredNameStringByValue(int64 Value) const { int32 Index = GetIndexByValue(Value); return GetAuthoredNameStringByIndex(Index); } bool UEnum::FindAuthoredNameStringByValue(FString& Out, int64 Value) const { int32 Index = GetIndexByValue(Value); if (Index == INDEX_NONE) { return false; } Out = GetAuthoredNameStringByIndex(Index); return true; } int32 UEnum::GetIndexByNameString(const FString& InSearchString, EGetByNameFlags Flags) const { ENameCase NameComparisonMethod = !!(Flags & EGetByNameFlags::CaseSensitive) ? ENameCase ::CaseSensitive : ENameCase ::IgnoreCase; ESearchCase::Type StringComparisonMethod = !!(Flags & EGetByNameFlags::CaseSensitive) ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase; FString SearchEnumEntryString = InSearchString; FString ModifiedEnumEntryString; // Strip or add the namespace int32 DoubleColonIndex = SearchEnumEntryString.Find(TEXT("::"), ESearchCase::CaseSensitive); if (DoubleColonIndex == INDEX_NONE) { ModifiedEnumEntryString = GenerateFullEnumName(*SearchEnumEntryString); } else { ModifiedEnumEntryString = SearchEnumEntryString.RightChop(DoubleColonIndex + 2); } const TMap* ValueChanges = FCoreRedirects::GetValueRedirects(ECoreRedirectFlags::Type_Enum, this); if (ValueChanges) { const FString* FoundNewEnumEntry = ValueChanges->Find(SearchEnumEntryString); if (FoundNewEnumEntry == nullptr) { FoundNewEnumEntry = ValueChanges->Find(ModifiedEnumEntryString); } if (FoundNewEnumEntry) { SearchEnumEntryString = **FoundNewEnumEntry; // Recompute modified name int32 NewDoubleColonIndex = SearchEnumEntryString.Find(TEXT("::"), ESearchCase::CaseSensitive); if (NewDoubleColonIndex == INDEX_NONE) { ModifiedEnumEntryString = GenerateFullEnumName(*SearchEnumEntryString); } else { ModifiedEnumEntryString = SearchEnumEntryString.RightChop(NewDoubleColonIndex + 2); } } } else if (DoubleColonIndex != INDEX_NONE) { // If we didn't find a value redirect and our original string was namespaced, we need to fix the namespace now as it may have changed due to enum type redirect SearchEnumEntryString = GenerateFullEnumName(*ModifiedEnumEntryString); } // Search for names both with and without namespace FName SearchName = FName(*SearchEnumEntryString, FNAME_Find); FName ModifiedName = FName(*ModifiedEnumEntryString, FNAME_Find); const int32 Count = Names.Num(); for (int32 Counter = 0; Counter < Count; ++Counter) { if (Names[Counter].Key.IsEqual(SearchName, NameComparisonMethod) || Names[Counter].Key.IsEqual(ModifiedName, NameComparisonMethod)) { return Counter; } } // Check authored name, but only if this is a subclass of Enum that might have implemented it // and we've ascertained that there are no entries that match on the cheaper FName checks const bool bCheckAuthoredName = !!(Flags & EGetByNameFlags::CheckAuthoredName) && (GetClass() != UEnum::StaticClass()); if (bCheckAuthoredName) { for (int32 Counter = 0; Counter < Count; ++Counter) { FString AuthoredName = GetAuthoredNameStringByIndex(Counter); if (AuthoredName.Equals(SearchEnumEntryString, StringComparisonMethod) || AuthoredName.Equals(ModifiedEnumEntryString, StringComparisonMethod)) { return Counter; } } } if (!InSearchString.Equals(SearchEnumEntryString, StringComparisonMethod)) { // There was an actual redirect, and we didn't find it UE_LOG(LogEnum, Warning, TEXT("EnumRedirect for enum %s maps '%s' to invalid value '%s'!"), *GetName(), *InSearchString, *SearchEnumEntryString); } else if (!!(Flags & EGetByNameFlags::ErrorIfNotFound) && !InSearchString.IsEmpty() && !InSearchString.Equals(FName().ToString(), StringComparisonMethod)) { // None is passed in by blueprints at various points, isn't an error. Any other failed resolve should be fixed UObject* SerializedObject = nullptr; if (FLinkerLoad* Linker = GetLinker()) { if (FUObjectSerializeContext* LoadContext = Linker->GetSerializeContext()) { SerializedObject = LoadContext->SerializedObject; } } UE_LOG(LogEnum, Warning, TEXT("In asset '%s', there is an enum property of type '%s' with an invalid value of '%s'"), *GetPathNameSafe(SerializedObject ? SerializedObject : FUObjectThreadContext::Get().ConstructedObject), *GetName(), *InSearchString); } return INDEX_NONE; } int64 UEnum::GetValueByNameString(const FString& SearchString, EGetByNameFlags Flags) const { int32 Index = GetIndexByNameString(SearchString, Flags); if (Index != INDEX_NONE) { return GetValueByIndex(Index); } return INDEX_NONE; } bool UEnum::ContainsExistingMax() const { if (GetIndexByName(*GenerateFullEnumName(TEXT("MAX")), EGetByNameFlags::CaseSensitive) != INDEX_NONE) { return true; } FName MaxEnumItem = *GenerateFullEnumName(*(GenerateEnumPrefix() + TEXT("_MAX"))); if (GetIndexByName(MaxEnumItem, EGetByNameFlags::CaseSensitive) != INDEX_NONE) { return true; } return false; } bool UEnum::SetEnums(TArray>& InNames, UEnum::ECppForm InCppForm, EEnumFlags InFlags, bool bAddMaxKeyIfMissing) { if (Names.Num() > 0) { RemoveNamesFromMasterList(); } Names = InNames; CppForm = InCppForm; EnumFlags = InFlags; if (bAddMaxKeyIfMissing) { if (!ContainsExistingMax()) { FName MaxEnumItem = *GenerateFullEnumName(*(GenerateEnumPrefix() + TEXT("_MAX"))); if (LookupEnumName(MaxEnumItem) != INDEX_NONE) { // the MAX identifier is already being used by another enum return false; } Names.Emplace(MaxEnumItem, GetMaxEnumValue() + 1); } } AddNamesToMasterList(); return true; } #if WITH_EDITOR FText UEnum::GetToolTipTextByIndex(int32 NameIndex) const { FText LocalizedToolTip; FString NativeToolTip = GetMetaData(TEXT("ToolTip"), NameIndex); static const FString Namespace = TEXT("UObjectToolTips"); FString Key = GetFullGroupName(false) + TEXT(".") + GetNameStringByIndex(NameIndex); if (!FText::FindText(Namespace, Key, /*OUT*/ LocalizedToolTip, &NativeToolTip)) { static const FString DoxygenSee(TEXT("@see")); static const FString TooltipSee(TEXT("See:")); if (NativeToolTip.ReplaceInline(*DoxygenSee, *TooltipSee) > 0) { NativeToolTip.TrimEndInline(); } LocalizedToolTip = FText::FromString(NativeToolTip); } return LocalizedToolTip; } #endif #if WITH_EDITORONLY_DATA bool UEnum::HasMetaData(const TCHAR* Key, int32 NameIndex /*=INDEX_NONE*/) const { bool bResult = false; UPackage* Package = GetOutermost(); check(Package); UMetaData* MetaData = Package->GetMetaData(); check(MetaData); FString KeyString; // If an index was specified, search for metadata linked to a specified value if (NameIndex != INDEX_NONE) { KeyString = GetNameStringByIndex(NameIndex); KeyString.AppendChar(TEXT('.')); KeyString.Append(Key); } // If no index was specified, search for metadata for the enum itself else { KeyString = Key; } bResult = MetaData->HasValue(this, *KeyString); return bResult; } FString UEnum::GetMetaData(const TCHAR* Key, int32 NameIndex /*=INDEX_NONE*/, bool bAllowRemap /*=true*/) const { UPackage* Package = GetOutermost(); check(Package); UMetaData* MetaData = Package->GetMetaData(); check(MetaData); FString KeyString; // If an index was specified, search for metadata linked to a specified value if (NameIndex != INDEX_NONE) { check(Names.IsValidIndex(NameIndex)); KeyString = GetNameStringByIndex(NameIndex) + TEXT(".") + Key; } // If no index was specified, search for metadata for the enum itself else { KeyString = Key; } FString ResultString = MetaData->GetValue(this, *KeyString); // look in the engine ini, in a section named after the metadata key we are looking for, and the enum's name (KeyString) if (bAllowRemap && ResultString.StartsWith(TEXT("ini:"))) { if (!GConfig->GetString(TEXT("EnumRemap"), *KeyString, ResultString, GEngineIni)) { // if this fails, then use what's after the ini: ResultString.MidInline(4, MAX_int32, false); } } return ResultString; } void UEnum::SetMetaData(const TCHAR* Key, const TCHAR* InValue, int32 NameIndex) const { UPackage* Package = GetOutermost(); check(Package); UMetaData* MetaData = Package->GetMetaData(); check(MetaData); FString KeyString; // If an index was specified, search for metadata linked to a specified value if (NameIndex != INDEX_NONE) { check(Names.IsValidIndex(NameIndex)); KeyString = GetNameStringByIndex(NameIndex) + TEXT(".") + Key; } // If no index was specified, search for metadata for the enum itself else { KeyString = Key; } MetaData->SetValue(this, *KeyString, InValue); } void UEnum::RemoveMetaData(const TCHAR* Key, int32 NameIndex /*=INDEX_NONE*/) const { UPackage* Package = GetOutermost(); check(Package); UMetaData* MetaData = Package->GetMetaData(); check(MetaData); FString KeyString; // If an index was specified, search for metadata linked to a specified value if (NameIndex != INDEX_NONE) { check(Names.IsValidIndex(NameIndex)); KeyString = GetNameStringByIndex(NameIndex) + TEXT(".") + Key; } // If no index was specified, search for metadata for the enum itself else { KeyString = Key; } MetaData->RemoveValue(this, *KeyString); } #endif int64 UEnum::ParseEnum(const TCHAR*& Str) { FString Token; const TCHAR* ParsedStr = Str; if (FParse::AlnumToken(ParsedStr, Token)) { FName TheName = FName(*Token, FNAME_Find); int64 Result = LookupEnumName(TheName); if (Result != INDEX_NONE) { Str = ParsedStr; } return Result; } else { return 0; } } IMPLEMENT_CORE_INTRINSIC_CLASS(UEnum, UField, {});