Godot 节点后端与 TL2 定制 Ogre 可见性

后续:静态模型资源后端与真实材质上下文已接入真实静态网格/材质和对应碰撞资源数据;世界注册仍待实现。下文保留本批历史状态。

本批结果

新增 NativeGodotSceneBackend,首次将此前的 NativeSceneAttachment 控制器接到实际 Godot Node3D 的创建、父子挂接、变换、可见性和释放操作。

  • Godot 4.7-stable 进程内 21 项集成检查通过。
  • 在该后端内重放 4,848 组原版父姿态参考,三个 x87 精度的派生姿态均逐位一致。
  • Godot 保存的显示矩阵/位置与核心输出逐位一致,包括非单位四元数、负缩放和零缩放。
  • TL2 自带 Ogre 的节点可见性与换父可见性部分:6,144 组原生参考的状态和回调顺序一致。
  • 累计 1,362 条核心断言、198 项相关 Python 检查通过;Godot C# 构建零警告、零错误;coresimlint 通过。

本批是实际世界宿主的节点层,不是完整地图宿主。尚未接入正式地图模型/材质构造、实际碰撞注册、世界分区、全部对象描述符和攻击型 proc。当前测试未加载主场景,没有进行像素对拍。

为什么不能直接交给 Godot 父子变换

Godot Node3D 默认继承父变换,可通过 top_level 关闭;强制更新变换要求节点已经进入 SceneTree。实现依照这些引擎接口约束处理。Godot Node3D 文档

后端保留原版局部/派生姿态作为权威数据:

  1. 父子姿态用已验证的 NativeRayMath.DeriveNodePose 计算。
  2. 显示的线性矩阵用原版四元数矩阵乘缩放矩阵的运算。
  3. 按行存储的原矩阵转换为 Godot Basis 的列。
  4. 每个 Godot 变换载体设置 TopLevel,避免引擎再次乘父变换或进行另一套非均匀缩放分解。
  5. 节点离树时写本地 Transform,入树后才调用 ForceUpdateTransform。

PoseLinearMatrix 从已有 RoomTransform 的正向矩阵部分提取,原 4,908 组 Room 射线/矩阵对照仍通过。显示不需要逆矩阵,因此允许零缩放;碰撞逆矩阵仍保留原来的奇异输入诊断,不将缩放偷偷改成很小的非零值。

集成测试故意给 Godot 挂载节点另设位置与缩放,验证它们不会被重复应用到原版世界姿态上。

TL2 的 Ogre 可见性并非标准 API 摘要

标准 Ogre 文档将 setVisible 描述为修改挂载对象,并可选择影响子节点;这不足以代表 TL2 的定制实现。Ogre SceneNode API

原游戏 OgreMain.dll 的 SceneNode 另外保存:

字段用途
byte+0x198本地可见性意图
byte+0x199派生/传播可见性
MovableObject byte+0x2D挂载对象自身可见性

SceneNode::isVisible(10031E70)要求两个节点字节都非零。_findVisibleObjects(10193680)也在入口检查它们;失败会跳过整个子树。因此不能只切换挂载模型,忽略节点层的遍历门。

本批开发中曾按通用 Ogre 摘要理解为没有持续的节点可见性位。检查原 DLL 后已推翻该假设,以游戏自带实现重做,没有将通用文档当作等价证明。

setVisible 的真实顺序

101937F0:

  1. 将请求值写入本地字节 +0x198。
  2. 查询父节点;存在时会再次查询父节点。
  3. 如果父节点的 +0x199 为零,保留当前 +0x199 的旧值,并禁止本次向子节点传播。
  4. 否则将当前 +0x199 写成请求值的非零归一化结果。
  5. 无论父节点是否隐藏,都把请求值传给当前挂载对象。
  6. 只有 cascade 为真且第 3 步没有禁止传播时,才调用子节点 setPropagatedVisible。

隐藏父级下保留旧派生值看起来反直觉,但它是原版机器码行为,不主动“纠正”为零。

setPropagatedVisible(10193960):

  • 当前派生值 = 传入父可见值非零 AND 当前本地值非零。
  • cascade 为真时继续传播这个派生值。
  • 不修改子节点本地意图,也不改挂载对象自身可见性。

因此“父级隐藏再显示”不能让明确隐藏的子对象恢复显示;也不能因为父级隐藏就抹掉子对象自己的可见标记。

换父与脱离

10192D50 的可见性部分:

  • 基础 parent 写入先发生。
  • 挂到父节点时,先通知场景归属,再按父节点派生可见性传播。
  • 脱离父节点时,以当前本地和派生可见性的合取继续传播,随后通知离开场景。
  • 已经被派生隐藏的节点在 detach 后不会自动变可见。

NativeNodeVisibility.ParentChanged 只实现这里的可见性部分;实际场景树进入/离开由 Godot 后端执行。它不冒充完整原 Node::setParent 的所有副作用。

Godot 的具体节点结构

text
NativeNode_父(变换载体,TopLevel;可见性表示节点遍历门)
 ├─ Content
 │   └─ 本节点挂载对象(自身 Visible 单独保存)
 └─ NativeNode_子
     ├─ Content
     │   └─ 子节点挂载对象
     └─ ……

本地/派生可见性控制载体;对象自身可见性控制挂载对象。Godot 的父级隐藏提供子树遍历效果,而原算法决定什么时候改派生字节、什么时候改自身对象字节。

后端保留明确的节点 handle、父节点、局部姿态、子列表和挂载对象身份。handle 不与原版 64 位运行时/源 ID 混用。运行时对象查找由宿主提供;集成探针使用真实 NativeFactoryRegistry,验证 long.MaxValue/long.MinValue 附近 ID 不截断。

注册尚未绑定的挂载对象不转移所有权;绑定后由后端节点拥有。清理时先逐个拆开变换载体,再释放各自内容,避免释放父节点时连带释放仍被表记录的子载体。重复 Dispose 不重复释放。

引擎内验证及发现

独立 SceneTree 探针不启动主场景,不打开存档。

21 项检查包括:

  • 无效根姿态在创建 Godot 节点之前拒绝。
  • Source Parent ID 设置与 TransformParent 挂接分开。
  • 实际 Godot 父子节点与原版控制器输出一致。
  • 非 cascade 的节点隐藏保留子节点本地状态和子挂载对象标记,但正确停止子树显示。
  • 父级重显不覆盖子级显式隐藏。
  • detach 保留派生隐藏,重新挂到可见父节点后恢复。
  • 有 TransformParent 的隐藏对象仍留在树上。
  • 新挂载对象保持自身初始 Visible,但仍受已隐藏节点的遍历门约束。
  • 零缩放不钳制。
  • 拒绝环形挂接。
  • 释放后没有仍存活的自有载体/挂载对象,未绑定对象仍归调用方。

一次探针失败来自测试直接把底层节点挂回 owner,却没有同步 NativeSceneAttachment 的 TransformParent/AttachmentNode。后续 show 按原控制器又挂回了隐藏 group。已改成通过 SetTransformParent 进行重挂接,未用强制显示掩盖状态不一致。

4,848 组姿态参考来自 build/native_node_pose_oracle/results.json,全量三精度重放在 Godot 进程内执行,不是又写一个预期公式替代原版结果。

原生参考范围与产物

OgreMain.dll SHA256: 974c1dc77ea2818276cc7eb94a8b95b2ccf8595e28fa3f14b2cd6d516fdb67a1

原生参考执行 101937F0、10193960、10192D50 与 MovableObject setter。容器存储、父 getter、基础 parent 写入和场景归属虚调用是明示边界;结果包含节点/对象字节和回调顺序。

文件:

  • core_sim/NativeNodeVisibility.cs。
  • core_sim/NativeRoomTransform.cs 的 PoseLinearMatrix。
  • torchlight-2-poc/NativeGodotSceneBackend.cs。
  • torchlight-2-poc/NativeSceneBackendProbe.cs、tl2_native_scene_backend_probe.gd。
  • tools/build_native_node_visibility_oracle.py、native_node_visibility_oracle.c。
  • tools/run_native_scene_backend_probe.py。
  • core_sim.tests/NativeNodeVisibilityChecks.cs。
  • tests/test_native_node_visibility.py。

日志与证据:

  • build/native_node_visibility_oracle/results.json:6,144 组原版参考。
  • build/native_godot_scene/result.json:21 检查、4,848 姿态,含实际引擎版本、源文件指纹和存档前后哈希。
  • build/native_godot_scene_core.log:1,362 断言。
  • build/native_godot_scene_python.log:198 项相关 Python 回归。
  • build/native_godot_scene_build.log:构建通过。

剩余

接下来将真实 Room 视觉资源/材质与碰撞后端接到本节点层,再整合完整装载宿主、世界注册/分区、实际任务与关卡上下文,继续攻击型 proc 和全武器验收。

本批仅测试 Node3D 挂载对象及节点状态,不是 Mesh/材质或像素级视觉验收,也未替换正式地图。复杂动态脚本回调、完整 Ogre 容器迭代/失效语义和持久世界快照尚不在本后端的完成声明内。

按 godot-master 的分层要求,原版姿态/可见性控制留在核心层,Godot 后端执行引擎操作;没有用 Godot 物理查询替代原版碰撞公式。新节点状态仍属于正在构建的装载宿主;正式世界状态及其快照尚未接入,v29 未改。

武器可构造覆盖仍为 1,053/1,419,全武器目标未完成。main 原地工作,未提交、未 attach 原游戏。默认存档前后 SHA256 相同: 9840fe858b6e311c73f8c2d4ed49912d59d71365f6278392811730e012d32a7f