- 游戏开发
- 逆向工程
【免费下载链接】RE-UE4SS
Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games
导读
UDataTable是 Unreal Engine 中用于存储哈希键值对(行名 → 行数据)的核心数据结构,常见于掉落表、升级经验表、角色基础属性等游戏配置。UE4SS 在 Lua 运行时中为UDataTable提供了完整封装,支持运行时读取、写入、删除、清空与遍历整张数据表。阅读本文后,你将掌握UDataTable的 Lua 对象模型、全部 10 个方法/元方法的参数与返回值约定、引用式访问的底层原理,以及基于行结构体的克隆、批量修改等实战用法。本文以 docs/lua-api/classes/udatatable.md 为主体,并结合 LuaUDataTable.cpp 源码进行纵深剖析。
对象模型概览
在 UE4SS 的 Lua 类型系统中,UDataTable的类继承关系为:
UDataTable → UObject → RemoteObject即 UObject 中定义的所有成员函数(如GetFullName()、GetFName()、GetAddress()、GetClass()、IsValid()、type()等)在UDataTable对象上同样可用。在源码层面,这一关系体现于 LuaUDataTable.hpp 中的class UDataTable : public UObjectBase<Unreal::UDataTable, UDataTableName>:它基于 UObject 构建,并额外注册了数据表专属的元方法与成员函数。
对应的 C++ 侧声明中,UDataTable还关联了一个内部工具结构体FDataTableInfo,用于缓存数据表的行结构体指针(row_struct)、其 FName(row_struct_fname)以及单行内存大小(row_size),所有行级操作都依赖这些信息完成。
元方法
__len
- 用法:
#UDataTable - 返回类型:
integer - 返回数据表的总行数。
该元方法在源码中对应 LuaUDataTable.cpp 的MetaMethod::Length注册逻辑,底层直接读取data_table->GetRowMap().Num(),因此#dt等价于行数统计。
local total = #dataTable print("Total rows:", total)方法详解
IsValid()
- 返回类型:
bool - 返回数据表是否有效(非空指针)。
该方法继承自 UObject 体系,用于在调用行操作前防御性地检查对象是否已被销毁。
if dataTable:IsValid() then -- 安全操作 endGetRowStruct()
- 返回类型:
UScriptStruct|nil - 返回定义该数据表行结构的 UScriptStruct;若未设置则返回 nil。
在源码中,GetRowStruct()调用data_table->GetRowStruct(),若指针为空会显式set_nil()。通过返回的 UScriptStruct 对象,你可以检查行包含哪些字段以及字段类型,例如配合FindFirstOf或反射 API 查看行结构定义。
local rowStruct = dataTable:GetRowStruct() if rowStruct then print("Row struct:", rowStruct:GetFullName()) endGetRowMap()
- 返回类型:
table - 返回包含全部行的 Lua table,以行名为键、行数据为值。
需要注意的是返回值的形式取决于行类型是否存在自定义的"值推送器"(custom pusher)。在 LuaUDataTable.cpp 的GetRowMap()实现中,代码遍历data_table->GetRowMap()(类型为TMap<FName, unsigned char*>),以Pair.Key.ToString()作为 Lua 表键,并将Pair.Value(指向行内存的指针)包装成ScriptStructWrapper后通过UScriptStruct::construct压入 Lua——也就是说默认情况下每个值都是一个UScriptStruct 包装对象,提供引用式访问;如果该行类型注册了专用的 property value pusher,则可能直接以普通 Lua table 形式返回。
local rowMap = dataTable:GetRowMap() for rowName, rowData in pairs(rowMap) do print(rowName, rowData) endFindRow(string RowName)
- 返回类型:
UScriptStruct|nil - 按行名查找行;找到则返回行数据(UScriptStruct),未找到返回 nil。
- 关键特性:返回的结构体是引用式访问——对其字段的修改会直接作用于数据表本身。
- 示例:
local row = dt:FindRow("Player"); row.Health = 200会真实修改数据表。
底层实现(DataTableOperation::FindRow分支)使用data_table->FindRowUnchecked(row_name)直接取得行内存指针,若为 null 则set_nil();否则构造ScriptStructWrapper{.script_struct = row_struct, .start_of_struct = row_data, .property = nullptr}并压栈。这与 C++ 中FindRow返回指向真实行内存指针的行为一致,因此修改立即生效,无需再写回。
local row = dataTable:FindRow("Player") if row then row.Health = 200 -- 直接修改数据表内的 Player 行 endAddRow(string RowName, table|UScriptStruct RowData)
- 向数据表新增一行,指定行名与数据;若同名行已存在则替换。
- 第二参数接受两种形式:
- Lua table:字段需与行结构定义匹配(如
{Health = 100, MaxHealth = 100}); - UScriptStruct:数据从该结构体复制(适合用于克隆已有行)。
- Lua table:字段需与行结构定义匹配(如
- 示例:
dt:AddRow("NewPlayer", {Health = 100, MaxHealth = 100}) - 示例:
dt:AddRow("Clone", dt:FindRow("Original"))— 复制一行
源码中的DataTableOperation::AddRow分支展示了完整的处理链路:
- 校验第一个参数必须是字符串(行名),否则抛出
"AddRow expects a string as the first parameter"; - 通过
lua.is_userdata(1)/lua.is_table(1)区分传入的是 UScriptStruct 还是 Lua table,两者都不是则抛出"AddRow expects a table or UScriptStruct as the second parameter"; - 用
FMemory::Malloc(row_size)分配临时内存并InitializeStruct;- UScriptStruct 路径:调用
CopyScriptStruct(new_row_data, wrapper_data.start_of_struct)从源结构复制字节数据; - Lua table 路径:若该行结构在
StaticState::m_property_value_pushers中注册了专用 pusher 则调用 pusher,否则回退到convert_lua_table_to_struct(lua, info.row_struct, new_row_data, 1, nullptr)通用转换函数;
- UScriptStruct 路径:调用
- 调用
data_table->AddRow(row_name, new_row_data, info.row_struct)将数据复制进数据表内部存储; - 由于
AddRow已复制数据,随后DestroyStruct+FMemory::Free释放临时分配,避免内存泄漏。
-- 用 table 新增一行 dataTable:AddRow("NewPlayer", {Health = 100, MaxHealth = 100}) -- 克隆已有行 local original = dataTable:FindRow("Original") dataTable:AddRow("Clone", original)RemoveRow(string RowName)
- 删除指定名称的行;若行不存在则不做任何事。
对应DataTableOperation::RemoveRow分支,内部直接调用data_table->RemoveRow(row_name),无需额外参数校验或返回值。
dataTable:RemoveRow("ObsoleteRow")EmptyTable()
- 清空数据表的所有行。
- 注意:不会清除 RowStruct 定义,清空后表仍然"知道"自己的行结构,可以继续
AddRow。
dataTable:EmptyTable() -- 表已清空,但 GetRowStruct() 依然有效GetRowNames()
- 返回类型:
table - 返回一个 1 起始索引的数组,包含数据表中所有行名。
实现中遍历data_table->GetRowNames(),以i + 1作为键、行名字符串作为值构建 Lua 表。
local names = dataTable:GetRowNames() for i = 1, #names do print(i, names[i]) endGetAllRows()
- 返回类型:
table - 返回一个 1 起始索引的数组,每个元素是包含两个字段的 table:
Name:行名(string)Data:行数据(table 或 UScriptStruct)
Data字段同样以ScriptStructWrapper方式包装行内存,因此同样具备引用式访问能力——修改entry.Data.xxx会直接改变数据表内容。
for _, entry in ipairs(dataTable:GetAllRows()) do print("Row:", entry.Name) print("Health:", entry.Data.Health) endForEachRow(function Callback)
- 遍历数据表所有行,并为每一行调用回调函数。
- 回调参数:
string rowName、UScriptStruct rowData rowData提供引用式访问——修改会直接影响数据表。- 回调可以返回
true提前终止遍历。
该方法的 C++ 实现是setup_member_functions中唯一一个没有走prepare_to_handle分发、而是内联实现的方法。其关键逻辑包括:
- 构造
FDataTableInfo并调用validate_row_struct(lua),若数据表没有 RowStruct 则抛出"DataTable has no RowStruct specified"; - 每轮迭代通过
lua_pushvalue(lua.get_lua_state(), 1)复制回调函数到栈顶,依次压入行名字符串与行数据 UScriptStruct 包装,再以lua.call_function(2, 1)调用; - 检查返回值:若第 2 个栈位置为布尔
true则break提前退出;否则丢弃栈上的 nil 返回值,防止 Lua 栈在下一轮迭代被污染。
dataTable:ForEachRow(function(rowName, rowData) print(string.format("Row: %s, Health: %d", rowName, rowData.Health)) -- 直接修改行 rowData.Health = rowData.Health + 10 -- 返回 true 停止遍历 if rowName == "TargetRow" then return true end end)实战组合示例
结合上述 API,可以在运行时完成一次完整的"查找 → 修改 → 克隆 → 删除"数据表维护流程:
local dt = FindFirstOf("DataTable") -- 或通过其他方式获取 UDataTable if not dt or not dt:IsValid() then print("DataTable not found") return end -- 1. 读取行并修改(引用式访问,直接生效) local player = dt:FindRow("Player") if player then player.Health = player.Health + 50 end -- 2. 新增一行 dt:AddRow("NewPlayer", {Health = 100, MaxHealth = 100}) -- 3. 克隆一行 dt:AddRow("Clone", dt:FindRow("Player")) -- 4. 遍历并统计 local count = 0 dt:ForEachRow(function(name, data) count = count + 1 print(name, data.Health) end) -- 5. 清理 dt:RemoveRow("Clone") print("Total rows:", #dt)底层原理与注意事项
引用式访问从何而来
FindRow、GetRowMap、GetAllRows、ForEachRow返回的UScriptStruct本质都是 ScriptStructWrapper(包含script_struct、start_of_struct、property三个字段)。start_of_struct直接指向数据表内部行内存(即TMap<FName, unsigned char*>的 Value 指针),因此通过 UScriptStruct 的__index/__newindex读写字段时,操作的就是数据表真实内存,无需额外写回步骤。这也解释了为什么FindRow的注释明确强调"modifications directly affect the DataTable"。
行结构校验
FDataTableInfo::validate_row_struct会在FindRow、AddRow、GetAllRows、ForEachRow前强制校验row_struct是否存在,否则抛出"DataTable has no RowStruct specified"。__len、GetRowStruct、GetRowNames、RemoveRow、EmptyTable不依赖行结构定义,因此不受此限制。
空指针保护
GetRowStruct、GetRowMap在data_table为空时都会set_nil()返回;ForEachRow与prepare_to_handle则在数据表指针为空时直接lua.throw_error("DataTable is null")。在实际 mod 代码中建议先以IsValid()或FindFirstOf的结果判空,再执行行操作。
设计背景
UE4SS 对 DataTable 的完整读写支持并非一蹴而就。官方开发日志 datatables-in-ue4ss.md 记录了早期方案的权衡:直接遍历 UE4SS 自研TMap的GetElementsPtr()只适合只读场景,因为AddRow/RemoveRow之后指针会失效;而利用 KismetUDataTableFunctionLibrary的反射函数又受限于GetDataTableRowFromName这类CustomThunk函数在纯 C++ 调用时Stack.MostRecentProperty无法填充的问题。最终当前版本的 Lua API 采用直接包装行内存指针(ScriptStructWrapper)的方案,实现了文档中描述的完整读/写/增/删/遍历能力。
适用前提
- 本文所述 API 适用于 UE4/UE5 游戏内注入 UE4SS 后的 Lua mod 场景,
UDataTable对象可通过FindFirstOf("DataTable")、FindAllOf或遍历GetRowMap等方式获取; - 行数据字段名必须与行结构定义一致(参考 UScriptStruct 的
__index/__newindex约定),大小写需与游戏内反射出的属性名匹配; - 部分游戏的行结构可能包含内存对齐/填充差异(参见 datatables-in-ue4ss.md 中对 FName 对齐的讨论),若遇到异常字段读写,可从行结构的内存布局角度排查。
方法速查表
| 方法 | 签名 | 返回类型 | 说明 |
|---|---|---|---|
__len | #dt | integer | 行数 |
IsValid | dt:IsValid() | bool | 对象是否有效(继承自 UObject) |
GetRowStruct | dt:GetRowStruct() | UScriptStruct/nil | 行结构定义 |
GetRowMap | dt:GetRowMap() | table | 行名 → 行数据的映射表 |
FindRow | dt:FindRow(name) | UScriptStruct/nil | 按名查行(引用式) |
AddRow | dt:AddRow(name, data) | 无 | 新增/替换行 |
RemoveRow | dt:RemoveRow(name) | 无 | 删除行 |
EmptyTable | dt:EmptyTable() | 无 | 清空所有行(保留 RowStruct) |
GetRowNames | dt:GetRowNames() | table | 行名数组(1 起始) |
GetAllRows | dt:GetAllRows() | table | {Name=..., Data=...}数组 |
ForEachRow | dt:ForEachRow(cb) | 无 | 遍历;回调返回true可提前退出 |
相关资源
- API 文档原文:docs/lua-api/classes/udatatable.md
- 基类文档:UObject、UScriptStruct
- 源码实现:LuaUDataTable.hpp、LuaUDataTable.cpp、LuaUScriptStruct.hpp
- 设计背景与早期方案讨论:datatables-in-ue4ss.md
- 游戏开发
- 逆向工程
【免费下载链接】RE-UE4SS
Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games
相关推荐
Tornado 6.3.3 安全更新深度解析:Content-Length 与 chunked 编码的严格解析如何阻断 HTTP 请求走私
Tornado 6.3.3 安全更新深度解析:Content Length 与 chunked 编码的严格解析如何阻断 HTTP 请求走私 导读 Tornado
游戏开发逆向工程RE-UE4SS Lua API 实战:使用 UE4SS.GetVersion() 获取与检测运行时版本
RE UE4SS Lua API 实战:使用 UE4SS.GetVersion 获取与检测运行时版本 本文围绕 RE UE4SS 官方 Lua API 文档 d
游戏开发逆向工程在 Lua 中使用 FlatBuffers:读写序列化数据的完整指南
在 Lua 中使用 FlatBuffers:读写序列化数据的完整指南 导读 本文以 FlatBuffers 官方文档中 Lua 语言支持章节( docs/sou
序列化代码生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考