Dumper-7 FName 名称系统
本文是 Dumper-7 偏移推断算法 的姊妹篇。 偏移推断文档讲解了"如何在内存中定位 FName 字段并检测其布局", 本文则讲解"如何把 FName 中的整数索引还原成可读字符串"——这是生成 SDK 中所有类名、成员名、函数名的前提。
目录
- 概述:FName 与 NameArray 的关系
- FName 结构与四种变体
- 两种名称存储后端
- GNames / FNamePool 的自动定位算法
- 名称表的初始化与索引函数
- FNameEntry 的字符串解析
- FName → 字符串的三种转换策略
- PostInit:动态修正块偏移位数
- 版本兼容性总结
- 关键函数与数据结构索引
1. 概述:FName 与 NameArray 的关系
1.1 为什么需要 NameArray
在 Unreal Engine 中,所有的名称(对象名、类名、属性名、函数名、枚举值名……)都不是以字符串形式直接存储在对象里的,而是被**池化(interned)**为一个全局名称表中的索引。FName 只保存这个索引(以及可选的数字后缀),真正的字符串存放在全局表中:
对象 A 的名字 "PlayerController"
│
▼
FName { ComparisonIndex = 12345, Number = 0 }
│
│ 通过全局名称表查询
▼
全局名称表[12345] ──► "PlayerController"
Dumper-7 要生成 SDK,必须把每个对象、属性、函数的 FName 翻译回字符串。NameArray 模块就是负责"在运行时进程内存中找到这个全局名称表,并提供索引 → 字符串的查询能力" 的核心组件。
1.2 与 OffsetFinder 的分工
| 模块 | 职责 | 产物 |
|---|---|---|
OffsetFinder::InitFNameSettings() |
检测 FName 的布局(大小、是否有 Number 字段、是否大小写保留) |
Off::FName::Number、Off::InSDK::Name::FNameSize、bUseCasePreservingName、bUseOutlineNumberName |
NameArray |
定位全局名称表并实现索引 → 字符串的解码 | Off::InSDK::NameArray::GNames、ByIndex 索引函数、FNameEntry::GetStr 解码器 |
FName::Init_Windows() |
选择 FName → 字符串的转换策略(引擎函数 vs 直接查表) |
FName::ToStr 函数指针 |
三者协作的顺序在 Generator::InitEngineCore() 中体现:
ObjectArray::Init(); // 先找到 GObjects
CALL_PLATFORM_SPECIFIC_FUNCTION(FName::Init); // FName::Init_Windows():选择转换策略,必要时初始化 NameArray
Off::Init(); // 其中调用 InitFNameSettings() 检测 FName 布局
核心要点:
FName::Init与NameArray是"按需"协作的——如果引擎导出了AppendString函数,Dumper 优先调用引擎自己的函数把FName转成字符串,根本不需要手动遍历名称表;只有当找不到引擎函数时,才退回到NameArray的手动查表方案。
2. FName 结构与四种变体
FName 在不同的编译宏与引擎版本下有四种布局。它们决定了 Off::FName::Number、FNameSize 的取值,也决定了名称表里是否额外存放 Number。
| 变体 | 触发宏 | 内存大小 | 布局 |
|---|---|---|---|
| 默认 | (无) | 8 字节 | { int32 ComparisonIndex; uint32 Number; } |
| 大小写保留 | WITH_CASE_PRESERVING_NAME |
16 字节 | { int32 ComparisonIndex; int32 DisplayIndex; uint32 Number; ... } |
| 外联编号 | FNAME_OUTLINE_NUMBER |
4 字节 | { int32 ComparisonIndex; },Number 移入 FNameEntry |
| 大小写保留 + 外联编号 | 两者皆有 | 8 字节 | { int32 ComparisonIndex; int32 DisplayIndex; } |
对应的内部标志(定义于 Settings.h 的 Settings::Internal 命名空间):
/* 使用 FNamePool(UE4.23+)而非 TNameEntryArray */
inline bool bUseNamePool = false;
/* 使用大小写保留 FName,FName 中多出 int32 DisplayIndex */
inline bool bUseCasePreservingName = false;
/* 使用 FNameOutlineNumber,把 Number 从 FName 移入 FNamePool 中的 FNameEntry */
inline bool bUseOutlineNumberName = false;
这些标志由
OffsetFinder::InitFNameSettings()自动推断,推断算法详见 Dumper-7 偏移推断算法。
3. 两种名称存储后端
UE 在 4.23 版本前后更换了名称表的底层数据结构。Dumper-7 通过 Settings::Internal::bUseNamePool 区分两者,并为每一种实现独立的索引函数。
3.1 TNameEntryArray(GNames,UE < 4.23)
旧版的全局名称表是一个"分块指针数组 + 计数字段"的结构:
GNames 指向的内存(TNameEntryArray)
┌────────────────────────────────────────┐
│ +0x00 : Chunk[0] (FNameEntry**) │ ┐
│ +0x08 : Chunk[1] (FNameEntry**) │ │ 一段连续的块指针
│ ... │ │ (每块最多 0x4000 个条目)
│ +N*8 : Chunk[N] (FNameEntry**) │ ┘
├────────────────────────────────────────┤
│ +X : NumElements (int32) │ 总名称数
│ +X+4 : MaxChunkIndex / NumChunks(int32)│ 已用块数
└────────────────────────────────────────┘
索引计算(每块固定 0x4000 = 16384 个条目):
const int32 ChunkIdx = ComparisonIndex / 0x4000; // 第几块
const int32 InChunk = ComparisonIndex % 0x4000; // 块内第几个
return reinterpret_cast<void***>(NamesArray)[ChunkIdx][InChunk];
3.2 FNamePool(UE4.23+)
新版的 FNamePool 把字符串数据直接连续存放在块内(而不是块里存指针再指向条目),并用"块索引 + 块内字节偏移"来寻址:
FNamePool 实例
┌────────────────────────────────────────┐
│ +MaxChunkIndex : int32 (已用块数 - 1) │ ← 由扫描确定
│ +ByteCursor : int32 (当前块写入游标) │
│ ... │
│ +0x10(ChunksStart) : uint8* Block[0] │ ┐ 块指针数组
│ +0x18 : uint8* Block[1] │ │ 每个块指向一段
│ ... │ ┘ 连续的 FNameEntry 数据
└────────────────────────────────────────┘
每个块内部:
[FNameEntry][FNameEntry][FNameEntry]... ← 以 NameEntryStride 对齐
索引计算(块大小由 FNameBlockOffsetBits 位决定,条目按 NameEntryStride 对齐):
const int32 ChunkIdx = ComparisonIndex >> NamePoolBlockOffsetBits;
const int32 InChunkOffset = (ComparisonIndex & ((1 << NamePoolBlockOffsetBits) - 1)) * NameEntryStride;
return Block[ChunkIdx] + InChunkOffset;
关键差异:旧版块大小固定为
0x4000,新版块大小是1 << FNameBlockOffsetBits,且条目以字节为单位寻址(乘以NameEntryStride)。FNameBlockOffsetBits与NameEntryStride都需要在运行时探测——见第 5、8 章。
4. GNames / FNamePool 的自动定位算法
定位全局名称表是整个系统的难点:它是一个全局变量,没有符号,且新旧版本的引用方式完全不同。Dumper-7 对两种后端分别实现了基于"已知字符串引用 + 汇编特征 + 导入函数验证"的多重启发式定位。
4.1 TryFindNameArray_Windows(定位 GNames)
定位思路:FName::StaticInit() 中一定会引用字符串 "ByteProperty",并在其附近调用 EnterCriticalSection。顺着这个锚点就能找到 GNames 或返回它的 GetNames() 函数。
1. 在全部节区中查找字符串 "ByteProperty" 的引用地址
│
2. 从该地址向上回扫,寻找 call 指令(机器码 0xFF.../0xE8)
│ 解析其相对调用目标
│
3. 若调用目标 == EnterCriticalSection 的导入地址
──► 确认这里是 FName 的静态初始化代码
│
4. 检查紧随其后的指令:
├─ 0xE8 (相对 call) ──► 这是 FName::GetNames() 函数
│ 需进一步进入函数体找 mov reg,[GNames]
└─ 0x48 0x8B (mov) ──► GNames 已被内联引用,直接解析 RIP 相对地址
│
5. 解析出地址,保存到 Off::InSDK::NameArray::GNames
关键在于通过 Architecture_x86_64::Resolve32BitRelativeCall / Resolve32BitRelativeMove 把相对寻址的机器码还原为绝对地址,并用 Platform::GetAddressOfImportedFunctionFromAnyModule 取得 EnterCriticalSection 的真实地址来做交叉验证。
4.2 TryFindNamePool_Windows(定位 FNamePool)
定位思路:FNamePool 是一个全局单例,它的构造函数会被 lea rcx, [rel FNamePool]; call FNamePool::FNamePool() 这样调用,而构造函数内部会调用 InitializeSRWLock 并引用字符串 "ByteProperty"。
1. 特征码扫描 "48 8D 0D ? ? ? ? E8"
= lea rcx, [rel ?] (? = FNamePool 实例地址)
call ? (? = FNamePool 构造函数)
│
2. 解析 call 目标 → 候选构造函数地址
│
3. 在构造函数体内 0x50 字节范围内搜索 "FF 15"(call [rel import])
解析其目标,验证 == InitializeSRWLock 或 RtlInitializeSRWLock 的导入地址
│
4. 在构造函数 0x2A0 字节范围内验证存在字符串 "ByteProperty" 的引用
(防止误匹配到其它含 SRWLock 的构造函数)
│
5. 解析步骤 1 中 lea 的 RIP 相对地址 → FNamePool 实例地址
保存到 Off::InSDK::NameArray::GNames
为什么用两层验证(SRWLock + ByteProperty):仅靠
lea + call特征会匹配到大量无关的构造函数;InitializeSRWLock调用把候选缩小到"带锁的单例构造";"ByteProperty"字符串则进一步确认这就是名称池——因为引擎在名称池初始化时会预先注册一批内建属性名。
这两个查找函数声明在 NameArray.h:54-55,实现位于 NameArray.cpp(分别在第 364、420 行附近)。它们都属于平台相关函数,通过
CALL_PLATFORM_SPECIFIC_FUNCTION宏分发。
5. 名称表的初始化与索引函数
定位到名称表地址后,还需要确定它内部各字段的偏移,并绑定正确的索引函数 ByIndex。这由 InitializeNameArray / InitializeNamePool 完成。
5.1 InitializeNameArray(旧版)
NameArray::InitializeNameArray(NameArray.cpp:144)通过"先一串有效指针、随后出现计数字段"的模式自动确定 NumElements 与 MaxChunkIndex 的偏移:
// 思路:块指针区都是有效指针;指针区结束后紧跟两个 int32(NumElements、NumChunks)
// 当某处读出的 int32 恰好等于"前面统计到的有效块指针个数",即认定找到了计数字段
for (int i = 0; i < 0x800; i += sizeof(void*))
{
uint8_t* SomePtr = *reinterpret_cast<uint8_t**>(NameArray + i);
if (SomePtr == 0) ZeroQWordCount++; // 进入计数区
else if (ZeroQWordCount == 0) ValidPtrCount++; // 仍在指针区
else /* 指针区已结束 */
{
int32 NumElements = *(int32*)(NameArray + i);
int32 NumChunks = *(int32*)(NameArray + i + 4);
if (NumChunks == ValidPtrCount) // 找到了!
{
Off::NameArray::NumElements = i;
Off::NameArray::MaxChunkIndex = i + 4;
ByIndex = [](void* NamesArray, int32 ComparisonIndex, int32 /*unused*/) -> void*
{
const int32 ChunkIdx = ComparisonIndex / 0x4000;
const int32 InChunk = ComparisonIndex % 0x4000;
if (ComparisonIndex > NameArray::GetNumElements())
return nullptr;
return reinterpret_cast<void***>(NamesArray)[ChunkIdx][InChunk];
};
return true;
}
}
}
5.2 InitializeNamePool(新版)
NameArray::InitializeNamePool(NameArray.cpp:195)固定了 ChunksStart = 0x10、ByteCursor = 0x4,然后扫描确定 MaxChunkIndex 的偏移——判据是"某个 int32 字段的值恰好等于实际非空块指针的个数减一":
for (int i = 0; i < 0x20; i += 4)
{
const int32 PossibleMaxChunkIdx = *(int32*)(NamePool + i);
if (PossibleMaxChunkIdx <= 0 || PossibleMaxChunkIdx > 0x10000)
continue;
// 统计从 i+8 开始连续非空的块指针个数(连续 0x500 个空指针即认为结束)
int32 NotNullptrCount = /* ... 扫描 ... */;
if (PossibleMaxChunkIdx == (NotNullptrCount - 1)) // 匹配!
{
Off::NameArray::MaxChunkIndex = i;
Off::NameArray::ByteCursor = i + 4;
break;
}
}
随后还会用两个魔数字符串做最终校验,确保第 0 块确实是名称数据:
constexpr uint64 CoreUObjAsUint64 = 0x6A624F5565726F43; // 小端 "/Script/CoreUObject" 的片段
constexpr uint32 NoneAsUint32 = 0x656E6F4E; // 小端 "None"
随后 FNameEntry::Init() 被调用,传入第 0 块指针,自动探测条目内字符串的偏移与长度位移量(见下一章),并据此确定 NameEntryStride,最终绑定 NamePool 版本的 ByIndex 索引函数(使用 FNameBlockOffsetBits 与 NameEntryStride)。
ByIndex 是一个统一的函数指针,签名为:
static inline void* (*ByIndex)(void* NamesArray, int32 ComparisonIndex, int32 NamePoolBlockOffsetBits) = nullptr;
旧版实现忽略第三个参数并硬编码 0x4000,新版实现使用第三个参数做移位——这样上层 NameArray::GetNameEntry() 无需关心后端差异。
6. FNameEntry 的字符串解析
FNameEntry::Init()(NameArray.cpp:38)负责自动探测条目布局,并生成一个解码 lambda GetStr 缓存起来。整个解析有两个分支。
6.1 FNamePool 版本:探测长度位移量
新版 FNameEntry 的头部是一个 uint16,其中混合了 bIsWide 标志位与名称长度——但长度占据的位数在不同版本不同。Dumper-7 用一个绝妙的办法探测:找到已知的 "ByteProperty" 条目(长度恰为 0xC = 12),把它的头部不断右移,直到值等于 12,移位次数就是长度字段的偏移位数。
constexpr uint16 BytePropertyStrLen = 0xC; // "ByteProperty" 长度
uint16 BytePropertyHeader = *(uint16*)(AssumedBytePropertyEntry + HeaderOffset);
while (BytePropertyHeader != BytePropertyStrLen && FNameEntryLengthShiftCount < 16)
{
FNameEntryLengthShiftCount++;
BytePropertyHeader >>= 1;
}
确定位移量后,解码 lambda 如下:
GetStr = [](uint8* NameEntry) -> std::wstring
{
const uint16 HeaderWithoutNumber = *(uint16*)(NameEntry + HeaderOffset);
const int32 NameLen = HeaderWithoutNumber >> FNameEntry::FNameEntryLengthShiftCount;
if (NameLen == 0) // 特殊情况:哈希条目(外联编号),见 6.3
{ /* ... */ }
if (HeaderWithoutNumber & NameWideMask) // 最低位 = bIsWide
return std::wstring((const wchar_t*)(NameEntry + StringOffset), NameLen);
return UtfN::StringToWString(std::string((const char*)(NameEntry + StringOffset), NameLen));
};
其中 HeaderOffset = (StringOffset == 6 ? 4 : 0)——这是为了兼容大小写保留模式下条目头部多出的 2 字节对齐。
6.2 TNameEntryArray 版本:扫描定位字段
旧版没有紧凑的头部,需要分别扫描"字符串偏移"与"索引偏移":
// 用 None(索引 0)定位字符串字段:找到内容为 'enoN'(小端 "None")的偏移
for (int i = 0; i < 0x20; i++)
if (*(uint32*)(FNameEntryNone + i) == 'enoN') { StringOffset = i; break; }
// 用索引 3、8 两个条目定位 Index 字段:(值 >> 1) 恰好等于 3 和 8
// (最低位是 bIsWide 掩码,所以右移 1 位才是真正的索引值)
for (int i = 0; i < 0x20; i++)
if ((*(uint32*)(Entry3 + i) >> 1) == 0x3 && (*(uint32*)(Entry8 + i) >> 1) == 0x8)
{ IndexOffset = i; break; }
解码 lambda:
GetStr = [](uint8* NameEntry) -> std::wstring
{
const int32 NameIdx = *(int32*)(NameEntry + IndexOffset);
const void* NameString = NameEntry + StringOffset;
if (NameIdx & NameWideMask) // 最低位 = bIsWide
return std::wstring((const wchar_t*)NameString);
return UtfN::StringToWString<std::string>((const char*)NameString);
};
6.3 特殊情况:哈希条目(外联编号)
在 FNAME_OUTLINE_NUMBER 模式下,带数字后缀的名字(如 Actor_3)不会单独存一个条目,而是存一个长度为 0 的"哈希条目",它指向真正的基名条目并携带一个 Number:
if (NameLen == 0)
{
const int32 EntryIdOffset = StringOffset + ((StringOffset == 6) * 2);
const int32 NextEntryIndex = *(int32*)(NameEntry + EntryIdOffset);
const int32 Number = *(int32*)(NameEntry + EntryIdOffset + sizeof(int32));
if (Number > 0) // 递归取基名,并拼接 "_数字"(注意减 1)
return NameArray::GetNameEntry(NextEntryIndex).GetWString() + L'_' + std::to_wstring(Number - 1);
return NameArray::GetNameEntry(NextEntryIndex).GetWString();
}
为什么 Number 要减 1:UE 内部约定
Number == 0表示"无后缀",Number == 1对应显示的_0。因此把内部 Number 转成显示后缀时需要Number - 1。
7. FName → 字符串的三种转换策略
FName::Init_Windows()(UnrealTypes.cpp)在初始化时会按优先级尝试三种策略,并把最终选定的方案绑定到 FName::ToStr 函数指针。后续所有 FName::ToWString() / ToRawWString() 都通过这个指针完成。
7.1 策略一:调用引擎的 AppendString(最优)
如果能在二进制中定位到引擎的 FName::AppendString(FString&) 函数,就直接借用它——这是最稳健的,因为它就是引擎自己用的实现。
thread_local FFreableString TempString(1024);
AppendString(Name, TempString); // 引擎函数把 FName 写入 FString
std::wstring Result = TempString.ToWString();
定位方式:通过字符串 "ForwardShadingQuality_" 等已知引用,配合若干 x64 汇编特征找到调用点,再用 Resolve32BitRelativeCall 解析出函数地址,保存到 Off::InSDK::Name::AppendNameToString。
7.2 策略二:内联的 GetNameEntry + GetName(次优)
某些编译配置把 AppendString 内联了,此时通过特征码识别"取条目 + 取名字"的两次调用序列:
mov ecx, [...] ; 加载 ComparisonIndex
call FName::GetComparisonNameEntry
lea rdx, [...] ; 目标 FString
mov rcx, rax
call FNameEntry::GetName
识别后分别绑定 GetNameEntryFromName 与 AppendString 两个函数指针。
7.3 策略三:GNames 直接查表(兜底)
当上面两种引擎函数都找不到时,退回到手动查表——这正是第 4~6 章构建的能力:
ToStr = [](const void* Name) -> std::wstring
{
if (!Settings::Internal::bUseOutlineNumberName)
{
const uint32 Number = FName(Name).GetNumber();
if (Number > 0)
return NameArray::GetNameEntry(Name).GetWString() + L'_' + std::to_wstring(Number - 1);
}
return NameArray::GetNameEntry(Name).GetWString();
};
7.4 三种策略对应的枚举
FName::EOffsetOverrideType(UnrealTypes.h)允许使用者手动指定使用哪种策略(在 Generator::InitEngineCore() 里有注释掉的手动覆盖示例):
enum class EOffsetOverrideType
{
AppendString, // 强制使用引擎的 AppendString 函数
ToString, // 使用 FName::ToString(仅在找不到 AppendString 时)
GNames // 强制走 GNames / FNamePool 手动查表
};
┌─────────────── FName::ToStr 选择决策 ───────────────┐
│ │
│ 找到 AppendString 函数? ──是──► 策略一 AppendString │
│ │否 │
│ 识别到内联 GetName 序列? ──是──► 策略二 内联调用 │
│ │否 │
│ 初始化 NameArray 成功? ──是──► 策略三 直接查表 │
│ │否 │
│ ▼ │
│ 报错 / 需手动覆盖 │
└─────────────────────────────────────────────────────┘
8. PostInit:动态修正块偏移位数
FNamePool 的块大小(即每块容纳多少个名称)在不同游戏可能不同。FNameBlockOffsetBits 默认值为 0x10(见 NameArray.h:39),但实际值需要根据真实数据推断。
NameArray::PostInit()(NameArray.cpp:636)的做法是:反向遍历 GObjects,找到 ComparisonIndex 最大的那个对象,逐步增大 FNameBlockOffsetBits 直到它算出的块索引与已知块数自洽:
void NameArray::PostInit()
{
if (GNames && Settings::Internal::bUseNamePool)
{
NameArray::FNameBlockOffsetBits = 0xE; // 从 14 位(块大小 16384)起试
for (int i = ObjectArray::Num(); i >= 0; i--)
{
const int32 ObjNameChunkIdx = Obj.GetFName().GetCompIdx() >> NameArray::FNameBlockOffsetBits;
if (ObjNameChunkIdx > CurrentBlock)
NameArray::FNameBlockOffsetBits++; // 块索引越界,说明位数不够,增大
if (ObjNameChunkIdx == CurrentBlock)
break;
}
Off::InSDK::NameArray::FNamePoolBlockOffsetBits = NameArray::FNameBlockOffsetBits;
}
}
这个偏移位数最终会写入生成的 SDK,使运行期 SDK 能用同样的公式索引名称池。
9. 版本兼容性总结
NameArray 系统通过若干运行时探测,实现了对 UE4.15 ~ UE5.4+ 的统一支持:
| 版本特征 | 检测方式 | 影响 |
|---|---|---|
| GNames vs FNamePool | bUseNamePool(由布局检测得出) |
选择 InitializeNameArray 或 InitializeNamePool,绑定不同 ByIndex |
| FNameEntry 头部位数 | 用 "ByteProperty"(长 12)移位探测 |
确定 FNameEntryLengthShiftCount |
| 字符串/索引字段偏移 | 用 "None"、索引 3/8 扫描 |
确定 StringOffset、IndexOffset(旧版) |
| 宽字符 vs 窄字符 | 头部/索引最低位 bIsWide |
选择 wchar_t 或 char 解码 |
| 外联编号(Outline Number) | bUseOutlineNumberName + 长度为 0 的哈希条目 |
名字后缀从 FNameEntry 递归取得 |
| 大小写保留 | bUseCasePreservingName |
HeaderOffset 偏移 2 字节对齐 |
| 块大小(NamePool) | PostInit 反向遍历对象推断 |
确定 FNameBlockOffsetBits |
10. 关键函数与数据结构索引
10.1 关键函数
| 函数 | 位置 | 职责 |
|---|---|---|
NameArray::TryFindNameArray_Windows() |
NameArray.cpp:364 | 定位旧版 GNames 全局变量 |
NameArray::TryFindNamePool_Windows() |
NameArray.cpp:420 | 定位新版 FNamePool 单例 |
NameArray::InitializeNameArray() |
NameArray.cpp:144 | 确定 TNameEntryArray 字段偏移并绑定 ByIndex |
NameArray::InitializeNamePool() |
NameArray.cpp:195 | 确定 FNamePool 字段偏移并绑定 ByIndex |
NameArray::TryInit() |
NameArray.cpp:504 / 558 | 顶层初始化入口(自动 / 手动覆盖两个重载) |
NameArray::PostInit() |
NameArray.cpp:636 | 推断 FNameBlockOffsetBits |
NameArray::GetNameEntry() |
NameArray.h:71-72 | 由 FName 或索引取得 FNameEntry |
FNameEntry::Init() |
NameArray.cpp:38 | 探测条目布局,生成 GetStr 解码器 |
FNameEntry::GetWString() |
NameArray.cpp:17 | 调用 GetStr 返回宽字符串 |
FName::Init_Windows() |
UnrealTypes.cpp | 选择 FName → 字符串转换策略 |
FName::ToWString() |
UnrealTypes.cpp | 通过 ToStr 完成转换 |
10.2 关键全局状态
| 变量 | 位置 | 含义 |
|---|---|---|
NameArray::GNames |
NameArray.cpp:10 | 名称表基址(GNames 或 FNamePool 实例) |
NameArray::FNameBlockOffsetBits |
NameArray.h:39 | NamePool 块大小的位数(默认 0x10,PostInit 修正) |
NameArray::NameEntryStride |
NameArray.h:44 | NamePool 中条目的字节步长 |
NameArray::ByIndex |
NameArray.h:46 | 索引 → 条目地址的后端无关函数指针 |
FNameEntry::FNameEntryLengthShiftCount |
NameArray.h:14 | 头部长度字段的右移位数 |
FNameEntry::GetStr |
NameArray.h:16 | 条目 → 宽字符串的解码器 |
FName::ToStr |
UnrealTypes.h | FName → 宽字符串的转换器 |
文档版本: 1.0 最后更新: 2026-06-30 基于 Dumper-7 源码分析(NameArray.cpp / NameArray.h / UnrealTypes.cpp / UnrealTypes.h)
评论
- 还没有评论,来说点什么吧。