# Better Endfield Next Runtime Interfaces 本文记录重构后的进程内接口协议。Host 按元数据定位方法和字段,不保存客户端固定地址或文件哈希白名单。部分模块仍有原生结构布局、枚举及字段回退常量,需要随客户端版本核对;当前 Windows 1.6.3 的检查和修复见[更新记录](research/1.6.3/hook-update/GAME_UPDATE_1_6_3.md)。 ## Host 与模块 Host 导出模块发现所需的 `BetterEndfieldNext_GetModuleApiV1`。模块清单使用 `modules/*.module.ini`,包含模块 ID、DLL、ABI、契约组及 `requires` 程序集列表。Host 等待所需 IL2CPP 程序集注册后,按清单文件名的稳定顺序加载 DLL,先调用 `initialize`,再推送对应配置;依赖未就绪会在 90 秒窗口内重试,ABI 或契约拒绝则不重复加载。模块配置节中的 `enabled=false` 会在本次进程启动时跳过该 DLL;已经加载的模块仍会收到后续配置变更并可停用自身行为,但从关闭改为开启需要下一次注入。 Host 是唯一的 HookBroker 所有者,负责 MinHook 初始化、目标冲突检查、启用、禁用和移除。运行中只停用模块行为,不卸载 DLL;模块卸载在游戏进程结束后完成。 ## 动态 IL2CPP 解析 Host 使用 `GameAssembly.dll` 的 IL2CPP 导出解析: - 域和程序集:`il2cpp_domain_get`、`il2cpp_domain_get_assemblies`、`il2cpp_assembly_get_image`、`il2cpp_image_get_name` - 类与方法:`il2cpp_class_from_name`、`il2cpp_class_get_methods`、`il2cpp_method_get_name`、`il2cpp_method_get_param_count`、`il2cpp_method_get_param`、`il2cpp_method_get_return_type`。Unity 2021 IL2CPP 的 `MethodInfo` ABI 以 `methodPointer` 开头;Host 只接受该入口位于当前 `GameAssembly.dll` 可执行 PE 节的结果,不扫描其他字段,也不尝试客户端地址。 - 嵌套类:当描述符使用 `Outer.Inner` 时,Host 通过 `il2cpp_class_get_nested_types` 和 `il2cpp_class_get_name` 逐级解析嵌套类型;缺少这两个导出只影响使用嵌套类型的模块。 - 字段:`il2cpp_class_get_field_from_name`、`il2cpp_field_get_offset`、`il2cpp_field_get_type` - 线程与字符串:`il2cpp_thread_attach`、`il2cpp_thread_detach`、`il2cpp_string_length`、`il2cpp_string_chars` 每个模块提供程序集、命名空间、类型、方法或字段描述符。Host 工作线程在使用元数据前附加到 IL2CPP 域,退出时解除附加。 ## 开屏模块 `BetterEndfieldNext.Model.dll` 在同一登录场景生命周期中提供模型替换、Logo 与登录色带主题。各能力组独立解析、独立安装 Hook;任一视觉契约缺失只停用对应功能。 模型替换动态验证并使用以下契约: - `Entry.Beyond.dll / Beyond.Login / LoginSceneRoot.OnBindToManager` - `Entry.Beyond.dll / Beyond.Login / LoginSceneAnimCtrl` - `Common.Beyond.dll / Beyond.Resource / StringPathHashBinary.InitMain` 与 `InitInit`(`HashStringPathProcessor.InitMainPathHash/InitInitPathHash` 的实际实现;PC 版编译器已把 `InitMainPathHash` 包装内联进 `GameInitState._ReloadResourceIndexes`,包装上的 Hook 不会触发) - `Common.Beyond.dll / Beyond.Resource / I18NAssetLoader.Load` - Unity `Object`、`GameObject`、`Transform`、`Renderer`、`Animator` 和 `AnimationClip` - Unity `PlayableGraph`、`AnimationClipPlayable`、`AnimationMixerPlayable` 和 `AnimationPlayableOutput` - `mscorlib / System.Array`,用于遍历原演员 Renderer 与 Animator 模块监听登录演员的克隆实例,并只接受原 Prefab 名称以 `SK_actor_` 开头的对象。如果 Host 安装时演员已经创建,则按 250 ms 周期使用 `GameObject.Find` 精确查找男女演员名称;不执行全局对象枚举。`LoginSceneAnimCtrl._target` 可能指向相机或登录场景根节点,因此不得作为演员回退来源。Initial 与 Main 路径哈希均就绪后,模块使用配置中的当前 VFS 路径哈希加载完整角色 Prefab 和四段 Humanoid `AnimationClip`,在原演员同级实例化替换体并同步本地变换、原始缩放与 Layer。 原演员根对象和 Animator 始终保持活动,只关闭 Renderer,并强制 Animator 使用 `AlwaysAnimate`。替换体与原演员的 `Bip001_Pelvis` 在创建后执行一次 XYZ 对齐,此后每帧精确跟随 XZ,并对 Y 使用 0.45 秒低通响应。初始 `sitLoop` 使用 `sitToWalk` Clip 在 `forward_lean_sample` 处以零速度定格;`sitToWalk` 从同一时刻继续,并在 `turn_duration` 内把 `start_yaw` 平滑插值到零。每个阶段动态创建 PlayableGraph,将 AnimationPlayableOutput 绑定替换体 Animator,使用非缩放时间并在 Play 后立即 `Evaluate(0)`。原生循环保持同一图连续播放;启用 `use_crossfade` 时,最终阶段使用双 AnimationClipPlayable 与 AnimationMixerPlayable 在配置区间交叉淡化。 首次 A1 请求发生而 Main 资源尚未就绪时,模块最多暂停该登录控制器 Tick 5 秒;替换体准备完成后调用原始 `_ResetToA1` 统一时间原点。只有 Prefab、四段有效动作、Animator、首个 PlayableGraph、原 Renderer 隐藏和初次锚点准备全部完成后才进入替换状态;任一步失败都会保留或恢复原演员。 模型及动作路径来自当前 `manifests/model/action-manifest.json` 和 UI 写入的配置,不编译角色资源哈希白名单,也不依赖可执行文件身份。 Logo 主题使用以下强类型契约: - `Entry.Beyond.dll / Beyond.Login / LoginDecorateUI.Tick` 与 `OnRelease` - `LoginDecorateUI._imgLogo` 与 `_targetGlow` - `Entry.Beyond.dll / Beyond.Login / LoginEnterGamePanel.OnValueChanged` - Unity `GameObject.Find("GameLogoRaw")` - Unity UI `Graphic.get_color/set_color` 与 `GetComponentsInChildren` - 可选:`Image.set_sprite`、`RawImage.set_texture`、`Sprite.Create` 与 `Texture2D`/`RenderTexture`/`Graphics.Blit` 读回接口(与登录色带共用,见下) 模块在原 `Tick` 返回后覆盖 `_imgLogo`、`_targetGlow` 两个精确子树以及登录主界面独立 `GameLogoRaw` 的 `Graphic.color` RGB,并保留动画当前 Alpha。`GameLogoRaw` 是世界层级快照中确认的 `UIRawImage + UIMaterialAnimation` 对象,不属于 `LoginDecorateUI` 的两个字段子树;运行时按 500 ms 重试精确名称查找,不枚举全局 UI。`GameLogoRaw` 使用的 `login_logo01`(及 `login_logo_en/jp/kr/tc`)纹理把黄色错位边层和 "ARKNIGHTS ENDFIELD" 字样直接烘焙在纹素里,乘色无法得到任意主题色;因此 Logo 路径对 `Image` 的 Sprite 和 `RawImage` 的 Texture 同样使用下文的"去色副本",再写入 `Graphic.color`。 登录界面的长条色带位于 `EnterGamePanel/MiddlePanel` 的 UGUI 子树。`LoginEnterGamePanel.OnValueChanged` 返回后,模块按面板实例缓存该子树的 `Graphic`(跳过 Text/NonDrawing 与 `GameLogoRaw`),并以精确层级、`login_deco_line*`/`login_deco_glitch*`/`login_cross_deco` Sprite/RawTexture 以及原始黄色 RGB 共同识别主题目标;Sprite 副本检测为带色的非目标 `Image` 也会被提升为目标。这会覆盖入场特效、最终静态 `LineLeft`、两侧 `LineDecoLeft`/`LineDecoRight` 小区块和四角标记。 这些素材的黄色同样烘焙在纹素中,所以主题处理分两层:先为每个目标的当前 Sprite/纹理生成一份"去色副本"——用 `Graphics.Blit` 把(可能压缩、不可读的)源纹理复制到临时 `RenderTexture`,`Texture2D.ReadPixels` 读回 Sprite 矩形,把饱和纹素的 RGB 归一为峰值通道(Alpha 不变),`Sprite.Create` 按原 pivot/ppu/border 生成新 Sprite 并 `Image.set_sprite`(`RawImage` 则整张纹理替换 `texture`,UV 不变);带色可见纹素少于 15% 的素材保留原件。副本按原始 Sprite/纹理缓存,游戏切换 Sprite 时按新原件重新生成。随后复制当前 `Material`,在副本的 `_Color`、`_TintColor`、`_BaseColor` 或 `_GlowColor` 属性上写入主题色,只赋给当前 `Graphic`;RectTransform、Shader 和层级保持不变,不创建白色 UI 四边形,也不修改共享材质或原始资源。`Graphic.color`、`CanvasRenderer` 颜色和祖先 `CanvasGroup.color`(HG 扩展)的 RGB 归一为白色并保留游戏当前 Alpha。模块在 `LoginDecorateUI.Tick`、`UIMaterialAnimation.LateTick` 返回后以及 `CanvasUpdateRegistry.PerformUpdate` 之前重新确认 Sprite、材质与颜色。重复的 `OnValueChanged` 不会重新枚举同一面板;热停用或界面释放时恢复原 Sprite/纹理、原材质与原 RGB,并销毁运行时的材质副本、Sprite 副本和纹理副本。 ## 语音模块 语音模块动态验证并挂接: - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / VoicePlayer.PlayVoice` - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / VoicePlayer._PlayVoice(ref VoiceContext)` - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / VoiceSpeakChannelProcessor._PlayVoice` - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / VoicePlayer._PlayEvent` - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / VoiceManager._SpeakNarrative` - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / VoiceUtils.TryGetVoiceDuration` - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / VoiceI18n.GetCurrentLanguage` - `AK.Wwise.Unity.API.dll / AkSoundEnginePINVOKE.CSharp_SetMedia` - `AK.Wwise.Unity.API.dll / AkSoundEnginePINVOKE.CSharp_UnsetMedia` 可选契约还包括 `VoiceUtils._GetVoDurationFromVoData`(时长叶子)、`Beyond.Cfg.VoiceData` 的 `get_speakerChannel`/`get_path`/`get_wavDuration*` getter(只调用、不挂接)、 `Beyond.Gameplay.Core.DialogManager._PlayLipSyncTrack`、 `Beyond.Gameplay.View.LipSync.LipSyncUtils.TryLoadTrack` 及对话动作的角色标识 getter。 剧情请求在 `VoicePlayer.PlayVoice` 调用作用域内保存已识别的角色和目标语言,外部语音提交按 “请求作用域、外部源路径、Wwise Event”顺序匹配规则。剧情文件路径不包含角色名时,可由 `vo_narrating_<角色>_*` Event 或上游 `speakerChannel` 继续完成路由;作用域支持嵌套并在原调用 返回后恢复。 时长修正只在 `_GetVoDurationFromVoData(voId, ref VoiceData)` 返回后改写返回值:两个 `TryGetVoiceDuration` 重载以及 IL2CPP 为 xLua 委托生成的 `Invoke` 快路径副本(其中内联了 `TryGetVoiceDuration(String)` 的整段函数体,因此挂在该入口上的 Hook 永远收不到 Lua 调用) 都会 out-of-line 调用这个叶子。模块按 `VoiceData.speakerChannel`(其次按 `path` 中的对白 ID token)选规则,目标语言与当前语言不同时优先读取该行的目标语言 `wavDuration*` 列,列为空再查 Catalog 时长表;`narrating/` 路径受剧情语音开关约束。两个 `TryGetVoiceDuration` 入口仅保留日志。 口型链路是 `DialogManager._PlayLipSyncTrack` / `DialogTimelineManager.PlayLipSync` → `DialogUtils.EntityPlayLipSyncByVoiceId` → `LipSyncUtils.TryLoadTrack(lineId)` → `GetLipSyncTrackPath` → `VoiceI18n.GetCurrentLanguage`,同步且无缓存。`TryLoadTrack` 是唯一入口, 路由来源按优先级为:剧情 WEM 成功重定向后按对白 ID 挂起的目标语言(只有收到完全相同的对白 ID 才消费,配置代数变化会使其失效)、`_PlayLipSyncTrack` 的角色作用域、以及 `lineId` 自身(它就是 voiceId,按角色 token 选规则;Timeline 对白不经过 `_PlayLipSyncTrack`,依赖这一项)。目标 Track 不可用时重试游戏原语言。口型通过线程局部语言覆盖生效,不修改 Wwise 全局语言;带语言参数的 `GetLipSyncTrackPath(AudioLang, …)` 重载在当前客户端没有调用方(被内联),不再挂接。 Wwise Media 只使用本机生成的 `BEVCAT01` Catalog。模块启动或配音规则热更新时会先读取全部已配置角色的 Catalog,按 Media ID 合并并常驻内存;为避免 Host 初始化时 Wwise 尚未就绪,`SetMedia` 会延迟到第一条命中已配置角色的语音请求时一次性注册所有合并路由。未配置角色发声不会卸载现有路由。通配规则先合并,角色专属规则覆盖同一角色的通配路由;两个专属规则若对同一源 Media 给出不同目标则拒绝激活。Catalog 中的 WEM 在 `SetMedia` 成功前保持驻留,清理状态确认后才释放。 ## 外部 Catalog `scripts/BuildVoiceCatalog.py` 从当前 VFS 的 PCK 和仓库资源清单生成角色级 Catalog。格式为: - `VoiceCatalogHeaderV1`:格式版本、目标语言、条目数和数据区位置。 - `VoiceCatalogEntryV1`:源 Media ID、目标 Media ID、驻留数据偏移和长度。 Catalog 不包含 `GameAssembly.dll` 身份条件。B 服只要提供可解析的 PCK/BNK/HIRC 和 `AudioDialog`,即可重新生成自己的 Catalog。 ## 战斗数据模块 `BetterEndfieldNext.CombatStats.dll` 是第四个独立模块,默认关闭。它动态解析: - `Gameplay.Beyond.dll / Beyond.Gameplay.Core.BattleManager.BattleRecorder.RecordDamage` - `UI.Gameplay.Beyond.dll / Beyond.UI.DamageTextCtrl._OnHpChanged` - `UI.Gameplay.Beyond.dll / Beyond.UI.DamageTextCtrlV2._OnHpChanged` - `Gameplay.Beyond.dll / Beyond.Gameplay.Core.BattleManager.Tick` `RecordDamage(ref AbilitySystem.Modifier)` 是普通地图和关卡战斗均经过的结算后路径。模块从未装箱的 `Modifier` 中读取 `value`、`realDelta`、攻击者、技能、伤害类型、装饰掩码和暴击标志。IL2CPP 元数据中的值类型字段偏移包含 16 字节装箱头,读取 `ref Modifier` 时必须扣除该头;类对象字段不扣除。 事件先进入有界队列,再由模块线程汇总; 队列满时只丢弃统计事件,不影响游戏线程。F11(可配置)切换开始和结束会话,结果写入 `%LocalAppData%\\BetterEndfieldNext\\combat-sessions\\combat-*.json`。 `BetterEndfieldNext.CombatOverlay.exe` 是随 CombatStats 模块分发的独立 Win32/GDI+ 伴随进程。 模块不在 Unity/IL2CPP 渲染线程中创建窗口,而是把最多 16 个角色的累计值和六种技能分类写入 `Local\\BetterEndfieldNext.CombatStats.` 共享内存;悬浮窗以 sequence 奇偶校验读取一致快照。 它显示嵌入 EXE 的本地角色头像、每 10 倍切换一级中文单位的数值和按普攻、战技、终结技、连携技、被动、其他堆叠的横向柱状图,并提供对应颜色图例。 F12(可配置)只切换共享状态,不注入输入处理;悬浮窗相对游戏客户区定位,Ctrl+鼠标左键拖动后的 偏移保存在 `%LocalAppData%\\BetterEndfieldNext\\combat-overlay.ini`。游戏退出或模块卸载时伴随进程自动退出。 头像来自 CEP 终末地规划器角色图鉴的构建时快照,来源记录在 `native/modules/combat_stats/assets/SOURCE.md`;正式运行时不访问该站点。 会话格式 schema 11 写入 `battle`、`dictionary`、完整队伍快照、`actions`、`effects` 和实时摘要,并分离原始实体与经验证的角色/装备来源。角色和状态的 64 位实例 ID 以十进制字符串保存。 `actions` 记录普攻、战技、终结技、连携技、闪避等具体操作的开始与结束;`effects` 记录伤害、Buff、 Debuff 和失衡状态的施加、刷新与移除。逐角色/技能聚合、0.25 秒时间桶、状态区间和 rDPS 归因均在读取时 从这些原子记录派生,磁盘中不再重复保存派生时间轴,也没有可关闭逐击结果的配置项。 技能分类由原生 `originSkillId` 判定:`_attack*`、`_normal_skill`、`_ultimate_skill`、`_combo*`、 `_passive_skill` 分别归入普攻、战技、终结技、连携技和被动,其余归入“其他”。 WinUI 历史页读取 schema 11 后构建相同的排行和时间轴视图;更早的开发格式不再兼容。 隐藏伤害数字只在开关开启时跳过最终 UI 控制器的 `_OnHpChanged`,同时兼容旧版与 V2 `DamageTextCtrl`;不触碰 `DamagePackData`、`Modifier.Apply`、BattleRecorder、生命值或韧性处理。 若任一契约在 B 服缺失,模块只禁用对应能力,其他三个模块继续运行。 ## 音乐模块 音乐模块动态验证并挂接: - `Gameplay.Beyond.dll / Beyond.Gameplay.Audio / AudioMusicSystem.PostMusicEvent` - `AudioMusicSystem._StartMusicWithEvent`、`_StopMusicByPlayingId` - `AudioMusicSystem.PauseMusic`、`ResumeMusic`、`StopMusic` - `AudioMusicSystem.OnTimelinePause`、`OnTimelineResume` - `UnityEngine.CoreModule.dll / UnityEngine / UnitySynchronizationContext.Exec` - `AK.Wwise.Unity.API.dll / AkAudioInputManager.PostAudioInputEvent` - `AkAudioInputManager.InternalAudioFormatDelegate`、`InternalAudioSamplesDelegate` - `AkSoundEngine.LoadBankMemoryCopy`、`RegisterGameObj`、`ExecuteActionOnPlayingID` `UnitySynchronizationContext.Exec` 只作为 Unity 主线程命令泵。共享内存连接、心跳、读取、声道转换和重采样全部在模块自己的工作线程执行;Wwise 实时回调只读取预分配的本地 SPSC 缓冲,不分配、不加锁、不记录日志,也不调用 OmniPcmShared。 OmniPcmShared 在加载后先通过 `OmniPcm_GetAbiVersion` 和 `OmniPcm_GetAbiInfo` 验证 ABI 主版本 `2`、共享协议 `2` 与交错 `float32`。实例使用 `OmniPcm_OpenInstanceUtf8`,流状态只读取带大小标记的 `OmniPcm_GetSnapshotV2`;该检查失败时不会连接共享内存,也不会暂停原生 Playing ID。 模块内嵌 188 字节、四 HIRC 对象的 Audio Input Bank,不发布独立 `.bnk`。正式版继续使用已经在当前客户端验证的 `PostAudioInputEvent(..., null, null)` 路径,并只为自己的 Playing ID 截获 Internal 回调;其他 Playing ID 无条件转发给游戏原实现。构造纯 C++ IL2CPP 托管 Delegate 不属于当前 ABI。 原游戏登录、主界面和游戏内 Playing ID 通过字段元数据动态取得。自定义 Event 路由到同一 Music Bus,因此模块只对选中范围的原生 Playing ID 执行 Pause/Resume,不静音总线。后端、流、预缓冲、格式回调和首个采样回调全部健康后才暂停原音乐;故障时按配置恢复。Audio Input 初始化失败按 2 秒退避重试;可闻游标在本地环形缓冲之外再扣除 100 ms 输出余量。 Audio Input Event 是进程期持久 Source。OmniMix 暂停、停止、流切换或故障时,模块通过 原子 PCM 门控让回调补零且不消费缓冲,不对持久 Event 执行 Pause/Resume;真正的 Source 故障使用最近一次 Wwise 样本回调时间判定。实例声明 `QueueManagement`、`Seek` 能力,`stream_id`、格式 generation 或 Seek generation 变化都会先清空旧流再绑定新流。策略只在状态边沿记录 `[music.policy]`,可区分正常接管、场景离开、传输失效和后端会话丢失。 图形后端不属于音乐模块接口。2026-08-17 的联调中曾偶发一次呈现线程崩溃,但后续测试 无法稳定复现,也没有证据表明它由音乐模块、OmniMix 或特定图形后端触发。因此音乐模块 不检测或修改渲染 API;UI 的自定义启动参数仅作为通用游戏启动能力保留。 ## 加载适配器 `BetterEndfieldNext.Injector.exe` 启动目标程序,并通过普通映像 Bootstrap 在启动阶段加载 Host;Host 根据自身 DLL 路径找到软件根目录。注入器支持在 `--` 后接收并转发游戏参数。 `payloads/xinput1_4.dll` 是唯一会部署到游戏目录的代理。它用 PE forwarder 转发 Windows XInput 1.4 API,仅从 `%LocalAppData%\BetterEndfieldNext\BetterEndfieldNext.ini` 的 `[Loader] install_root` 定位 Host,不包含旧代理配置回退。 ### XInput 代理时序 `xinput1_4.dll` 的 `DllMain` 只禁用线程通知并创建 Worker,不在 Loader Lock 中加载 Host 或模块。Worker 使用以下时序: 1. 写入代理加载标记和状态文件,便于诊断 Windows 是否选择了本地 XInput DLL。 2. 读取 `[Loader] load_host` 和 `install_root`,验证软件目录中的 Host 存在。 3. 等待 `GameAssembly.dll` 出现,并取得 `il2cpp_domain_get`、`il2cpp_thread_attach` 和 `il2cpp_thread_detach`。 4. 等待 Domain 就绪,在 Worker 线程附加 IL2CPP 后加载 `runtime\BetterEndfieldNext.Host.dll`,随后解除附加。 ## 界面增强与移动端 UI 模块 `BetterEndfieldNext.UiModule.dll` 提供 PC 客户端下切换移动端/触控 UI 与隐藏 UID 水印的实验性支持。两个功能默认关闭、相互独立;管理器中的开关会立即保存并由 Host 热重载。移动端相关接口与逆向成果详见专题文档:[docs/ui/MOBILE_UI_REVERSING.md](../ui/MOBILE_UI_REVERSING.md)。 模块通过 Hook 拦截以下三层运行时契约: 1. **输入状态**:`Beyond.DeviceInfo.get_inputType` (Touch=1)、`usingTouch`、`usingKeyboard`、`usingController`、`ChangeInputType`。 2. **平台判定**:`Beyond.DeviceInfo.get_isMobile`、`get_isAndroid`、`get_isPC`、`get_isPCorConsole`、`get_platform`、`UnityEngine.Application.get_isMobilePlatform`、`get_platform`。 3. **云游戏判定**:`UnityEngine.Application.get_isCloudGame`、`Beyond.CloudGameUtility.IsCloudGame`、`Beyond.CloudGame.get_enabled`、`get_isMobilePlatform`。 ### UID 水印隐藏 模块不使用依赖分辨率、渲染后端和固定屏幕坐标的替换 Shader。它通过 `UnityEngine.GameObject.Find` 和 `GameObject.SetActive` 定位当前资源清单中的 `UIDPanelPanel`、`WaterMarkGridPanel`、`WaterMarkCell`、`BottomNodeWatermarkUI` 根对象;运行时新激活的同名对象由 `SetActive` Hook 捕获。模块只记录并恢复由自己关闭的对象,避免关闭功能时误启用游戏原本隐藏的其他界面。 ### 全部 HUD 隐藏 开启 `hide_hud_enabled` 后,通过 `hide_hud_hotkey` 指定的热键(默认主键盘数字 0)隐藏或恢复游戏内全部 UI。当前实现复用 `UIManager.OnToggleUiAction` 中纯显示侧的原生接口:通过 `Beyond.Gameplay.View.CameraUtils.get_cameraManager()` 获取相机管理器,再以独立键 `BetterEndfieldNext.HideHUD` 调用 `CameraManager.AddUICamCullingMaskConfig(string, int) -> bool`,遮罩值为 `UIConst.LAYERS.Nothing` 对应的 `0`;恢复时调用 `CameraManager.RemoveUICamCullingMaskConfig(string) -> bool`,只移除模块自己的配置。两个返回值表示配置集合是否发生增删,重复添加或移除不存在的键会返回 `false`,不代表最终遮罩状态失败。 模块不广播 `ON_TOGGLE_UI_ACTION`、`CLEAR_SCREEN_ON/OFF`,也不覆盖 `CameraControllerBase.hideHUD`,因此不会进入 `UIManager._ToggleUIInputBinding` 的输入组冻结分支。相机管理器暂不可用时才回退扫描 `MainHudRoot` 的 Canvas/Graphic,并每两秒重试原生遮罩,以覆盖地图切换或相机管理器重建。Windows 当前客户端已实测确认:数字 0 可隐藏及恢复全部 UI,隐藏期间角色移动和原生鼠标视角保持可用。所有 Unity/IL2CPP 调用均在游戏主线程执行。 ## 相机增强模块 `BetterEndfieldNext.Camera.dll` 独立负责自由视角和镜头近距离反虚化,读取 `[betterendfieldnext.camera]`。模块以 `Beyond.Gameplay.View.CameraMono._ProcessDitherByPitch` 作为游戏主线程上的相机更新泵;所有 Unity 对象读取和写入均发生在该线程,不从 Host 配置轮询线程调用 Unity API。 ### 自由视角 开启 `free_camera_enabled` 后,通过 `toggle_hotkey` 指定的热键(默认主键盘数字 9)在游戏内进入或退出自由视角;通过 `pause_enabled` 和 `pause_hotkey`(默认主键盘数字 8)独立控制世界时间冻结。两个热键均由独立输入轮询捕获,但 Unity 对象操作仍排队到游戏主线程。模块通过 `UnityEngine.Camera.get_main` 获取当前主相机,保存其位置和 FOV,随后在每帧原相机逻辑执行后只覆盖位置。冻结请求和退出请求通过 `Time.get_unscaledDeltaTime` 的主线程心跳处理,避免游戏时间缩放为 0 后相机逻辑停止导致无法退出。退出自由视角时会恢复本模块捕获的时间缩放。方向键前后左右移动、PageUp/PageDown 升降;镜头旋转继续使用游戏原生鼠标控制。主相机实例变化时自动退出。 `pause_enabled` 默认关闭,可按需在自由视角期间启用独立的 `pause_hotkey` 冻结或恢复角色与世界;移动速度和 FOV 有范围校验,并支持配置热更新。 ### 角色近距离反虚化 模块拦截 `Beyond.Gameplay.View.CameraMono._ProcessDitherByPitch`,保留原始相机处理后按开关调用同类的 `ForceClearDither`。这条路径直接复用游戏自身的清理逻辑,不修改材质、Shader 或渲染管线。 ## 失败规则 动态方法、字段、Hook 目标或 Catalog 校验失败时,模块进入 `contract-mismatch` 或 `failed` 状态并记录原因。Host 不尝试其他地址、过期配置、过期资源映射或未知代理链。