单位主虚表、模型来源与动态包围盒

本批结果

从构造器主虚表写入确认角色、装备和金币的模型 getter;实现 CBaseUnit 包围盒刷新及角色专用水平朝向矩阵,并将缓存接给现有几何查询。

  • 1,440 组三精度原生对照:矩阵、包围盒输出、零回落和 getter 顺序逐位一致。
  • 核心累计 1,426 条断言、252 项相关 Python 回归通过。
  • Godot C# 构建零警告、零错误,coresimlint 通过。
  • 共用八角点运算后,既有 Room bounds 对照及 22 项 Godot GPU/资源回归继续通过。
  • 计算后的单位缓存与明确提供的碰撞几何组合执行实际扫掠通过。

模型 getter 来源已经确认;完整的对象模型装载/释放、局部包围盒初始化和活动位生命周期仍由后续真实世界宿主完成,没有在本批伪装成完整地图接入。

必须使用主虚表

CCharacter 和 CEquipment 都有多重继承附属虚表。IDA 名称不带后缀的那一张分别写入对象 +0x1F4/+0x274,不能拿它当对象起始处的主虚表。

必须从构造器的 mov [esi], vtable 写入取证。本次脚本直接核对原始指令 C7 06 + 地址,再读取该表的方法槽。

主表构造写入+110 显示/偏移模型+11C 碰撞模型+180 bounds 矩阵
CCharacter217275C555553555EE0555EF0519F90
CMonster21765A45B0D4D555EE0555EF0519F90
CPlayer21778045C2F22555EE0555EF0519F90
CEquipment2174864585162585700585720514500
CItemGold21760245A89A65A83505A8360514500

这些类的 +13C/+140 均使用 511E80/511EA0 包围盒 getter。

只看错位的附属表,会把向量 getter、空函数或其他接口函数误当成模型/bounds getter。本次在实现前已从构造器纠正,没有把这些错误映射接入正式场景。

显示模型与碰撞模型不是一个槽

实例+110 返回值+11C 返回值
角色/怪物/玩家对象 +0x220 的显示模型指针对象 +0x624 的碰撞模型指针
装备对象 +0x2E0 的显示模型指针对象 +0x2E8 的碰撞模型指针
金币对象 +0x27C 的显示模型指针5A8360 直接返回 0,无碰撞模型

注意伪代码 this[393] 若 this 为 DWORD 指针,对应字节偏移 0x624;它不是字节偏移 +393 的 COLLIDEABLE。必须按原寻址宽度解释。

所以“显示模型存在”不能补成“碰撞模型也存在”,金币的显示模型尤其不能被自动拿来做原初始对象查询的碰撞代理。

独立的 CCullingBounds

CBaseUnit+0x1B8 保存 CCullingBounds 指针:

  • +8/+20:local minimum/maximum。
  • +32/+44:world minimum/maximum。
  • 5116F0 构造时把 local 同时复制到 world。

511E80/511EA0:

  • 缓存存在,返回 world min/max。
  • 缓存不存在,返回 Ogre Vector3::ZERO 的地址,读取值为 (0,0,0)。

这不是返回空指针,也不是回退到显示 Mesh AABB。NativeUnitBounds.Minimum/Maximum 保留原零向量行为;碰撞模型存在不会自动创建 culling cache。

511F00 刷新时序

  1. 若 +0x1B8 为空,直接返回,不查询位置、模型或矩阵。
  2. 61DCE0(useDerived=true) 获取对象位置。
  3. 读取主表 +110 的偏移模型。
  4. 第一次非空时再读取第二次;第二次非空则读取第三次,并取其 61DCE0(useDerived=false) 保存位置;第二次为空则加 ZERO。
  5. 将该偏移加到对象位置。
  6. 调用主表 +180 得到 bounds 矩阵。
  7. setTrans 用计算后的位置覆盖矩阵平移列。
  8. 重新读取当前 +0x1B8,并对其 local bounds 执行原 511750 八角点变换,更新 world bounds。

与 5125F0 的两次偏移模型读取不同,这里可能读取三次。不能缓存第一次返回值覆盖后续读取。

原生对照还模拟第一次模型 getter 中替换 culling cache,验证最后更新的是当前指针而非函数入口缓存的旧指针。实际模型指针及位置 getter 是明确的参考输入边界;没有宣称各种非法销毁或异步写入都能安全重现。

两种 bounds 矩阵

角色:519F90

从保存的 FORWARD(对象 +0xB0)取 X/Z,先把 Y 覆盖为零:

text
f = Ogre.normalise((forward.x, 0, forward.z))
right = Ogre.crossProduct(f, UNIT_Y)
matrix.columns = (right, UNIT_Y, f)
translation = 0
bottom_row = (0,0,0,1)

归一化使用原 Ogre length 与 double 1e-8 阈值、原 x87 运算和 float32 存储。

  • 不使用保存的 scale。
  • 不直接使用节点四元数。
  • 不复制对象完整缓存矩阵。
  • FORWARD.Y 在参与浮点运算前已被覆盖,所以即使原值是 NaN 也不会影响结果;实现只验证实际使用的 X/Z。
  • 水平朝向为零时保留零 X/Z 轴,包围盒可能坍缩;不凭空补单位矩阵或默认朝向。

装备/金币:514500

复制对象 +0xBC 起的 64 字节,即保存的 4×4 矩阵。511F00 随后覆盖平移列;原来的平移值不能再额外叠加一次。

INativeUnitBoundsSource.GetBoundsMatrix 必须由实际类型选择正确来源。窄相位 5125F0 使用节点派生四元数,是另一条规则;不能因为两者都“涉及旋转”就合并。

八角点运算共享

NativeRayMath.TransformCullingBounds 复用原 511750 顺序:

text
min, max,
(min.x,min.y,max.z),
(min.x,max.y,max.z),
(min.x,max.y,min.z),
(max.x,min.y,min.z),
(max.x,max.y,min.z),
(max.x,min.y,max.z)

每点用原 Ogre Matrix4-vector 运算,之后按严格大小比较扩大 world min/max。没有预先交换负缩放导致的端点,也没有使用 Godot AABB 转换取代。

既有 RoomBounds 改为调用该公共函数;Room 自己的 padding、保存 scale 和派生姿态计算没有改变。旧 Room 的原生对照继续覆盖此次提取共用函数的风险。

接入接口与状态归属

  • NativeUnitCullingBounds:可重建的 local/world 几何缓存。
  • INativeUnitBoundsSource:真实位置、偏移模型、当前缓存及矩阵来源。
  • INativeUnitCollisionSource:补充独立碰撞模型、活动标志及窄相位派生四元数。
  • NativeUnitCollisionBinding:把这些源接入已有 INativeInitialCollisionObject,不替代对象所有权。

缓存是从真实局部 bounds 与变换重建的派生状态,不能当作持久化角色状态的新权威来源。原模型的资源装载/引用计数、指针所属运行时类和局部 bounds 初始化仍由宿主负责。

接线测试使用明确给定的三角形模型,加上本批真实算法算出的 culling cache,执行 InitialObjectSweep。它没有用预设命中结果,但也不是完整角色的模型/动画装载验收。

已定位但未完成的装载部分

角色 55E110 的碰撞模型装载规则已定位:

  • COLLISIONFILE 非空时,拼接 RESOURCEDIRECTORY + "/" + COLLISIONFILE + ".mesh"。
  • 缺省路径为 media/models/collider.mesh。
  • 通过 649520 的碰撞资源缓存获得模型,存入 +0x624。
  • 随后读取 COLLISION_ADD(默认 0.25),计算局部 culling 范围,再调用 511F00。

本批没有完整实现后续局部范围对称化、所有资源缓存/变体/释放或装备的对应装载阶段。不能因 getter 和 world bounds 已实现就声称这些生命周期工作也已完成。

证据与产物

  • core_sim/NativeUnitBounds.cs。
  • core_sim/NativeBounds.cs 的共用八角点提取。
  • core_sim.tests/NativeUnitBoundsChecks.cs。
  • tools/build_native_unit_bounds_oracle.py、native_unit_bounds_oracle.c。
  • tests/test_native_unit_bounds.py。
  • build/native_unit_bounds_oracle/results.json,含构造器主虚表证据。
  • build/native_unit_bounds_mismatches.json(空)。
  • build/native_unit_bounds_core.log、native_unit_bounds_python.log、native_unit_bounds_godot.log、native_unit_bounds_visual.log。

参考执行原 511F00/511750/511E80/511EA0/519F90/514500 和 Ogre 运算;位置与偏移 getter 使用明确输入并记录顺序。UNIT_Y 来自原 DLL 已初始化常量数据,ZERO 来自核实后的 PE loader 零填充区。

EXE SHA256: 186472c3057b38f4cdff4696959a943c396ae6166995f7418997b5ea853e8a5e

OgreMain.dll SHA256: 974c1dc77ea2818276cc7eb94a8b95b2ccf8595e28fa3f14b2cd6d516fdb67a1

继续推进

后续进展:角色碰撞资源请求与局部范围初始化已补路径、真实几何及局部范围计算,并以默认 collider 贯通查询;完整共享模型管理和实例生命周期仍需继续。

下一批应补实际单位碰撞资源装载与局部 culling 范围初始化、活动位生命周期及完整地图世界宿主。主虚表归属、模型槽和 world bounds 刷新算法不再列为未知。

武器可构造覆盖保持 1,053/1,419;正式攻击遮挡、攻击型 proc、动画、缺导出资产及全武器验收仍需继续。部分角色表的 UniverseComplete=false 没有放宽。

按 godot-master 的分层要求,缓存/计算留在核心层,宿主提供准确类型及原资源;不从显示网格猜碰撞。本批在 main,未提交、未 attach 原游戏、未修改正式场景与 UI,快照 v29 未改。默认存档 SHA256 未变: 9840fe858b6e311c73f8c2d4ed49912d59d71365f6278392811730e012d32a7f