原版 Group 子物件缓存、后处理与偏移
本批结果
core_sim/NativeGroupRuntime.cs 实现 Group 的子引用缓存、状态传播、子 Room 碰撞切换、可见性覆盖、初始化与场景后处理控制。它使用明确的场景/引用后端,不依赖 Godot 类型。
- 21,504 组发行机器码控制参考与移植状态/调用顺序一致。
- 1,572 组完整 Group 偏移路径及 Ogre 运算,在 x87 PC24/PC53/PC64 下逐位一致。
- 累计 1258 条核心断言、159 项相关 Python 回归通过。
- Godot C# 构建零警告、零错误;coresimlint 通过。
- 组合测试接入 NativeFactoryRegistry 的实际 ID 有序表,并通过 NativeSceneAttachment 完成 Room 节点挂接。
本批不替换正式地图。Group 选择/接受、完整 BINLAYOUT 装载宿主、实际视觉与世界分区后端仍未全部接入;全武器目标未完成,构造覆盖仍保持 1053/1419。
Group 的独立状态字段
| 原字段 | 本批字段 | 含义边界 |
|---|---|---|
| +292..300 | Offset | 布局版本相关的 Group 偏移 |
| +304/+308/+312 | Children | 可失效的子对象引用数组/长度/容量 |
| +320/+324/+328 | ChildSourceIds | 建缓存时记录的 SourceId 数组 |
| byte +368 | Enabled | 本组变换/传播路径的启用门,不等同于全局单位存活或 Group 选择通过 |
| byte +605 | HasNonPositionableChild | 发现不能转为可定位对象的直接子项;在这些函数中只置位 |
| byte +606 | DisableChildCollision | 场景后处理时请求关闭子 Room 碰撞 |
| byte +607 | RefreshRequested | 可见性更新触发的标记 |
| byte +608 | LastEffectiveVisible | 620940 保存的完整可见性查询结果 |
| byte +609 | CacheReady | 子引用缓存已建立 |
| byte +610 | Initialized | Group 后处理启用状态 |
| byte +611 | LegacyAttached | 原版缓存/旧布局相关标记,构造默认为 true |
自身可见性 byte+105、Owner+76、Node+88 继续来自前批节点关联状态。它们不能与以上字段合并。
子缓存建立:6201E0/620630
两个入口共用以下缓存建立控制,但外层门和遍历操作不同:
- 只有 Initialized、Owner 存在、Enabled 且 CacheReady=false 才尝试建缓存。
- 先通过 61F6F0 清空旧引用及 SourceId 列表。
- 取得 Owner 的运行时 ID 树首项。
- 树为空时返回,CacheReady 仍为 false。以后再次请求会再次尝试清缓存/建立。
- 树非空时置 CacheReady=true。
- 读取 Owner 派生位置、Group 派生位置和 Owner 派生旋转,执行原相对偏移计算。
- 按 ID 树顺序遍历,只取 ParentRuntimeId 等于当前 Group RuntimeId 的对象。
- 可转为 CPositionableObject 的对象保存为弱式可失效引用,并记录 SourceId;其他直接子项设置 byte+605。
- 完成收集后,再按缓存顺序调用有效子对象的虚表 +8,把变换父对象设为 Group。
- 置 byte+611=true。
候选顺序来自 signed Int64 运行时 ID 树,不是源文件预序或 SourceId 排序。 所以需要前批真实 ID 生成与注册表,不能直接按文本子节点数组重建此缓存。
此处还调用 Group 本地位置 getter 和 Offset 的 isZeroLength;后者返回值被丢弃,不控制任何分支。移植保留相关位置读取,不将被丢弃的结果解释为额外变换条件。被忽略运算的浮点异常状态字不在等价性声明内。
原版针对编辑器/旧布局的某段子位置修正,被直调 6A4650 的恒 false 返回挡住;当前发行 EXE 不执行。移植没有凭该死分支额外平移子物件。
缓存清理与失效引用
61F6F0 调用 733010(list,true):
- 按当前数组长度从前往后释放引用。
- 清子数组并释放数组存储。
- 清 SourceId 数组并释放其存储。
- 设置 CacheReady=false。
- 不清 byte+605,也不清 byte+611。
本批参考执行了原 61F6F0/733010 的控制,内存分配、释放和引用注册使用明确 spy。托管列表不模拟原堆地址、容量变化或分配失败。
引用对象的 Target 可以在数组条目仍存在时变为空,因此不能简单把所有缓存元素当作永久有效对象。
6201E0 遍历遇到失效引用时:
- 释放该引用包装。
- 用最后一个数组元素补到当前下标。
- 缩短数组,并立即重新检查当前下标。
- 不同时整理 ChildSourceIds 数组。
例:子引用标签 [1,失效,2,3,4,6] 变为 [1,6,2,3,4],而此前记录的 SourceId 数组仍有 6 项。后续不能无条件将这两个列表按下标 zip 成一对一关系。
状态传播必须按实际类型分派
6201E0 的外层要求 Owner 存在、Initialized、Enabled;通过后才建缓存和遍历。
| 类型检查优先级 | 子对象操作 |
|---|---|
| CBaseUnit | 调用虚表 +60,参数为 !groupEffectiveVisible |
| CRandomGroup | 调用 620960,使用子 Group 自己的完整可见性 |
| CEditorScene | 调用 4D6630(false) |
| CRoomPiece | 调用虚表 +60,参数为 !groupEffectiveVisible |
| 其他可定位对象 | 不执行上述后续传播操作 |
各子对象的 +60 并不等价:
- 基类 464D30 通过 +56 应用参数的反值。
- Group 的 620940 忽略传入参数,查询 624B40,保存到 byte+608,再调用 6201E0。
- Room Piece 的 622DE0 忽略传入参数,只调用 622C20 刷新碰撞注册。
因此不能对所有子对象统一执行 visible=groupVisible。NativeGroupRuntime 保留类型顺序和虚调用边界;单位、嵌套场景和实际碰撞注册的完整后端仍须由具体对象适配器提供。
本批不把 byte+106 当作这个 +60 操作:624B30 写 byte+106 位于 +48 槽,二者不是同一接口。
子 Room 碰撞切换:620630
620630 在满足建缓存条件时先建立缓存,然后遍历现有子缓存。
与 6201E0 不同:即使 Owner/Initialized/Enabled 不满足,已有缓存也仍然会被访问。
- 只处理能转为 Room Piece 的子项。
- 调用 622DC0(enabled,true)。
- 622DC0 写 CollisionEnabled byte+388,然后刷新碰撞注册。
- 跳过失效 Target,但不在这条路径中整理数组。
- 不是释放碰撞资源、删除模型或改变 BAKE 标记。
可见性覆盖与后处理
620960
满足 Enabled、自身 Node、Owner,以及“尚未建缓存或缓存非空”时,取得 624B40 的结果再调用传播入口。
624B40 不仅看 byte+105,还要求节点存在、节点的对应可见性查询及 isPropagateVisible。后端必须提供完整结果,不能只传一个未经验证的 Godot visible 值。
6209B0
只有新旧自身可见性不同时:
- 直接调用基类 624B80 更新自身节点可见性。
- 满足上述刷新条件时传播完整可见性结果。
- 如果 Initialized=true,设置 byte+607。
NativeSceneAttachment 新增可选的虚可见性分派回调,让其 SetTransformParent 的强制虚调用能进入 Group 覆盖;SetAttachmentNode 中原有的直接基类调用仍绕过覆盖。原 17,536 组节点关联/父 ID 参考回归继续通过。
61F1E0 与 4B6EB0
61F1E0 先处理 Offset,再写 Initialized。如果启用:
- 保存 byte+607。
- 经基类将自身可见性取反。
- 再经 Group 虚覆盖取反回去,触发相应缓存/传播。
- 恢复先前的 byte+607。
描述符后处理 4B6EB0 对每个 Group:
- Initialize(true)。
- byte+606 非零时关闭子 Room 碰撞。
- 清 byte+607。
这发生在前批确认的“全部对象 +76 初始化之后、描述符 +80 后处理”阶段。不能拿 Godot 的父子 _ready 顺序代替。
旧/新布局偏移
Owner byte+312 是这里的布局版本相关输入,不是 BINLAYOUT 第一字节的文件版本。
大于 3 时:
difference = f32(groupDerivedPosition - ownerDerivedPosition)
Offset = inverse(Matrix4(ownerDerivedRotation)) * difference不使用父节点缩放;矩阵也不经过额外的 R*S 乘法。
不大于 3 时,原汇编真正在计算的是否更新条件是:
(float(Offset.Y == Offset.X) != Offset.Z)满足时重算 Offset;无论是否重算,随后将 Group 本地位置设为 (0,0,0)。
这不是常见的 XYZ 全零判断。例如 Offset=(2,2,1) 时保持原 Offset,但本地位置仍归零。该行为通过发行机器码执行确认,不是照抄反编译的猜测。
NativeRayMath.GroupRelativeOffset 保留差值 float32 落地、原 Ogre 四元数转矩阵/逆矩阵/乘向量顺序。1,572 组参考覆盖:
- 上述特殊条件的相等/不等、正负零和极小值。
- 256 组固定种子的随机位置、旋转和偏移输入。
- 旧/新两条布局路径。
- x87 PC24、PC53、PC64。
仅声明有限、可逆的受支持输入。GroupRelativeOffset 数学入口对非有限位置/旋转或奇异逆矩阵明确报缺输入;旧 Offset 本身的非有限值及浮点异常状态字未作完整等价性声明。
验证范围与产物
原 EXE SHA256: 186472c3057b38f4cdff4696959a943c396ae6166995f7418997b5ea853e8a5e。
原 OgreMain.dll SHA256: 974c1dc77ea2818276cc7eb94a8b95b2ccf8595e28fa3f14b2cd6d516fdb67a1。
- tools/build_native_group_oracle.py/tools/native_group_oracle.c:隔离 32 位进程,无原游戏 attach。
- 原样执行 Group 控制、树迭代、缓存清理和 Ogre 数学;分配/弱引用、RTTI、实际节点、位置读取、嵌套刷新等边界为明示 spy。
- 生成补丁前逐一检查 ASM 中的直调地址与原 EXE 的 E8 目标相符,保留 ASM 来源哈希。
- build/native_group_oracle/results.json:21,504 组控制、1,572 组偏移参考。
- build/native_group_core.log:1258 断言通过。
- build/native_group_python.log:159 项回归通过。
- build/native_group_godot.log:零警告、零错误。
另外验证了实际 NativeFactoryRegistry → Group 收集 → NativeSceneAttachment 的组合挂接,以及 Group 虚可见性分派。没有运行正式地图视觉验收,也没有把嵌套刷新 spy 当作已完成实际单位/场景/碰撞后端。
对循环传播的畸形 Group 图,移植端报诊断错误以避免挂死;不声称原版存在相同循环保护。
Group 状态、引用及控制器当前均为分阶段装载数据;尚未归属持久世界或快照。正式跨帧接入前必须落实所有权、失效通知及序列化/重建策略。快照 v29 未改。
按 godot-master 的数据/表现分层,本批未修改正式画面,main 原地工作且未提交。默认存档 SHA256 未变: 9840fe858b6e311c73f8c2d4ed49912d59d71365f6278392811730e012d32a7f。
下一批继续 Group 属性/选择与完整 BINLAYOUT 装载宿主,接通真实实例创建、节点/碰撞注册和世界分区,再完成攻击型触发技能。289 个场景候选仍缺完整随机上下文,本批不自动将它们计为已解析。