RegisterLiveEntityCollision had a premature gate at the top of the method:
if (!hasCyl && !hasSphere && !hasRadius) return;
This fired BEFORE ShadowShapeBuilder.FromSetup ran. The builder emits a BSP shape
for every Part whose GfxObj has a PhysicsBSP, regardless of CylSpheres/Spheres/Radius.
A furniture weenie with only a physics-BSP mesh (candle holder, candelabra, etc.)
has no CylSpheres, no Spheres, and Radius=0 -- so it was always dropped, making it
fully passable (invisible wall that lets the player walk through it).
Fix: remove the premature gate. The three `bool` locals (hasCyl, hasSphere, hasRadius)
are retained -- `hasRadius` is still used by the Radius fallback lower in the method
for entities with no CylSphere/Sphere/BSP but a non-zero setup.Radius. The correct
final gate at shapes.Count==0 (after builder + Radius fallback) handles all cases:
- BSP-only entity: builder emits BSP shape -> shapes.Count>0 -> registered.
- Truly shapeless (no BSP, no cyl, no sphere, no radius): builder empty, no Radius
fallback fires -> shapes.Count==0 -> return (not registered, passable). Correct.
Retail anchor: CPhysicsObj::FindObjCollisions (acclient_2013_pseudo_c.txt:276917) --
the gate at pc:276917 is on the MOVER's CPartArray, not a target-side shape filter.
CPartArray::FindObjCollisions (pc:286236) iterates ALL parts; each part's
find_obj_collisions tests physics_bsp when present. There is no retail equivalent of
our premature gate that skips BSP-only targets.
Tests (ShadowShapeBuilderShapeSourceTests): two new cases.
Setup_WithBspPart_NoCylSpheres_EmitsBspShape -- proves the builder emits the shape
the premature gate was discarding.
Setup_WithPartButNoBsp_NoCylSpheres_YieldsEmptyShapeList -- regression guard proving
truly shapeless entities are still not registered (the shapes.Count==0 gate holds).
Full Core suite: 1595 pass / 0 fail / 2 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
3.9 KiB
C#
93 lines
3.9 KiB
C#
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);
|
|
}
|
|
|
|
// D4+W1 (2026-06-24) — BSP-only furniture weenie guard.
|
|
//
|
|
// A Setup with no CylSpheres, no Spheres, no Radius, but whose Part has a
|
|
// PhysicsBSP (e.g. a candle holder or floor candelabra) MUST produce a BSP
|
|
// ShadowShape. The premature `if (!hasCyl && !hasSphere && !hasRadius) return`
|
|
// gate in GameWindow.RegisterLiveEntityCollision was dropped because it fired
|
|
// BEFORE ShadowShapeBuilder ran and discarded these entities entirely.
|
|
//
|
|
// Retail oracle: CPhysicsPart::find_obj_collisions@0x0050D8D0 -- when
|
|
// physics_bsp is non-null the part IS tested; the outer loop in
|
|
// CPartArray::FindObjCollisions iterates all parts regardless of
|
|
// CylSpheres/Spheres. ShadowShapeBuilder.FromSetup mirrors this by emitting
|
|
// one BSP shape per part that `hasPhysicsBsp` returns true for.
|
|
[Fact]
|
|
public void Setup_WithBspPart_NoCylSpheres_EmitsBspShape()
|
|
{
|
|
// hasPhysicsBsp predicate returns true for 0x0100AAAAu, simulating a
|
|
// GfxObj that has a PhysicsBSP but no CylSpheres/Spheres.
|
|
const uint BspGfxObjId = 0x0100AAAAu;
|
|
var setup = new Setup
|
|
{
|
|
CylSpheres = new List<CylSphere>(),
|
|
Spheres = new List<Sphere>(),
|
|
Parts = { BspGfxObjId },
|
|
PlacementFrames = new Dictionary<Placement, AnimationFrame>(),
|
|
};
|
|
var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f,
|
|
hasPhysicsBsp: id => id == BspGfxObjId);
|
|
|
|
Assert.Contains(shapes, s => s.CollisionType == ShadowCollisionType.BSP);
|
|
}
|
|
|
|
// Complementary regression guard: a Setup with no CylSpheres, no Spheres,
|
|
// and a Part that has NO PhysicsBSP must still produce an empty shape list.
|
|
// The premature gate removal does not accidentally register shapeless entities.
|
|
[Fact]
|
|
public void Setup_WithPartButNoBsp_NoCylSpheres_YieldsEmptyShapeList()
|
|
{
|
|
const uint NoBspGfxObjId = 0x0100BBBBu;
|
|
var setup = new Setup
|
|
{
|
|
CylSpheres = new List<CylSphere>(),
|
|
Spheres = new List<Sphere>(),
|
|
Parts = { NoBspGfxObjId },
|
|
PlacementFrames = new Dictionary<Placement, AnimationFrame>(),
|
|
};
|
|
var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f,
|
|
hasPhysicsBsp: _ => false);
|
|
|
|
Assert.Empty(shapes);
|
|
}
|
|
}
|