从零给 GTA 5 增强版菜单加功能:YimMenuV2 注入菜单框架实战指南
【免费下载链接】YimMenuV2Experimental menu for GTA 5: Enhanced项目地址: https://gitcode.com/GitHub_Trending/yi/YimMenuV2
给 GTA 5 写菜单,最折磨人的从来不是功能本身,而是功能背后的三件事:游戏一更新,写死的函数地址就全废了;想拦截网络消息,得先知道它藏在哪个调用点;写好的逻辑想塞进 ImGui 菜单里,还得手动对位。YimMenuV2 是面向 GTA 5: Enhanced 的开源注入菜单框架,用 C++23 把特征扫描、函数钩子、Lua 热重载脚本这几套基础设施一次搭好,你只管写功能。这篇就走一遍完整流程:克隆、构建、读懂三个关键子系统,最后亲手加一个"自动回血"功能。
项目全景:一个注入式 GTA 5 增强菜单
YimMenuV2 最终产物是一个 DLL,用 Xenos 这类注入器在加载界面或主菜单注入到GTA5_Enhanced.exe,之后按INSERT或Ctrl+\呼出菜单。它分三层:src/core/是通用引擎(内存扫描、钩子、Lua、日志、设置),src/game/是 GTA 专属逻辑(反作弊绕过、玩家管理、各类功能特性),src/game/frontend/是 ImGui 界面。对玩家,它提供神行模式、瞬移、载具管理这类现成功能;对开发者,它把"菜单底座"的部分彻底抽象掉了——你不需要再操心 D3D12 怎么钩、Lua 沙箱怎么建。适合两类人:想魔改菜单行为的玩家,以及想研究"注入式菜单到底怎么落地"的逆向方向开发者。
构建流程:一条主线加四张检查清单
先 clone:
git clone https://gitcode.com/GitHub_Trending/yi/YimMenuV2 cd YimMenuV2CMakeLists.txt只做了三件事:GLOB_RECURSE收走src/下全部源码、把src/common.hpp设为预编译头、以MODULE类型(即 DLL)链接 AsyncLogger、ImGui、MinHook、nlohmann_json、LuaJIT。真正值得注意的是 cmake/cross-compile.cmake:在 Windows 上它默认关闭,用 Visual Studio 2022 即可;在非 Windows 上它默认开启,自动选 Zig 或 Clang+MinGW 交叉编译出 Windows DLL——这意味着 Linux 机器也能直接出包。
清单一 · 工具链:Windows 侧要 Visual Studio 2022(含 C++ 工作负载)+ CMake ≥ 3.20 + Ninja(CMakeSettings.json 里两条预设都是 Ninja 生成器,一条走 MSVC,一条走 clang-cl);Linux 交叉编译侧装 Zig,或者 Clang 加x86_64-w64-mingw32ucrt工具链,二者缺一脚本会直接FATAL_ERROR。
清单二 · 配置:
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo选 Ninja 是为了和官方预设保持一致;RelWithDebInfo是仓库预设里自带的配置,出问题的时候还能看调用栈。
清单三 · 编译:
cmake --build build产出是YimMenuV2.dll(MODULE 库),不是 exe——它是被注入的,没有自己的进程。
清单四 · 跑起来:在启动器里关闭 BattlEye(Steam/Epic 版可能还要加-nobattleye启动参数),正常启动游戏,在加载界面注入 DLL,进主菜单按INSERT。日志落在%appdata%/YimMenuV2/cout.log,后面排障全靠它。
三个子系统拆解
指针扫描:地址为什么会失效,怎么让它别失效
注入菜单面对的第一个现实问题是:游戏更新一次,所有函数地址都漂了。硬编码地址的方案从第一天起就是错的。YimMenuV2 的做法是把"找地址"变成"找字节特征",入口在 src/game/pointers/Pointers.cpp:
// 带通配符的字节序列作为特征 constexpr auto screenResPtrn = Pattern<"75 39 0F 57 C0 F3 0F 2A 05">("ScreenRes"); scanner.Add(screenResPtrn, this { ScreenResX = ptr.Add(0x5).Add(4).Rip().As<std::uint32_t*>(); });PatternScanner在模块映像里搜?通配符特征,命中后回调里用PointerCalculator做Add/Rip/As<T*>链式偏移,从命中点走到真正要用的指针。注意很多特征是"路过式"的:比如InitNativeTables的特征命中在函数中间,要Sub(0x2A)回函数头。扫描只在全局初始化时跑一遍,结果写进 src/core/backend/PatternCache.cpp 维护的哈希缓存,后续启动直接命中缓存,不必重新扫全模块。排障时记住一点:某个指针为nullptr,一定是对应特征失配,日志里会有记录,别急着怀疑注入器。
钩子系统:菜单的全部"超能力"从这里过
钩子层的实现在 src/core/hooking/Hooking.cpp:所有拦截点以静态列表形式注册到BaseHook,统一挂DetourHook,最后由 MinHook 的ApplyQueued()批量生效。名单大致分四块——反作弊路径(GameSkeletonUpdate、GetThreadContext、HttpStartRequest)、脚本引擎(ScriptVM、RunScriptThreads)、网络同步(ReceiveNetMessage、WriteSyncTree)、匹配(MatchmakingAdvertise)。菜单之所以能实时看到谁进了房间,靠的就是AssignPhysicalIndex和ReceiveNetMessage上的 hook。两条使用纪律:hook 函数里处理完必须调用原始函数,断掉调用链游戏直接崩;卸载时Hooking::Destroy()会统一DisableAll并BytePatches::RestoreAll()还原字节补丁,退出路径尽量完整,减少残留。
Lua 脚本层:不重启就能换功能
C++ 改完要重编译注入,而 Lua 是菜单的"热插拔层"。src/core/scripting/LuaManager.cpp 的RunScript每帧做三件事:处理加载队列、处理禁用队列、推进已加载脚本。设计上有两个值得注意的点。第一,加载与卸载全走队列,由脚本帧线程串行消费,UI 线程只 push 请求,不会在渲染线程里动文件系统。第二,禁用/启用是通过"移动文件"实现的:禁用把脚本移进disabled/子树(保留原目录结构),启用再移回原位,所以重启后脚本状态天然一致,不需要额外持久化。沙箱方面,每个脚本独立 LuaJIT 状态,io/os/ffi/loadfile全部禁用,require被改写成只能加载 scripts 目录内的文件;目录名含include的被视为模块(自动跳过加载,供require用),含disabled的进禁用列表。API 细节在 docs/lua-api.md,给编辑器做补全的类型标注在同目录的yimmenu_v2.lua。
实战:给你的第一个功能走一遍闭环
做个"自动回血":开关打开后每帧补血,关闭时归零。C++ 路线分四步。
第一步,在src/game/features/self/下新建AutoHeal.cpp,继承LoopedCommand——它的构造会自动注册进命令中心,Tick每帧替你调OnTick:
#include "core/commands/LoopedCommand.hpp" #include "game/backend/Self.hpp" #include "game/gta/Natives.hpp" namespace YimMenu::Features { class AutoHeal : public LoopedCommand { using LoopedCommand::LoopedCommand; virtual void OnTick() override { if (auto ped = Self::GetPed()) PED::SET_ENTITY_HEALTH(ped.GetHandle(), 100, false); } }; static AutoHeal _AutoHeal{"autoheal", "Auto Heal", "Keep regenerating health"}; }第二步,把它挂进 UI。打开src/game/frontend/submenus/Self.cpp,在toolsGroup里加一行AddItem(std::make_shared<CommandItem>("autoheal"_J));,与旁边的heal、suicide并列。
第三步,编译并按清单四注入。菜单 Self 页的 Tools 分组里应出现 Auto Heal 开关,状态会自动写进settings.json,重启不丢。
如果不想碰 C++,同样需求一个 Lua 脚本就够,放在%appdata%/YimMenuV2/scripts/:
menu.get_submenu():add_category("Heal"):add_looped_checkbox( "luaautoheal", "Auto Heal (Lua)", "Regen health every frame", function() local self = ped.get_self() if self then self:set_health(100) end end )新脚本会出现在 Settings → Lua Scripts 的 Disabled 列表,点一下即加载。两条路线的取舍:要改底层(碰 hook、碰内存)走 C++,功能逻辑走 Lua,后者改完不用重编译,迭代快得多。
性能与稳定性:四条可落地的建议
1. 新增特征前先查缓存。模式扫描只在首次全量执行,命中后按特征哈希存入PatternCache,之后每次启动只查哈希。你要加新指针时走PatternScanner.Add的既有流程,别自己起循环去扫——重复全模块扫描会直接拉长注入后的初始化时间。
2. 循环逻辑里别做重活。LoopedCommand的Tick每帧执行且只跑已启用的项,这是框架帮你做的第一层控制;第二层靠自己:tick 内避免堆分配和文件 IO。参考SuperRun的写法——它每帧只做一次 native 调用,把"恢复默认值"这种一次性动作放到了OnDisable里。
3. 阻塞操作交给脚本协程。Lua 侧的 latent 调用(等待模型加载、等网络结果)会yield当前协程,让出帧给别的脚本。自己写循环时同理:script.run_in_callback里用script.yield(ms)分段执行,而不是while true do ... end卡死协程。
4. imgui 回调只做布局。group:imgui(fn)这类回调在菜单打开时每帧执行,文档明确说不要在回调里script.yield。把数据计算挪到commandmgr.add_looped_command的 tick 里,imgui 回调只负责读缓存好的值画图。
新手避坑清单
现象:注入后菜单直接卸载,日志报特征失配。根因:Pointers::Init()发现GTA5_Enhanced.exe里没有可命中的特征,或某个关键指针为nullptr——通常是游戏版本更新导致字节序列变了。 解法:先翻cout.log确认是哪个特征失配,再用util.get_game_version()对照你的游戏 online version;跨版本用script.require_game_build给脚本加版本门槛,避免旧偏移硬撑新游戏。
现象:公局每五分钟被踢出。根因:README 自己写了——项目没有 BattlEye 完整绕过,正常房主的心跳检查会把你清掉。 解法:要么留在非严格环境,要么等上游补上真正的 bypass;别把这当成注入姿势问题去调。
现象:去掉 FSL 后,之前的进度全没了,甚至游戏起不来。根因:FSL 把账号存档重定向到了磁盘,去掉它后游戏读的是另一份数据。 解法:存档回不来的只能认栽;游戏起不来时删掉Documents/GTAV Enhanced/Profiles目录即可恢复。
现象:新放的 Lua 脚本在菜单里看不到。根因:脚本名含disabled子串(大小写敏感)会被归档不加载;文件放在含include的目录里则被当作模块跳过。 解法:改名、挪出 include 目录,等目录周期刷新后它出现在 Disabled 列表。
现象:Linux 上配置直接 FATAL_ERROR,提示找不到交叉编译工具链。根因:cmake/cross-compile.cmake在非 Windows 平台默认开CROSSCOMPILE,找不到 Zig 和 MinGW 就会硬停。 解法:装 Zig 或 Clang+x86_64-w64-mingw32ucrt;如果本机就是 Windows,加-DCROSSCOMPILE=OFF关掉即可。
YimMenuV2 把注入菜单最脏活(扫描、hook、沙箱、热重载)封装成了可直接继承的基类,你加功能时面对的是一层干净的 API。想继续往下挖:Lua 全量 API 看 docs/lua-api.md,hook 注册与还原逻辑看 src/core/hooking/Hooking.cpp,所有游戏结构体定义集中在src/types/,对着读一遍基本就通了这个框架的骨架。
【免费下载链接】YimMenuV2Experimental menu for GTA 5: Enhanced项目地址: https://gitcode.com/GitHub_Trending/yi/YimMenuV2
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考