使用 RegisterCustomEvent 钩住蓝图事件与函数:UE4SS Lua 自定义事件拦截全指南 游戏开发逆向工程【免费下载链接】RE-UE4SSInjectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games项目地址https://gitcode.com/gh_mirrors/re/RE-UE4SS点击查看免费下载RegisterCustomEvent 是 UE4SS 提供给 Lua Mod 的一个全局函数用于注册一个回调当游戏中的蓝图函数或事件以指定名称被调用时该回调会被自动触发。本文基于 官方 API 文档 展开结合仓库源码LuaMod.cpp、LuaMod.hpp与真实 Mod 用例BPML_GenericFunctions完整讲解参数约定、回调签名、返回值处理、底层实现原理与最佳实践。读完本文你将掌握如何在 UE4/UE5 游戏中按函数名拦截蓝图调用、读写参数与返回值并理解其背后的钩子机制。RegisterCustomEvent 是什么按名字钩住任意蓝图事件与函数在 UE4SS 的 Lua 脚本体系中绝大多数回调式 API如RegisterLoadMapPreHook、RegisterBeginPlayPreHook等钩住的是固定的引擎生命周期事件而RegisterCustomEvent的特殊之处在于它钩住的是任意蓝图函数或事件——只要目标函数在运行时以你注册的名字被调用回调就会触发。其核心机制可以概括为两步注册时把事件名与 Lua 回调函数记录到全局钩子容器中事件名会被转换成 Unreal 的FName用于后续匹配触发时每次蓝图函数/事件被调用时钩子层会取出当前UFunction的名字与所有已注册的事件名逐一比对命中后调用对应的 Lua 回调。从源码结构看这一比对发生在 LuaMod.cpp 的script_hook钩子函数中它对m_custom_event_callbacks容器调用find_function_hook_data(callback_container, node-GetNamePrivate())即以UFunction的私有名字GetNamePrivate为键进行精确匹配。这也是为什么注册时的事件名必须与蓝图函数/事件的真实名字完全一致包括大小写。典型应用场景包括监听游戏内特定蓝图事件实现事件总线式的跨 Mod 通信在蓝图函数调用时截获参数进行日志记录、数据统计或参数篡改配合 BP 事件系统把游戏内事件转发到 Lua 侧处理参见下文 BPML_GenericFunctions 案例。函数签名与参数说明RegisterCustomEvent在 Lua 侧暴露的签名如下与 LuaMod.cpp 中定义的错误提示文案一致RegisterCustomEvent(string EventName, LuaFunction Callback)#类型说明1string要钩住的事件名称蓝图函数或事件的名称需与运行时UFunction名字精确一致。2function事件被调用时执行的回调函数。参数校验非常严格如果第一个参数不是 string或第二个参数不是 functionUE4SS 会直接抛出 Lua 错误错误信息为No overload found for function RegisterCustomEvent. Overloads: #1: RegisterCustomEvent(string EventName, LuaFunction Callback)此外注册全程受std::recursive_mutexm_thread_actions_mutex保护LuaMod.cpp保证在游戏多线程环境下注册操作的线程安全。回调函数的调用约定回调函数触发时其参数顺序是有固定约定的这一点在官方文档中没有展开但在源码与真实 Mod 中有明确体现第一个参数永远是ParamContext即触发本次调用的UObject实例以RemoteUnrealParam包装LuaMod.cpp 中LuaType::RemoteUnrealParam::construct(lua, Context, ...)随后依次是蓝图函数的各个参数按UFunction的参数顺序压入同样以RemoteUnrealParam包装Operation::GetParam回调可以返回一个值如果蓝图函数本身有返回值且 Lua 回调返回了非 nil 的值UE4SS 会尝试将该值写回蓝图返回值详见下文返回值处理。因此回调的标准形态是RegisterCustomEvent(MyCustomEvent, function(ParamContext, Param1, Param2, ...) -- ParamContext 是触发事件的 UObject 实例 -- Param1、Param2 等是蓝图函数的参数RemoteUnrealParam 包装 end)RemoteUnrealParam的详细用法可参考 remoteunrealparam 文档。快速上手最小示例官方文档给出的最小示例registercustomevent.mdRegisterCustomEvent(MyCustomEvent, function() print(MyCustomEvent was called\n) end)当游戏中的任何蓝图事件/函数以MyCustomEvent为名被调用时控制台会输出MyCustomEvent was called。带参数的完整示例结合回调调用约定RegisterCustomEvent(PlayerDied, function(ParamContext, ParamPlayer, ParamKiller) local Player ParamPlayer:get() if Player:IsValid() then print(string.format(Player %s was killed\n, Player:GetFullName())) end end)参数访问与类型检查真实 Mod 中的规范写法参数虽然以RemoteUnrealParam包装传入但 UE4SS不会在触发回调前自动校验参数类型。如果蓝图调用方传入了与预期不同的参数类型脚本可能运行到一半才出错。官方随仓库附带的 BPML_GenericFunctions Mod 明确给出了规范做法——必须在回调内手动做类型检查RegisterCustomEvent(PrintToModLoader, function(ParamContext, ParamMessage) -- Retrieve the param value from the param container. local Message ParamMessage:get() -- We must do type-checking here! -- This is to guard against mods that dont use the correct params for their custom event. -- Theres no way to avoid it. if Message:type() ~ FString then error(string.format(PrintToModLoader Param #1 must be FString but was %s, Message:type())) end -- Now the Message param is validated and were safe to use it. local NameParts Explode(ParamContext:get():GetClass():GetFullName(), /); local ModName NameParts[#NameParts - 1] Log(string.format([%s] %s\n, ModName, Message:ToString())) end)这段代码演示了三个关键点取值用ParamMessage:get()从包装容器中取出真实值类型检查用:type()校验取出的值是否为预期类型如FString不匹配时直接error(...)抛出明确错误上下文利用用ParamContext:get()拿到触发事件的 UObject 实例进而通过GetClass():GetFullName()反查调用来源该 Mod 用它解析出发起调用的 Mod 名字。返回值与 Out 参数处理RegisterCustomEvent的回调不仅只读还可以写回参数与返回值这让它具备了拦截并修改蓝图行为的能力。1. Out 参数回写Param:set(value)当蓝图函数带有out参数即 CPF_OutParm时回调中可以通过对应包装参数的:set()方法把值写回蓝图侧。BPML_GenericFunctions 的第二个案例 ConstructPersistentObject 展示了完整流程RegisterCustomEvent(ConstructPersistentObject, function(ParamContext, ParamClass, OutParam) -- Param Type Checking local Class ParamClass:get() if not Class:IsValid() then error(ConstructPersistentObject Param #1 must be a valid UClass) end -- ... 省略类型校验 ... -- Function Logic local GameInstance FindFirstOf(GameInstance) local GarbageCollectionKeepFlags 0x0E000000 local PersistentObject StaticConstructObject(Class, GameInstance, 0, 0, GarbageCollectionKeepFlags, false, false, nil, nil, nil) if not PersistentObject:IsValid() then Log(string.format(Was unable to construct persistent object: %s\n, Class:GetFullName())) end -- Return Value OutParam:set(PersistentObject) end)这里OutParam即蓝图函数的 out/返回值参数通过OutParam:set(PersistentObject)把 Lua 侧构造的持久化对象回写到蓝图调用方。2. Lua 返回值写回蓝图返回值从 script_hook 源码 可以看到回调调用使用lua.call_function(num_unreal_params 1, 1)——即允许回调返回 1 个值。后续逻辑会检查该UFunction是否声明了返回值return_value_offset ! 0xFFFFLua 栈上返回值是否为非 nil返回值类型是否注册了对应的属性处理器LuaType::StaticState::m_property_value_pushers。三者都满足时UE4SS 会把 Lua 的返回值通过Operation::Set写回蓝图返回值内存。若返回值类型没有注册处理器则会输出错误日志Tried altering return value of a custom BP function without a registered handler for return type ...。需要说明的是这类改返回值能力对参数/返回值类型有依赖只有 UE4SS 内置了属性处理器的类型常用 UObject、FString、基本数值等才能完成回写自定义未知类型会走错误分支并被安全丢弃。底层实现剖析注册流程RegisterCustomEvent的注册实现位于 LuaMod.cpp完整流程如下加锁m_thread_actions_mutex校验参数类型string function不合法直接抛错解析事件名通过get_mod_ref(lua)/get_hook_lua(mod)找到当前 Mod 对应的 hook Lua 状态用lua_xmove把回调函数从当前 Lua 状态迁移到 hook 状态用hook_lua-registry().make_ref()为回调函数建立注册表引用registry reference防止其被 GC 回收用Unreal::FName(event_name, Unreal::FNAME_Add)把事件名转成FName并通过find_function_hook_data查询是否已存在同名钩子若不存在则向m_custom_event_callbacks追加一条FunctionHookData记录若已存在则不再追加新的钩子记录即同名事件重复注册不会产生重复触发。数据结构钩子记录的底层结构定义在 LuaMod.hppLuaCallbackData保存 hook Lua 状态指针、instance_of_class以及回调的注册表索引registry_indexes支持同一事件绑定多个 Lua 状态下的回调FunctionHookData由一组Unreal::FName与一个LuaCallbackData组成m_custom_event_callbacks是std::vectorFunctionHookDataLuaMod.hpp与m_script_hook_callbacks并列共同服务于脚本钩子。触发机制每次蓝图函数/事件被调用时钩子层进入 script_hook加锁后对m_custom_event_callbacks执行execute_hook(..., false)——false表示按UFunction名字GetNamePrivate匹配而非按对象指针精确匹配命中后遍历该记录的registry_indexes逐个取出回调引用注册表索引为-1表示已注销跳过每个回调单独用 try/catch 保护LuaMod.cpp一个回调抛异常不会中断其余回调也不会逃逸进钩子层异常会被记录为[script_hook] A callback threw an exception: ...并恢复 Lua 栈顶压入参数先压入Context触发实例再按属性遍历压入蓝图参数Out 参数通过FindOutParamValueAddress取地址普通参数通过ContainerPtrToValuePtr(Stack.Locals())取栈局部值调用回调并处理返回值见上文。重复注册与注销的边界行为值得注意的细节即使同名事件已注册再次调用RegisterCustomEvent时新回调仍会被make_ref引用但不会被加入容器——因为find_function_hook_data命中后直接跳过了emplace_back。从源码行为看这意味着同名事件只保留第一个注册的有效钩子。若需要替换回调请先注销再注册见下文。配套 APIUnregisterCustomEvent与RegisterCustomEvent配套的反向操作是UnregisterCustomEvent其注册于 LuaMod.cpp签名与错误文案为UnregisterCustomEvent(string EventName)#类型说明1string要注销的事件名称。实现上它把事件名转换为字符串后调用remove_function_hook_data(LuaMod::m_custom_event_callbacks, custom_event_name)从容器中移除对应的钩子记录。此后该事件被调用时已注销的回调不会再触发。-- 注册 RegisterCustomEvent(MyCustomEvent, function() print(MyCustomEvent was called\n) end) -- 需要停用时注销 UnregisterCustomEvent(MyCustomEvent)注意钩子数据在 LuaMod.cpp 处也通过erase_from_container(this, m_custom_event_callbacks)在 Lua 状态清理/重载时被统一清除因此 Mod 重载后旧钩子不会残留。实战注意事项与最佳实践事件名必须精确匹配匹配基于UFunction::GetNamePrivate()任何大小写或拼写差异都会导致钩子不触发。建议先用游戏内的对象/函数名 dump 工具如 dumpers确认目标函数的确切名字。回调内必须做参数类型检查UE4SS 不会在触发前校验参数类型错误会在回调内部才暴露BPML_GenericFunctions 的注释直言Theres no way to avoid it这是社区验证过的硬性要求。同名事件只注册一次重复注册同名事件不会产生多个触发若需替换回调应先UnregisterCustomEvent再重新注册。善用 ParamContext第一个参数是触发事件的 UObject 实例可用于获取调用者信息、判断归属等是事件处理中最重要的上下文入口。回调内避免重活钩子会拦截所有同名函数的每次调用回调执行在游戏线程的脚本钩子路径上应保持轻量避免在回调里做阻塞式长任务。类型支持有边界返回值/参数回写依赖m_property_value_pushers中注册的属性处理器超出支持范围的类型只能读取、无法回写且会输出错误日志属预期行为。线程安全由框架保证注册、触发均受递归互斥锁保护你不需要也不应该在 Lua 侧自行加锁。进一步阅读官方 API 索引Lua API 总览 与 SUMMARY参数包装类型用法remoteunrealparam.md底层实现LuaMod.cpp注册/注销/触发三段实现、LuaMod.hpp数据结构实战范例BPML_GenericFunctions/Scripts/main.luaIDE 补全定义shared/Types.luafunction RegisterCustomEvent(EventName, Callback) end通过合理组合RegisterCustomEvent与UnregisterCustomEvent你可以在不改动游戏代码的前提下对 UE4/UE5 游戏中的任意蓝图事件与函数实现监听、参数校验、数据回写乃至返回值篡改这是 UE4SS 脚本体系中连接 Lua 与蓝图世界的关键桥梁之一。赞分享游戏开发逆向工程【免费下载链接】RE-UE4SSInjectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games项目地址https://gitcode.com/gh_mirrors/re/RE-UE4SS点击查看免费下载相关推荐终极指南掌握 deoplete.nvim 自定义事件处理和钩子函数终极指南掌握 deoplete.nvim 自定义事件处理和钩子函数 想要让 deoplete.nvim 这个强大的异步补全框架真正为你所用吗 本文将带你开发工具Mirai 事件系统Events完全指南事件通道、监听器、自定义事件与协程工具函数Mirai 事件系统Events完全指南事件通道、监听器、自定义事件与协程工具函数 Mirai 是一个高效率的 QQ 机器人支持库其大量核心功能消息收即时通讯深入掌握Craft.js自定义编辑事件系统与钩子函数终极指南 想要构建功能强大的React拖拽页面编辑器吗Craft.js作为一个可扩展的React框架提供了完整的事件系统和钩子函数让你能够完全控制编辑器的行为前端上一篇TegraRcmGUIWindows平台最直观的Switch注入工具完全指南下一篇3步解锁Nintendo Switch隐藏功能TegraRcmGUI图形化注入工具终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考