test(phys): A6.P4 door — directional + geometric pin tests reframe inside-out bug
Built three new tests to investigate the inside-out asymmetric collision that persists after the AddAllOutsideCells coord fix: 1. Directional_OutsideIn_SouthApproach_BlocksAtSlabSouthFace — sphere south of door moving NORTH; expects block with cn.Y less than -0.5 2. Directional_InsideOut_NorthApproach_BlocksAtSlabNorthFace — sphere north of door moving SOUTH; expects block with cn.Y greater than +0.5 3. Geometric_DoorSlabZRange_AbovePlayerSphereTop — pins the slab Z range vs sphere top math BOTH directional tests PASS — collision is symmetric at unit-test level. The asymmetric production bug therefore comes from something the unit tests do not capture (multi-tick state, cell-tracking flicker, walkable polygon edge interactions). The geometric pin test reveals the real story: Setup 0x020019FF places the part-0 BSP slab 1.275 m ABOVE the entity origin via PlacementFrames[Default][0].Origin. With the cottage door entity at world Z=94.1, the slab world Z range is [95.375, 97.865]. Player sphere top reaches Z=95.20. The slab BOTTOM is 0.175 m ABOVE the sphere top — the slab NEVER collides with the player. The slab is a LINTEL (door frame above the doorway), not a leaf. The door's only effective collider at sphere height is the 0.10 m radius foot cylinder. The directional tests pass because the cylinder blocks, not the BSP. User-reported inside-out off-center walkthrough is the sphere walking AROUND the foot cylinder (sphere reach 0.48 + cyl 0.10 = 0.58 m; any sphere center over 0.58 m from cylinder center passes freely). The visual "body partially intersects door" is the character model occupying the visual door volume while the collision sphere passes beside the cylinder. Reframed handoff in docs/research/2026-05-25-door-bug-partial-fix-shipped.md points to three candidate next-step investigations: - Retail-faithfulness audit on setup.Radius / setup.Height interpretation - Re-inspect door parts 1+2 (GfxObj 0x010044B6) for missed physics shapes - Test the cottage cell BSP (cell 0x0150 walls) + door together — the COMBINED collision may be what retail relies on Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28cd97be62
commit
c27fded61e
2 changed files with 413 additions and 20 deletions
|
|
@ -106,29 +106,90 @@ The `[bsp-test]` probe fires 245 times for the door entity during the
|
||||||
post-fix inside-out attempts — door IS being queried. The
|
post-fix inside-out attempts — door IS being queried. The
|
||||||
collision-detection mechanics produce the wrong response.
|
collision-detection mechanics produce the wrong response.
|
||||||
|
|
||||||
## What's next (separate bug)
|
## What's next (separate bug — REFRAMED 2026-05-25 evening)
|
||||||
|
|
||||||
**Investigate BSPQuery.FindCollisions's response for two-sided polygons
|
**Initial hypothesis was wrong.** Two new directional tests built
|
||||||
when the sphere is already overlapping the slab.** Retail's
|
post-fix (`Directional_OutsideIn_*`, `Directional_InsideOut_*`) BOTH
|
||||||
`CBSPTree::find_collisions` family handles this specifically — the
|
PASS — the BSP collision response is symmetric at unit-test level.
|
||||||
sphere's path through the slab faces gets traced and the FIRST face
|
The asymmetric production bug must come from something the unit tests
|
||||||
crossed in motion direction is the collision. With two-sided polygons,
|
weren't capturing.
|
||||||
both faces are collidable; the front-vs-back determination is by
|
|
||||||
sphere-velocity vs face-normal dot product.
|
|
||||||
|
|
||||||
Likely files:
|
A geometric pin test (`Geometric_DoorSlabZRange_AbovePlayerSphereTop`)
|
||||||
- `src/AcDream.Core/Physics/BSPQuery.cs` — the BSP traversal +
|
reveals the real story:
|
||||||
sphere-poly intersection logic.
|
|
||||||
- Retail decomp anchors:
|
|
||||||
`acclient_2013_pseudo_c.txt:BSPTREE::find_collisions` +
|
|
||||||
`SPHEREPATH::sphere_intersects_poly` family.
|
|
||||||
|
|
||||||
Apparatus to write next: a focused test that registers the door at its
|
```
|
||||||
actual production world transform (entity origin + partFrame offset
|
Setup 0x020019FF (cottage door):
|
||||||
from the dat, with correct rotation) and replays a sphere passing
|
CylSphere[0]: r=0.10, h=0.20, origin=(0, 0, 0.018)
|
||||||
through it from EACH side at various speeds. Compare collision normal
|
→ world Z [94.118, 94.318] when entity at Z=94.1
|
||||||
+ position-resolution per side. The asymmetric response will be
|
Part 0 (GfxObj 0x010044B5, the BSP "slab"):
|
||||||
reproducible at unit-test speed.
|
placement frame [Default][0].Origin = (-0.006, 0.125, 1.275)
|
||||||
|
→ BSP world Z [95.375, 97.865] when entity at Z=94.1
|
||||||
|
|
||||||
|
Player at floor Z=94:
|
||||||
|
sphere height = 1.20, sphere top = 95.20
|
||||||
|
|
||||||
|
BSP slab BOTTOM (95.375) is ABOVE sphere TOP (95.20) by 0.175 m.
|
||||||
|
The slab NEVER collides with the player's body sphere.
|
||||||
|
```
|
||||||
|
|
||||||
|
The slab is a LINTEL (the door frame above the doorway), not a leaf.
|
||||||
|
The door's only collision against a player at floor level is the
|
||||||
|
0.10 m radius foot cylinder. Sphere radius 0.48 + cyl 0.10 = 0.58 m
|
||||||
|
collision reach. Any sphere center > 0.58 m from cylinder center
|
||||||
|
(132.6, 17.1) passes freely.
|
||||||
|
|
||||||
|
The user-reported "inside-out walkthrough at ~50 cm off-center" is
|
||||||
|
the sphere walking AROUND the cylinder, at X = 132.6 ± 0.6+ m where
|
||||||
|
collision misses entirely. "Body partially intersects door" is the
|
||||||
|
character model occupying the visual door's volume while the collision
|
||||||
|
sphere passes beside the foot cylinder.
|
||||||
|
|
||||||
|
The "outside-in works" case is also the foot cylinder doing the
|
||||||
|
blocking — when the user approaches more centered or hits the cylinder
|
||||||
|
head-on. The cell-visibility fix made the cylinder visible from indoor
|
||||||
|
cells (it wasn't before), which is why outside-in went from "walks
|
||||||
|
through" to "blocks" — but the cylinder is the actual collider.
|
||||||
|
|
||||||
|
**This is a door-geometry interpretation question, NOT a BSP query bug.**
|
||||||
|
Three candidate next-step investigations:
|
||||||
|
|
||||||
|
1. **Retail-faithfulness audit on door collision.** Read retail's
|
||||||
|
`CPhysicsObj::set_setup_for_door` (or similar) to determine what
|
||||||
|
shapes retail loads for a closed cottage door. If retail uses the
|
||||||
|
SAME setup data + finds the same shapes we do, the door geometry
|
||||||
|
in the dat IS the spec. The "blocking" the user sees in retail
|
||||||
|
might similarly be the foot cylinder + perhaps a different default
|
||||||
|
sphere from setup.Radius/setup.Height that we're not registering.
|
||||||
|
|
||||||
|
2. **Inspect door parts 1+2 (GfxObj 0x010044B6).** Per prior session's
|
||||||
|
handoff they are visual-only (HasPhysics=false). Verify by direct
|
||||||
|
dat read — maybe the PhysicsBSP is null but the cylinder/sphere
|
||||||
|
list on the GfxObj itself has collision data we're missing.
|
||||||
|
`DoorSetupGfxObjInspectionTests` already prints this; re-read.
|
||||||
|
|
||||||
|
3. **The cottage cell BSP encloses the doorway.** Cell 0x0150
|
||||||
|
(the doorway alcove) is bounded by cottage walls. The DOOR opens
|
||||||
|
through a gap in those walls. When the door is closed, the gap
|
||||||
|
is filled by the door geometry. Maybe retail's collision relies
|
||||||
|
on the cottage walls (cell BSP) for the "doorway side walls" and
|
||||||
|
the door's slab covers ONLY the opening's leaf area. The asymmetric
|
||||||
|
block we see could be the cottage cell walls catching outside-in
|
||||||
|
approach (cell BSP collision via FindEnvCollisions, before the
|
||||||
|
door's shadow ever fires) but NOT catching inside-out at the same
|
||||||
|
alcove geometry.
|
||||||
|
|
||||||
|
The 50cm off-center approach probably exits the cottage walls' BSP
|
||||||
|
through the doorway gap, walks past the foot cylinder (which is small),
|
||||||
|
and never has anything to collide against in the world's collision
|
||||||
|
representation. Retail might block this via a `setup.Radius=0.1414`
|
||||||
|
cylinder we're missing (Setup.Radius isn't currently registered by
|
||||||
|
`ShadowShapeBuilder.FromSetup` for entities WITH a CylSphere).
|
||||||
|
|
||||||
|
Apparatus to write next: a test that registers the door using
|
||||||
|
ShadowShapeBuilder.FromSetup AND verifies setup.Radius is reflected
|
||||||
|
in the shape list. If it isn't, that's a candidate fix — the door's
|
||||||
|
0.14 m radius + 0.20 m height SETUP sphere is wider than the 0.10 m
|
||||||
|
CylSphere and would catch the off-center approach.
|
||||||
|
|
||||||
## Commits
|
## Commits
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,338 @@ public class DoorBugTrajectoryReplayTests
|
||||||
string.Join(",", candidates.Select(c => $"0x{c:X8}")));
|
string.Join(",", candidates.Select(c => $"0x{c:X8}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A6.P4 inside-out asymmetric collision (2026-05-25 evening) —
|
||||||
|
/// synthesizes a sphere approaching a faithfully-registered door
|
||||||
|
/// from each side and asserts the BSP collision fires symmetrically.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Door registered via the SAME path production uses
|
||||||
|
/// (ShadowShapeBuilder.FromSetup + RegisterMultiPart) with the
|
||||||
|
/// real Setup 0x020019FF loaded from the dat, including the
|
||||||
|
/// part-0 BSP's PlacementFrame[Default][0] origin of
|
||||||
|
/// (-0.006, 0.125, 1.275). Entity world rotation is 180° around Z
|
||||||
|
/// to match the cottage's world transform (per the cellar fixture
|
||||||
|
/// pattern + observed [bsp-test] world position alignment).
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// After the 180° entity rotation, the slab's local Y thickness axis
|
||||||
|
/// maps to world -Y. Slab spans world Y in approximately
|
||||||
|
/// [<see cref="DoorSpawnPos"/>.Y - 0.261, <see cref="DoorSpawnPos"/>.Y]
|
||||||
|
/// (entity Y minus 0 to entity Y minus 0.261 thickness). Two faces
|
||||||
|
/// matter:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>Higher-Y face</b> (world Y ≈ entity.Y) has world
|
||||||
|
/// normal +Y. A sphere NORTH of the slab moving SOUTH (-Y)
|
||||||
|
/// hits this face. <c>cn</c> should be near (0, +1, 0).</item>
|
||||||
|
/// <item><b>Lower-Y face</b> (world Y ≈ entity.Y - 0.261) has world
|
||||||
|
/// normal -Y. A sphere SOUTH of the slab moving NORTH (+Y)
|
||||||
|
/// hits this face. <c>cn</c> should be near (0, -1, 0).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// User-reported behavior post-AddAllOutsideCells-fix:
|
||||||
|
/// outside→inside blocks cleanly; inside→outside shows the body
|
||||||
|
/// partially intersecting the door before sphere slides through.
|
||||||
|
/// If the asymmetry is in BSP collision, these tests will
|
||||||
|
/// reproduce it at unit-test speed.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Geometric finding (2026-05-25 evening) — pins the door geometry
|
||||||
|
/// math that explains why the "inside-out walkthrough" persists
|
||||||
|
/// after the cell-visibility fix.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// The cottage door Setup 0x020019FF has:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>One CylSphere (r=0.10, h=0.20, origin=(0, 0, 0.018)) — a
|
||||||
|
/// TINY foot collider at entity Z + 0.018, extending Z just
|
||||||
|
/// to 0.218 above entity Z.</item>
|
||||||
|
/// <item>Part 0 = GfxObj 0x010044B5 (BSP slab 1.925 × 0.261 × 2.490 m),
|
||||||
|
/// placed via PlacementFrames[Default][0].Origin =
|
||||||
|
/// (-0.006, 0.125, 1.275). The slab's local Z=0 origin sits
|
||||||
|
/// at entity Z + 1.275 — i.e., the slab's BOTTOM is 1.275 m
|
||||||
|
/// ABOVE the door's entity foot.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// With entity at (132.6, 17.1, 94.1) (the captured Holtburg cottage
|
||||||
|
/// door spawn position):
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Cylinder world Z range: [94.118, 94.318] — touches the
|
||||||
|
/// ground (sphere foot Z=94 to top Z=95.20 overlaps cyl Z up to 94.318).</item>
|
||||||
|
/// <item>Slab world Z range: [95.375, 97.865]. <b>The slab's BOTTOM
|
||||||
|
/// (95.375 m) is 0.175 m ABOVE the player's sphere top
|
||||||
|
/// (95.20 m).</b> The slab NEVER intersects the player's
|
||||||
|
/// body sphere vertically.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Implication: the door's only effective collision against a player
|
||||||
|
/// at floor level is the 0.10 m radius foot cylinder. The 1.93 m wide
|
||||||
|
/// slab is collision-irrelevant at sphere height — it's a LINTEL
|
||||||
|
/// (the door frame above), not a leaf collision. The user-reported
|
||||||
|
/// "off-center walkthrough" is the sphere walking AROUND the
|
||||||
|
/// 0.10 m foot cylinder (sphere reach 0.48 + cyl 0.10 = 0.58 m;
|
||||||
|
/// any sphere center >0.58 m from cylinder center passes freely).
|
||||||
|
/// The "body partially intersects door" is the rendered character model
|
||||||
|
/// occupying volume the door visual fills, but no collision body to
|
||||||
|
/// stop it because the slab is too high.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Next session: this is a DAT or registration issue, not a BSP query
|
||||||
|
/// bug. Options:
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>Verify retail actually USES this geometry — maybe the door
|
||||||
|
/// relies on the cottage CELL'S walls (cell 0x0150's
|
||||||
|
/// PhysicsPolygons) to enclose the doorway, and the door's
|
||||||
|
/// only collision is the foot cylinder + a leaf shape we're
|
||||||
|
/// missing.</item>
|
||||||
|
/// <item>Inspect parts 1+2 (the door LEAVES, GfxObj 0x010044B6) to
|
||||||
|
/// confirm they're truly visual-only or if we missed a
|
||||||
|
/// physics shape.</item>
|
||||||
|
/// <item>cdb attach to retail at a Holtburg cottage door — set a
|
||||||
|
/// breakpoint on CTransition::FindObjCollisions for the door
|
||||||
|
/// entity and inspect what shapes retail tests against.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Geometric_DoorSlabZRange_AbovePlayerSphereTop()
|
||||||
|
{
|
||||||
|
var datDir = ResolveDatDir();
|
||||||
|
if (datDir is null) return;
|
||||||
|
|
||||||
|
// Load the door setup + part 0's PlacementFrame.
|
||||||
|
DatReaderWriter.Types.Frame partFrame;
|
||||||
|
float slabLocalZMax;
|
||||||
|
using (var dats = new DatCollection(datDir, DatAccessType.Read))
|
||||||
|
{
|
||||||
|
var setup = dats.Get<DatReaderWriter.DBObjs.Setup>(0x020019FFu)!;
|
||||||
|
Assert.NotNull(setup);
|
||||||
|
Assert.True(setup.PlacementFrames.ContainsKey(DatReaderWriter.Enums.Placement.Default));
|
||||||
|
partFrame = setup.PlacementFrames[DatReaderWriter.Enums.Placement.Default].Frames[0];
|
||||||
|
|
||||||
|
var gfx = dats.Get<DatReaderWriter.DBObjs.GfxObj>(DoorGfxObjId)!;
|
||||||
|
Assert.NotNull(gfx.PhysicsPolygons);
|
||||||
|
// Compute local AABB Z max from vertex array.
|
||||||
|
slabLocalZMax = float.MinValue;
|
||||||
|
foreach (var poly in gfx.PhysicsPolygons.Values)
|
||||||
|
{
|
||||||
|
foreach (ushort vid in poly.VertexIds)
|
||||||
|
{
|
||||||
|
if (gfx.VertexArray.Vertices.TryGetValue(vid, out var sv))
|
||||||
|
if (sv.Origin.Z > slabLocalZMax) slabLocalZMax = sv.Origin.Z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The door's entity is placed at world Z=94.1 in Holtburg (per
|
||||||
|
// captured spawn log). Slab local Z=0 origin offsets to:
|
||||||
|
float slabWorldZBottom = DoorSpawnPos.Z + partFrame.Origin.Z;
|
||||||
|
float slabWorldZTop = slabWorldZBottom + slabLocalZMax;
|
||||||
|
|
||||||
|
// The player has sphereHeight=1.20, sphereRadius=0.48. Sphere top
|
||||||
|
// in world = foot.Z + height - radius + radius = foot.Z + height
|
||||||
|
// (the top of the head sphere centered at foot.Z + height - radius).
|
||||||
|
const float SphereHeight = 1.20f;
|
||||||
|
const float PlayerFootZ = 94f; // standard Holtburg floor
|
||||||
|
float sphereTopZ = PlayerFootZ + SphereHeight;
|
||||||
|
|
||||||
|
// The crucial assertion: slab bottom is above sphere top.
|
||||||
|
Assert.True(slabWorldZBottom > sphereTopZ,
|
||||||
|
$"Door slab bottom ({slabWorldZBottom:F3}) should be ABOVE " +
|
||||||
|
$"player sphere top ({sphereTopZ:F3}). Gap = " +
|
||||||
|
$"{slabWorldZBottom - sphereTopZ:F3} m. This pins the geometric " +
|
||||||
|
$"fact that the slab does not collide with the player at floor " +
|
||||||
|
$"level — only the foot cylinder does. The inside-out 'walkthrough' " +
|
||||||
|
$"is the sphere passing around the cylinder, not through the slab.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Directional_OutsideIn_SouthApproach_BlocksAtSlabSouthFace()
|
||||||
|
{
|
||||||
|
var datDir = ResolveDatDir();
|
||||||
|
if (datDir is null) return;
|
||||||
|
|
||||||
|
var (engine, _) = BuildFaithfulDoorEngine(datDir);
|
||||||
|
|
||||||
|
// Sphere starts SOUTH of slab (low Y), moves NORTH (+Y) toward door.
|
||||||
|
// Slab world Y ∈ [16.84, 17.10] approximately after 180° entity rot.
|
||||||
|
// Sphere south edge needs to be just south of slab south face.
|
||||||
|
var currentPos = new Vector3(132.5f, 16.3f, 94f);
|
||||||
|
var targetPos = new Vector3(132.5f, 16.7f, 94f); // +0.4 m north
|
||||||
|
|
||||||
|
var (result, body) = ResolveAt(engine, currentPos, targetPos, DoorCellOutdoor);
|
||||||
|
|
||||||
|
// The slab's south face has world normal (0, -1, 0) after the
|
||||||
|
// 180° entity rotation. Sphere moving +Y hits it; collision
|
||||||
|
// normal should oppose motion, i.e., negative Y component.
|
||||||
|
Assert.True(result.CollisionNormalValid,
|
||||||
|
$"Outside-in: door should block sphere. Got: pos={result.Position}, " +
|
||||||
|
$"cnValid={result.CollisionNormalValid}, cn={result.CollisionNormal}.");
|
||||||
|
Assert.True(result.CollisionNormal.Y < -0.5f,
|
||||||
|
$"Outside-in: cn.Y should be negative (south face normal). " +
|
||||||
|
$"Got cn={result.CollisionNormal}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Directional_InsideOut_NorthApproach_BlocksAtSlabNorthFace()
|
||||||
|
{
|
||||||
|
var datDir = ResolveDatDir();
|
||||||
|
if (datDir is null) return;
|
||||||
|
|
||||||
|
var (engine, _) = BuildFaithfulDoorEngine(datDir);
|
||||||
|
|
||||||
|
// Sphere starts NORTH of slab (high Y), moves SOUTH (-Y) toward door.
|
||||||
|
var currentPos = new Vector3(132.5f, 17.6f, 94f);
|
||||||
|
var targetPos = new Vector3(132.5f, 17.2f, 94f); // -0.4 m south
|
||||||
|
|
||||||
|
var (result, body) = ResolveAt(engine, currentPos, targetPos, DoorCellOutdoor);
|
||||||
|
|
||||||
|
// The slab's north face has world normal (0, +1, 0) after the
|
||||||
|
// 180° entity rotation. Sphere moving -Y hits it; collision
|
||||||
|
// normal should oppose motion, i.e., positive Y component.
|
||||||
|
Assert.True(result.CollisionNormalValid,
|
||||||
|
$"Inside-out: door should block sphere. Got: pos={result.Position}, " +
|
||||||
|
$"cnValid={result.CollisionNormalValid}, cn={result.CollisionNormal}.");
|
||||||
|
Assert.True(result.CollisionNormal.Y > 0.5f,
|
||||||
|
$"Inside-out: cn.Y should be positive (north face normal). " +
|
||||||
|
$"Got cn={result.CollisionNormal}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Faithful engine: registers the real Setup 0x020019FF door via
|
||||||
|
/// ShadowShapeBuilder.FromSetup at the captured entity world position
|
||||||
|
/// (132.6, 17.1, 94.1) with the cottage's 180° Z rotation. Mirrors
|
||||||
|
/// production GameWindow.RegisterLiveEntityCollision exactly.
|
||||||
|
/// </summary>
|
||||||
|
private static (PhysicsEngine engine, PhysicsDataCache cache)
|
||||||
|
BuildFaithfulDoorEngine(string datDir)
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var engine = new PhysicsEngine { DataCache = cache };
|
||||||
|
|
||||||
|
// Cache GfxObj 0x010044B5 (the BSP slab) from dat.
|
||||||
|
DatReaderWriter.DBObjs.Setup setup;
|
||||||
|
using (var dats = new DatCollection(datDir, DatAccessType.Read))
|
||||||
|
{
|
||||||
|
var gfx = dats.Get<DatReaderWriter.DBObjs.GfxObj>(DoorGfxObjId);
|
||||||
|
Assert.NotNull(gfx);
|
||||||
|
cache.CacheGfxObj(DoorGfxObjId, gfx!);
|
||||||
|
|
||||||
|
setup = dats.Get<DatReaderWriter.DBObjs.Setup>(0x020019FFu)!;
|
||||||
|
Assert.NotNull(setup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub landblock at (0, 0) so TryGetLandblockContext succeeds.
|
||||||
|
var heights = new byte[81];
|
||||||
|
var heightTable = new float[256];
|
||||||
|
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
|
||||||
|
engine.AddLandblock(
|
||||||
|
landblockId: DoorLandblockId,
|
||||||
|
terrain: new TerrainSurface(heights, heightTable),
|
||||||
|
cells: Array.Empty<CellSurface>(),
|
||||||
|
portals: Array.Empty<PortalPlane>(),
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
|
||||||
|
// Build shape list the same way production does
|
||||||
|
// (GameWindow.RegisterLiveEntityCollision):
|
||||||
|
// 1. ShadowShapeBuilder.FromSetup with entScale=1
|
||||||
|
// 2. Substitute BSP shape's radius with the real BoundingSphere.Radius
|
||||||
|
var rawShapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f,
|
||||||
|
id => cache.GetGfxObj(id)?.BSP?.Root is not null);
|
||||||
|
var shapes = new List<ShadowShape>(rawShapes.Count);
|
||||||
|
foreach (var s in rawShapes)
|
||||||
|
{
|
||||||
|
if (s.CollisionType == ShadowCollisionType.BSP)
|
||||||
|
{
|
||||||
|
var phys = cache.GetGfxObj(s.GfxObjId);
|
||||||
|
float bspR = phys?.BoundingSphere?.Radius ?? 2f;
|
||||||
|
shapes.Add(s with { Radius = bspR });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shapes.Add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Assert.Contains(shapes, s => s.CollisionType == ShadowCollisionType.BSP);
|
||||||
|
|
||||||
|
// Register the door at the cottage's entity world transform:
|
||||||
|
// - Position from the captured spawn data: (132.6, 17.1, 94.1)
|
||||||
|
// - Rotation 180° around Z to match cottage orientation
|
||||||
|
// (consistent with [bsp-test] world position alignment)
|
||||||
|
engine.ShadowObjects.RegisterMultiPart(
|
||||||
|
entityId: DoorEntityId,
|
||||||
|
entityWorldPos: DoorSpawnPos,
|
||||||
|
entityWorldRot: Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI),
|
||||||
|
shapes: shapes,
|
||||||
|
state: DoorClosedState,
|
||||||
|
flags: EntityCollisionFlags.None,
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f,
|
||||||
|
landblockId: DoorLandblockId);
|
||||||
|
|
||||||
|
return (engine, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run one <see cref="PhysicsEngine.ResolveWithTransition"/> call
|
||||||
|
/// against <paramref name="engine"/> at the given positions/cell,
|
||||||
|
/// returning the result + the body's post-call state.
|
||||||
|
/// </summary>
|
||||||
|
private static (ResolveResult result, PhysicsBody body)
|
||||||
|
ResolveAt(PhysicsEngine engine, Vector3 currentPos, Vector3 targetPos, uint cellId)
|
||||||
|
{
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = currentPos,
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
ContactPlaneValid = true,
|
||||||
|
ContactPlane = new System.Numerics.Plane(0f, 0f, 1f, -94f),
|
||||||
|
ContactPlaneCellId = cellId,
|
||||||
|
WalkablePolygonValid = true,
|
||||||
|
WalkablePlane = new System.Numerics.Plane(0f, 0f, 1f, -94f),
|
||||||
|
WalkableVertices = new[]
|
||||||
|
{
|
||||||
|
// Big walkable poly covering Y in [10, 30], X in [120, 145].
|
||||||
|
new Vector3(120f, 10f, 94f),
|
||||||
|
new Vector3(145f, 10f, 94f),
|
||||||
|
new Vector3(145f, 30f, 94f),
|
||||||
|
new Vector3(120f, 30f, 94f),
|
||||||
|
},
|
||||||
|
WalkableUp = Vector3.UnitZ,
|
||||||
|
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = engine.ResolveWithTransition(
|
||||||
|
currentPos: currentPos,
|
||||||
|
targetPos: targetPos,
|
||||||
|
cellId: cellId,
|
||||||
|
sphereRadius: 0.48f,
|
||||||
|
sphereHeight: 1.20f,
|
||||||
|
stepUpHeight: 0.60f,
|
||||||
|
stepDownHeight: 1.5f,
|
||||||
|
isOnGround: true,
|
||||||
|
body: body,
|
||||||
|
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||||
|
movingEntityId: DoorEntityId + 1);
|
||||||
|
|
||||||
|
return (result, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The captured door spawn position from launch.log [entity-source]:
|
||||||
|
// "live: spawn ... name=Door setup=0x020019FF pos=(132.6,17.1,94.1)@0xA9B40029"
|
||||||
|
private static readonly Vector3 DoorSpawnPos = new(132.6f, 17.1f, 94.1f);
|
||||||
|
private const uint DoorCellOutdoor = 0xA9B40029u;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Direct test of <see cref="CellTransit.AddAllOutsideCells"/> with
|
/// Direct test of <see cref="CellTransit.AddAllOutsideCells"/> with
|
||||||
/// the captured sphere position (132.36, 16.81, 94) and currentCellId
|
/// the captured sphere position (132.36, 16.81, 94) and currentCellId
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue