Implement proper FName / FNamePool
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
namespace Prospect.Unreal.Core.Names;
|
||||
|
||||
public enum EFindName
|
||||
{
|
||||
/** Find a name; return 0 if it doesn't exist. */
|
||||
FNAME_Find,
|
||||
|
||||
/** Find a name or add it if it doesn't exist. */
|
||||
FNAME_Add,
|
||||
|
||||
/** Finds a name and replaces it. Adds it if missing. This is only used by UHT and is generally not safe for threading.
|
||||
* All this really is used for is correcting the case of names. In MT conditions you might get a half-changed name.
|
||||
*/
|
||||
FNAME_Replace_Not_Safe_For_Threading,
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
namespace Prospect.Unreal.Core.Names
|
||||
{
|
||||
public enum UnrealNameKey
|
||||
public enum EName
|
||||
{
|
||||
None = 0,
|
||||
|
||||
@@ -1,36 +1,150 @@
|
||||
namespace Prospect.Unreal.Core.Names;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
public class FName
|
||||
namespace Prospect.Unreal.Core.Names;
|
||||
|
||||
public readonly struct FName
|
||||
{
|
||||
public FName()
|
||||
public FName() : this(EName.None, FNameHelper.NAME_NO_NUMBER_INTERNAL)
|
||||
{
|
||||
NameIndex = -1; // ?
|
||||
Number = -1; // ?
|
||||
Str = "None";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an FName with a hardcoded string index.
|
||||
/// </summary>
|
||||
/// <param name="hardcodedName">The hardcoded value the string portion of the name will have</param>
|
||||
public FName(EName hardcodedName) : this(hardcodedName, FNameHelper.NAME_NO_NUMBER_INTERNAL)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an FName with a hardcoded string index and (instance).
|
||||
/// </summary>
|
||||
/// <param name="hardcodedName">The hardcoded value the string portion of the name will have</param>
|
||||
/// <param name="number">The hardcoded value for the number portion of the name</param>
|
||||
public FName(EName hardcodedName, int number)
|
||||
{
|
||||
Index = FNamePool.Find(hardcodedName);
|
||||
Number = (uint)number;
|
||||
}
|
||||
|
||||
public FName(string str)
|
||||
/// <summary>
|
||||
/// Create an FName. If FindType is FNAME_Find, and the string part of the name
|
||||
/// doesn't already exist, then the name will be NAME_None.
|
||||
/// </summary>
|
||||
/// <param name="value">Value for the string portion of the name</param>
|
||||
/// <param name="findType">Action to take</param>
|
||||
public FName(string value, EFindName findType = EFindName.FNAME_Add)
|
||||
{
|
||||
NameIndex = -1;
|
||||
Number = -1;
|
||||
Str = str;
|
||||
var name = FNameHelper.MakeDetectNumber(value, findType);
|
||||
|
||||
Index = name.Index;
|
||||
Number = name.Number;
|
||||
}
|
||||
|
||||
public FName(string str, int number)
|
||||
/// <summary>
|
||||
/// Create an FName. If FindType is FNAME_Find, and the string part of the name
|
||||
/// doesn't already exist, then the name will be NAME_None.
|
||||
/// </summary>
|
||||
/// <param name="value">Value for the string portion of the name</param>
|
||||
/// <param name="number">Value for the number portion of the name</param>
|
||||
/// <param name="findType">Action to take</param>
|
||||
public FName(string value, int number, EFindName findType = EFindName.FNAME_Add)
|
||||
{
|
||||
NameIndex = -1;
|
||||
Number = number;
|
||||
Str = str;
|
||||
var name = FNameHelper.MakeWithNumber(value, findType, number);
|
||||
|
||||
Index = name.Index;
|
||||
Number = name.Number;
|
||||
}
|
||||
|
||||
public Int32 NameIndex { get; set; }
|
||||
/// <summary>
|
||||
/// Only use this if you know what you are doing (:
|
||||
/// </summary>
|
||||
public FName(FNameEntryId index, int number)
|
||||
{
|
||||
Index = index;
|
||||
Number = (uint)number;
|
||||
}
|
||||
|
||||
public Int32 Number { get; set; }
|
||||
/// <summary>
|
||||
/// Index of the name
|
||||
/// </summary>
|
||||
public FNameEntryId Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number portion of the string/number pair
|
||||
/// (stored internally as 1 more than actual, so zero'd memory will be the default, no-instance case)
|
||||
/// </summary>
|
||||
public uint Number { get; }
|
||||
|
||||
public static implicit operator FName(EName name)
|
||||
{
|
||||
return new FName(name);
|
||||
}
|
||||
|
||||
public string Str { get; set; }
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetNumber()
|
||||
{
|
||||
return (int)Number;
|
||||
}
|
||||
|
||||
public string GetPlainNameString()
|
||||
{
|
||||
return FNamePool.Resolve(Index);
|
||||
}
|
||||
|
||||
public EName? ToEName()
|
||||
{
|
||||
return FNamePool.FindEName(Index);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Str;
|
||||
var name = GetPlainNameString();
|
||||
|
||||
if (Number == FNameHelper.NAME_NO_NUMBER_INTERNAL)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return $"{name}_{FNameHelper.NAME_INTERNAL_TO_EXTERNAL(GetNumber())}";
|
||||
}
|
||||
|
||||
public static bool operator ==(FName left, EName right)
|
||||
{
|
||||
return (left.Index == right) & (left.GetNumber() == 0);
|
||||
}
|
||||
|
||||
public static bool operator !=(FName left, EName right)
|
||||
{
|
||||
return (left.Index != right) | (left.GetNumber() != 0);
|
||||
}
|
||||
|
||||
public static bool operator ==(FName left, FName right)
|
||||
{
|
||||
return (left.Index == right.Index) & (left.GetNumber() == right.GetNumber());
|
||||
}
|
||||
|
||||
public static bool operator !=(FName left, FName right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public bool Equals(FName other)
|
||||
{
|
||||
return this == other;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is FName other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return (Index.GetHashCode() * 397) ^ (int)Number;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace Prospect.Unreal.Core.Names;
|
||||
|
||||
public readonly struct FNameEntryId
|
||||
{
|
||||
public FNameEntryId()
|
||||
{
|
||||
Value = 0;
|
||||
}
|
||||
|
||||
public FNameEntryId(uint value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public readonly uint Value;
|
||||
|
||||
public static bool operator ==(FNameEntryId left, EName right)
|
||||
{
|
||||
return left == FNamePool.Find(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(FNameEntryId left, EName right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static bool operator ==(FNameEntryId left, FNameEntryId right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(FNameEntryId left, FNameEntryId right)
|
||||
{
|
||||
return left.Value != right.Value;
|
||||
}
|
||||
|
||||
public bool Equals(FNameEntryId other)
|
||||
{
|
||||
return this == other;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is FNameEntryId other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (int)Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Prospect.Unreal.Core.Names;
|
||||
|
||||
internal static class FNameHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Externally, the instance number to represent no instance number is NAME_NO_NUMBER,
|
||||
/// but internally, we add 1 to indices, so we use this #define internally for
|
||||
/// zero'd memory initialization will still make NAME_None as expected
|
||||
/// </summary>
|
||||
public const int NAME_NO_NUMBER_INTERNAL = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Special value for an FName with no number
|
||||
/// </summary>
|
||||
public const int NAME_NO_NUMBER = NAME_NO_NUMBER_INTERNAL - 1;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum size of name.
|
||||
/// </summary>
|
||||
public const int NAME_SIZE = 1024;
|
||||
|
||||
public static FName MakeDetectNumber(string name, EFindName findType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return new FName();
|
||||
}
|
||||
|
||||
var nameLen = name.Length;
|
||||
var internalNumber = ParseNumber(name, ref nameLen);
|
||||
return MakeWithNumber(name.Substring(0, nameLen), findType, (int)internalNumber);
|
||||
}
|
||||
|
||||
private static uint ParseNumber(ReadOnlySpan<char> name, ref int nameLength)
|
||||
{
|
||||
var len = nameLength;
|
||||
var digits = 0;
|
||||
|
||||
for (var i = len - 1; i >= 0 && name[i] >= '0' && name[i] <= '9'; --i)
|
||||
{
|
||||
++digits;
|
||||
}
|
||||
|
||||
var firstDigit = len - digits;
|
||||
const int maxDigitsInt32 = 10;
|
||||
if (digits != 0 && digits < len && name[firstDigit - 1] == '_' && digits <= maxDigitsInt32)
|
||||
{
|
||||
// check for the case where there are multiple digits after the _ and the first one
|
||||
// is a 0 ("Rocket_04"). Can't split this case. (So, we check if the first char
|
||||
// is not 0 or the length of the number is 1 (since ROcket_0 is valid)
|
||||
if (digits == 1 || name[firstDigit] != '0')
|
||||
{
|
||||
var number = long.Parse(name.Slice(len - digits, digits));
|
||||
if (number < int.MaxValue)
|
||||
{
|
||||
nameLength -= 1 + digits;
|
||||
return NAME_EXTERNAL_TO_INTERNAL((uint)number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NAME_NO_NUMBER_INTERNAL;
|
||||
}
|
||||
|
||||
public static FName MakeWithNumber(string name, EFindName findType, int internalNumber)
|
||||
{
|
||||
if (name.Length == 0)
|
||||
{
|
||||
return new FName();
|
||||
}
|
||||
|
||||
return Make(name, findType, internalNumber);
|
||||
}
|
||||
|
||||
private static FName Make(string name, EFindName findType, int internalNumber)
|
||||
{
|
||||
if (name.Length >= NAME_SIZE)
|
||||
{
|
||||
return new FName("ERROR_NAME_SIZE_EXCEEDED");
|
||||
}
|
||||
|
||||
FNameEntryId index;
|
||||
|
||||
if (findType == EFindName.FNAME_Add)
|
||||
{
|
||||
index = FNamePool.Store(name);
|
||||
}
|
||||
else if (findType == EFindName.FNAME_Find)
|
||||
{
|
||||
index = FNamePool.Find(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
return new FName(index, internalNumber);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint NAME_EXTERNAL_TO_INTERNAL(uint number)
|
||||
{
|
||||
return number + 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int NAME_EXTERNAL_TO_INTERNAL(int number)
|
||||
{
|
||||
return number + 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint NAME_INTERNAL_TO_EXTERNAL(uint number)
|
||||
{
|
||||
return number - 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int NAME_INTERNAL_TO_EXTERNAL(int number)
|
||||
{
|
||||
return number - 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
namespace Prospect.Unreal.Core.Names;
|
||||
|
||||
public static class FNamePool
|
||||
{
|
||||
private static readonly object NamesLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Map hardcoded names in <see cref="UnrealNames"/>.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<EName, FNameEntryId> HardcodedNames = new Dictionary<EName, FNameEntryId>();
|
||||
private static readonly Dictionary<FNameEntryId, EName> HardcodedNamesReverse = new Dictionary<FNameEntryId, EName>();
|
||||
|
||||
/// <summary>
|
||||
/// Map existing strings to a <see cref="FName.Index"/>.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, FNameEntryId> Names = new Dictionary<string, FNameEntryId>();
|
||||
private static readonly Dictionary<FNameEntryId, string> NamesReverse = new Dictionary<FNameEntryId, string>();
|
||||
|
||||
private static uint _counter;
|
||||
|
||||
static FNamePool()
|
||||
{
|
||||
// Initialize hardcoded names.
|
||||
foreach (var (key, value) in UnrealNames.Names)
|
||||
{
|
||||
var index = Store(value);
|
||||
HardcodedNames[key] = index;
|
||||
HardcodedNamesReverse[index] = key;
|
||||
}
|
||||
}
|
||||
|
||||
public static EName? FindEName(FNameEntryId index)
|
||||
{
|
||||
if (HardcodedNamesReverse.TryGetValue(index, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static FNameEntryId Find(string name)
|
||||
{
|
||||
if (Names.TryGetValue(name, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return new FNameEntryId((uint)EName.None);
|
||||
}
|
||||
|
||||
public static FNameEntryId Find(EName name)
|
||||
{
|
||||
return HardcodedNames[name];
|
||||
}
|
||||
|
||||
public static string Resolve(FNameEntryId index)
|
||||
{
|
||||
return NamesReverse[index];
|
||||
}
|
||||
|
||||
public static FNameEntryId Store(ReadOnlySpan<char> valueSpan)
|
||||
{
|
||||
var value = valueSpan.ToString();
|
||||
|
||||
if (Names.TryGetValue(value, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
lock (NamesLock)
|
||||
{
|
||||
// Check again incase a previous lock added it.
|
||||
if (Names.TryGetValue(value, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create new FName.
|
||||
result = new FNameEntryId(_counter++);
|
||||
|
||||
// Store FName.
|
||||
Names[value] = result;
|
||||
NamesReverse[result] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace Prospect.Unreal.Core.Names
|
||||
{
|
||||
/// <summary>
|
||||
/// Hardcoded names in Unreal Engine 4.26.2. See "UnrealNames.inl"
|
||||
/// </summary>
|
||||
public static class UnrealNames
|
||||
{
|
||||
public const int MaxNetworkedHardcodedName = 410;
|
||||
@@ -217,15 +220,11 @@ namespace Prospect.Unreal.Core.Names
|
||||
names.Add(702, "Root");
|
||||
|
||||
// Save.
|
||||
Names = names;
|
||||
FNames = Names.ToDictionary(x => (UnrealNameKey) x.Key, y => new FName(y.Value, y.Key));
|
||||
MaxHardcodedNameIndex = Names.Last().Key + 1;
|
||||
Names = names.ToDictionary(x => (EName) x.Key, y => y.Value);
|
||||
MaxHardcodedNameIndex = (int)Names.Last().Key + 1;
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<int, string> Names { get; }
|
||||
|
||||
public static IReadOnlyDictionary<UnrealNameKey, FName> FNames { get; }
|
||||
|
||||
public static IReadOnlyDictionary<EName, string> Names { get; }
|
||||
public static int MaxHardcodedNameIndex { get; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user