feat(physics): #101 — add IsPhantomGfxObjSource predicate

Retail's CPartArray::InitParts emits collision shapes only from
Setup-level CylSpheres/Spheres or per-Part PhysicsBSP — never
from visual mesh AABBs. The predicate captures the retail rule:
a stab whose source is a GfxObj (high byte 0x01) with no cached
GfxObjPhysics is phantom (no collision). Wired into GameWindow's
mesh-aabb-fallback synthesis in the next commit.

Refs docs/research/2026-05-25-a6-stairs-cyl-retail-investigation.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-25 19:45:18 +02:00
parent 8795655250
commit f6305b1e3c
2 changed files with 93 additions and 0 deletions

View file

@ -359,6 +359,25 @@ public sealed class PhysicsDataCache
}
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
/// <summary>
/// Issue #101 (2026-05-25): retail-faithful phantom check for
/// GfxObj-only entity sources. Returns true when the entity's
/// <c>SourceGfxObjOrSetupId</c> is a GfxObj (high byte
/// <c>0x01</c>) AND has no cached <see cref="GfxObjPhysics"/> —
/// meaning the underlying GfxObj had <c>HasPhysics=False</c> or
/// a null <c>PhysicsBSP.Root</c>, so <see cref="CacheGfxObj"/>
/// short-circuited at the early-return on line 45/46. Retail's
/// <c>CPartArray::InitParts</c> emits NO collision shapes for
/// these — acdream's <c>mesh-aabb-fallback</c> synthesis at
/// <c>GameWindow.cs:6116</c> must do the same.
/// </summary>
public bool IsPhantomGfxObjSource(uint sourceId)
{
if ((sourceId & 0xFF000000u) != 0x01000000u) return false;
return GetGfxObj(sourceId)?.BSP?.Root is null;
}
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
public int GfxObjCount => _gfxObj.Count;