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:
parent
8795655250
commit
f6305b1e3c
2 changed files with 93 additions and 0 deletions
|
|
@ -359,6 +359,25 @@ public sealed class PhysicsDataCache
|
||||||
}
|
}
|
||||||
|
|
||||||
public GfxObjPhysics? GetGfxObj(uint id) => _gfxObj.TryGetValue(id, out var p) ? p : null;
|
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 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 CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
||||||
public int GfxObjCount => _gfxObj.Count;
|
public int GfxObjCount => _gfxObj.Count;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Issue #101 (2026-05-25) — phantom-stair fix. Retail's
|
||||||
|
/// <c>CPartArray::InitParts</c> emits collision shapes only from
|
||||||
|
/// Setup-level <c>CylSpheres</c>/<c>Spheres</c> or per-Part
|
||||||
|
/// <c>PhysicsBSP</c>. There is NO synthesis from visual mesh AABB.
|
||||||
|
/// Acdream's <c>mesh-aabb-fallback</c> path at
|
||||||
|
/// <c>GameWindow.cs:6116</c> previously fired for ANY entity that
|
||||||
|
/// reached it with <c>entityBsp==0 && entityCyl==0</c>,
|
||||||
|
/// including GfxObj-only stabs whose GfxObj has
|
||||||
|
/// <c>HasPhysics=False</c>. This produced the 10 phantom 0.80 m
|
||||||
|
/// cylinders that block the Holtburg upper-floor staircase (see
|
||||||
|
/// <c>docs/research/2026-05-25-a6-stairs-cyl-retail-investigation.md</c>).
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="PhysicsDataCache.IsPhantomGfxObjSource"/> captures the
|
||||||
|
/// retail rule as a predicate: "the entity's source is a GfxObj
|
||||||
|
/// (high byte 0x01) AND the cache has no <see cref="GfxObjPhysics"/>
|
||||||
|
/// entry for it." When this returns true, the caller suppresses the
|
||||||
|
/// fallback synthesis.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class PhysicsDataCachePhantomSourceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsPhantomGfxObjSource_SetupHighByte_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Setup source (high byte 0x02) is never the GfxObj-phantom case.
|
||||||
|
// The existing isPhantomSetup check at GameWindow.cs:6090 handles
|
||||||
|
// the Setup-side phantom. This predicate is scoped to GfxObj only.
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
Assert.False(cache.IsPhantomGfxObjSource(0x020019FFu)); // door setup
|
||||||
|
Assert.False(cache.IsPhantomGfxObjSource(0x02000266u)); // some setup
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsPhantomGfxObjSource_GfxObjUncached_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// GfxObj source (high byte 0x01) with NO cached GfxObjPhysics
|
||||||
|
// = the phantom case. The stair-step GfxObj 0x0100081A from
|
||||||
|
// issue #101's broken-stairs capture has HasPhysics=False and
|
||||||
|
// does not enter the cache. Acdream should treat it as phantom.
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
Assert.True(cache.IsPhantomGfxObjSource(0x0100081Au));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsPhantomGfxObjSource_GfxObjCached_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// GfxObj source (high byte 0x01) WITH a cached GfxObjPhysics
|
||||||
|
// (i.e. the GfxObj's HasPhysics flag was set and its PhysicsBSP
|
||||||
|
// root is non-null) is NOT phantom. The staircase BSP entity
|
||||||
|
// 0x40B50089 from issue #101's capture, backed by GfxObj
|
||||||
|
// 0x01000C16 with hasPhys=True, is this case.
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var leaf = new PhysicsBSPNode { Type = BSPNodeType.Leaf };
|
||||||
|
var fakePhysics = new GfxObjPhysics
|
||||||
|
{
|
||||||
|
BSP = new PhysicsBSPTree { Root = leaf },
|
||||||
|
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
|
||||||
|
Vertices = new VertexArray(),
|
||||||
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
|
};
|
||||||
|
cache.RegisterGfxObjForTest(0x01000C16u, fakePhysics);
|
||||||
|
Assert.False(cache.IsPhantomGfxObjSource(0x01000C16u));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue