☰
YimMenuV2 多语言支持实现教程:13 种会话语言伪装全解析
2026/10/23 1:50:44 网站建设 项目流程

YimMenuV2 多语言支持实现教程:13 种会话语言伪装全解析

【免费下载链接】YimMenuV2Experimental menu for GTA 5: Enhanced项目地址: https://gitcode.com/GitHub_Trending/yi/YimMenuV2

YimMenuV2 是一款面向 GTA 5: Enhanced 的模组菜单,以 DLL 形式注入游戏进程,提供刷车、刷钱、会话控制等功能。它的"多语言支持(i18n)"和网站教程里那种 UI 翻译完全是两回事:核心能力是让玩家在 GTA Online 的匹配流程里,从 13 种语言中挑选一个"会话语言",让匹配系统优先为你找到说该语言的玩家。下面把这条链路从数据表、数据结构、Hook 拦截到 UI 接入拆开讲清楚。

这个项目的"多语言"场景长什么样

先说痛点:GTA Online 的会话有语言属性,写进匹配请求的MatchmakingAttributes结构体里。原版游戏只按客户端自身设置决定这个值,玩家没法主动选择"我要进一个简体中文会话"或"我要进一个日语会话"。

YimMenuV2 在 Network 子菜单里补了这个缺口:一个 "Spoof Language" 开关,加一个 13 项的语言下拉框,覆盖 English、French、German 直到 Chinese (Simplified) 等语言。🌐

需要说明的是,菜单界面本身没有做界面翻译——所有标签都是硬编码英文。这是注入类工具常见的取舍:用户规模有限、界面文本量小,搭一套完整 i18n 的投入产出比不高,所以项目把"多语言"的精力花在了更实际的会话语言伪装上。

整体思路:Command 承载数据,Hook 承载执行

整个方案分成两层,理解这个分层是读懂后面所有代码的前提:🧩

  • 数据层:语言选项被定义成项目通用的ListCommand(一种命令对象,带 name、label、description 和 int 型状态值),负责"选了什么"以及"记住选了什么"。
  • 执行层:游戏在创建、更新、应答会话匹配时各有一个函数出口,项目用 Detour Hook 把这三个出口都拦下来,把选中的语言值写进发出去的匹配属性里。

数据层和执行层只靠一个开关命令的名字耦合,互不感知。这种"状态与生效时机分离"的写法,是这个项目所有功能的公共骨架。

13 种语言表与 ListCommand 数据模型

语言清单是一张"游戏内部枚举值 + 界面显示名"的映射表,定义在 CustomMatchmaking.cpp 里:

static std::vector<std::pair<int, const char*>> g_LanguageTypes = { {0, "English"}, {1, "French"}, {2, "German"}, {3, "Italian"}, {4, "Spanish (Spain)"}, {5, "Portuguese (Brazil)"}, {6, "Polish"}, {7, "Russian"}, {8, "Korean"}, {9, "Chinese (Traditional)"}, {10, "Japanese"}, {11, "Spanish (Mexico)"}, {12, "Chinese (Simplified)"}, }; ListCommand _Language{"mmlanguage", "Language", "The language to spoof the session to", g_LanguageTypes};

这里有个关键的类型选择:pair 里存的是int而不是字符串。int 是游戏匹配协议里的枚举值,const char*只给 ImGui 下拉框显示用。两者解耦后,UI 文案随便改都不会碰协议值。ListCommand的状态就是一个 int,选中后由 ListCommand.hpp 里的SaveState/LoadState以 JSON 数字形式持久化——重启游戏后你选的语言还在。

伪装值如何写进匹配属性

执行层的全部逻辑集中在PatchMatchmakingAttrs一个函数里:

static void PatchMatchmakingAttrs(MatchmakingAttributes* attrs) { if (Features::_SpoofRegionType.GetState()) attrs->m_ParamValues[4] = Features::_RegionType.GetState(); if (Features::_SpoofLanguage.GetState()) attrs->m_ParamValues[3] = Features::_Language.GetState(); if (Features::_SpoofPlayerCount.GetState()) attrs->m_ParamValues[2] = Features::_PlayerCount.GetState(); }

注意m_ParamValues是裸下标写入:3 是语言、4 是区域、2 是玩家数。这个偏移来自 MatchmakingAttributes.hpp 对游戏结构体的逆向,文件末尾用static_assert(sizeof(MatchmakingAttributes) == 0x10C)锁住了结构体大小——一旦游戏更新导致布局变化,编译期就会报警。

Advertise 与 Update:两个主出口

游戏广播会话(advertise)和周期性刷新(update)时都会携带匹配属性。项目在 MatchmakingAdvertise.cpp 这类钩子里,先调CustomMatchmaking::OnAdvertise完成补丁写入,再放行原函数:

bool Matchmaking::MatchmakingAdvertise(int profile_index, int num_slots, int available_slots, MatchmakingAttributes* data, ...) { if (!CustomMatchmaking::OnAdvertise(num_slots, available_slots, info, data, out_id, status)) return true; return BaseHook::Get<Matchmaking::MatchmakingAdvertise, ...>()->Original()( profile_index, num_slots, available_slots, data, ...); }

钩子只做了"先改参数、再透传"这一件事,不吞掉原函数(除非启用了多倍会话广播功能,那条路径才需要接管整个任务状态机)。

Detail 响应:容易漏掉的第三个出口

还有一处藏得更深:别的玩家客户端来查询你这个会话的详情时,游戏会发一条 session detail 应答报文,里面同样内嵌了MatchmakingAttributes。项目钩了MatchmakingSessionDetailSendResponse,在报文发出去之前再跑一遍PatchMatchmakingAttrs。只改 advertise 不改这里的话,伪装只对自己生效,别人看到的还是原值——这类"同构数据从多个出口流出"的问题,是伪装类功能最常见的翻车点。

UI 接入:开关打开才显示语言下拉框

界面侧的接线在 Network.cpp 里,只有三行:

auto srvSpoofLanguage = std::make_shared<Group>("", 1); srvSpoofLanguage->AddItem(std::make_shared<BoolCommandItem>("mmspooflanguage"_J)); srvSpoofLanguage->AddItem(std::make_shared<ConditionalItem>( "mmspooflanguage"_J, std::make_shared<ListCommandItem>("mmlanguage"_J, "##mmlanguage")));

ConditionalItem监听mmspooflanguage开关,为 true 时才绘制下拉框,避免用户看到一堆没启用的死控件;_J后缀是把命令名做一次 Joaat 哈希(Rage 引擎沿用的字符串哈希),UI 和命令定义之间靠哈希值而非指针绑定,编译期就能防拼写错误。

下拉框的绘制在ListCommandItem::Draw()里用ImGui::BeginCombo实现,有个小细节:控件宽度不是定死的,而是遍历选项找出最长字符串后CalcTextSize加 40px 边距。13 个语言名里Portuguese (Brazil)最长,如果不做这个计算,短名字的列表会把长名字截断。

容易踩的坑与细节

⚠️ 几个读代码时容易忽略的点:

  • 客户端伪装 vs 服务端伪装的区分。Network 菜单里 "Matchmaking (Client)" 和 "Matchmaking (Server)" 是两个组。Client 侧的spoofmmregion是LoopedCommand:启用时先存旧值,之后每个游戏 tick 覆写*Pointers.RegionCode,禁用时还原;Server 侧的mmspooflanguage走的是报文补丁。前者改的是本地内存,后者改的是发出去的字节,手段完全不同,不要混为一谈。
  • 持久化只存 int。语言选择在 JSON 里就是一个数字,列表本身不入库。这意味着如果后续版本给g_LanguageTypes调整了顺序(比如把 index 3 从 Italian 换成别的),老配置会静默变成错误语言——这类"存值不存名"的持久化方案在枚举变更时天然有坑。
  • 菜单 UI 的字体是内嵌的。Fonts.hpp里MainFont(约 77KB)和IconFont(约 1MB)是编译进二进制的字节数组,不含 CJK 字形。设置页只开放了FontGlobalScale缩放(0.5–2.0 倍),没有任何换字体入口。这也解释了为什么界面不翻译:想支持中文界面,光有翻译字符串表没用,字体管线得先动。

可以搬到自己项目的两点

这套实现里真正通用的不是 GTA 相关的部分,而是两个模式:一是"命令对象 = 状态 + 持久化 + UI 标签"的统一抽象,任何需要"选项 + 记住选项 + 可选显示"的功能都能套用,YimMenuV2 里颜色、浮点、向量命令全是同一套骨架;二是"多出口一致性"——只要一份数据会从 N 个函数流出去,补丁就得打 N 次,少打一次就是一次静默失效。

反过来说,如果你做的是需要真正 UI i18n 的项目(比如 Web 或桌面应用),YimMenuV2 恰好提供了反面参照:在动手写翻译表之前,先确认字体能不能渲染目标文字、文本控件宽度会不会被长文案撑爆,这两件事在这个项目里分别对应内嵌字体和CalcTextSize取最长项的做法,顺序颠倒了会返工。

【免费下载链接】YimMenuV2Experimental menu for GTA 5: Enhanced项目地址: https://gitcode.com/GitHub_Trending/yi/YimMenuV2

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询