fix(physics): Slice 1 — delete render-mesh-AABB synthetic collision; DAT-only shape authority
Retail oracle: CPartArray::InitParts@0x00517F40, CGfxObj::Serialize@0x00534970 (physics_bsp gated on serialized-flags bit-0), CPhysicsPart::find_obj_collisions@0x0050D8D0 (returns OK / passable when physics_bsp==null). Render-mesh bounds never enter collision. Changes: - GameWindow.cs: delete the ~200-line VISUAL mesh-bounds collision block (the isPhantomSetup / isPhantomGfxObj locals + the if-block computing worldMin/worldMax AABB + the ShadowObjects.Register call that capped and registered the synthetic cylinder). Also removes dead counter variables scHaveBounds/scRegistered/scNoBounds/ scTooThin; trims the ProbeBuildingEnabled summary line accordingly. - PhysicsDataCache.cs: delete IsPhantomGfxObjSource (the predicate that only existed to fence the mesh-AABB synthesis; the "phantom" concept is now the default — no DAT shape means no registration, verbatim with retail). - PhysicsDataCachePhantomSourceTests.cs: deleted (tested the removed method). - ShadowShapeBuilderShapeSourceTests.cs: new guard test — a Setup with parts but hasPhysicsBsp=false and no CylSpheres/Spheres yields an empty shape list, locking the DAT-only rule in the builder. - retail-divergence-register.md: AP-2 row deleted (divergence retired). Objects with no DAT physics shape (no CylSpheres, no Spheres, no part with a PhysicsBSP) now register no collision shape and are passable, verbatim with retail. Objects with real DAT shapes (BSP parts, CylSpheres) are unaffected. dotnet build green, 22/22 tests passing (ShadowShapeBuilderShapeSourceTests + CellarUpTrajectoryReplay + CornerFlood + Issue147ArwicBuildings replay harnesses). Visual gate pending: walk Holtburg + open world; objects that become passable must match retail (DAT has no physics shape — trees with real CylSpheres still solid). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4f26067755
commit
79dee342f2
5 changed files with 44 additions and 297 deletions
|
|
@ -1,74 +0,0 @@
|
|||
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:6127</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));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Guards the DAT-only shape-source rule (Slice 1 of the unified
|
||||
/// collision-inclusion phase, 2026-06-24).
|
||||
///
|
||||
/// <para>
|
||||
/// Retail oracle: <c>CPartArray::InitParts@0x00517F40</c> (parts come from
|
||||
/// the Setup list only), <c>CGfxObj::Serialize@0x00534970</c> (physics_bsp
|
||||
/// gated on serialized-flags bit-0), <c>CPhysicsPart::find_obj_collisions@0x0050D8D0</c>
|
||||
/// (returns OK / passable when <c>physics_bsp == null</c>). Render-mesh bounds
|
||||
/// never enter collision.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class ShadowShapeBuilderShapeSourceTests
|
||||
{
|
||||
// Retail: an object with no DAT physics shape (no CylSphere, no Sphere,
|
||||
// no part with a PhysicsBSP) emits NO collision shape and is passable.
|
||||
// CPhysicsPart::find_obj_collisions@0x0050D8D0 returns OK when physics_bsp==null.
|
||||
[Fact]
|
||||
public void Setup_WithNoCylSpheres_NoSpheres_NoPhysicsBspParts_YieldsEmptyShapeList()
|
||||
{
|
||||
var setup = new Setup
|
||||
{
|
||||
CylSpheres = new List<CylSphere>(),
|
||||
Spheres = new List<Sphere>(),
|
||||
Parts = { 0x01000ABCu },
|
||||
PlacementFrames = new Dictionary<Placement, AnimationFrame>(),
|
||||
};
|
||||
var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f, hasPhysicsBsp: _ => false);
|
||||
Assert.Empty(shapes);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue