docs(physics): implementation plan — unified verbatim-collision (5 slices)
Task-by-task plan for the unified collision-inclusion fix: D1 DAT-only shape authority + delete mesh-AABB, D3 sphere primitive, D2+D5 obstruction_ethereal port, D8 RemoveCellsForLandblock, D4+W1 entry-restrictions + dispatch wiring. Each task: grep-named -> pseudocode -> port -> conformance-test, build+test green, commit; visual gates batched to the end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2d9e0cf424
commit
4f26067755
1 changed files with 411 additions and 0 deletions
411
docs/superpowers/plans/2026-06-24-unified-collision-inclusion.md
Normal file
411
docs/superpowers/plans/2026-06-24-unified-collision-inclusion.md
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
# Unified Verbatim-Retail Collision-Inclusion — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. This is the **DO-NOT-RETRY collision area** — every task MUST follow grep-named-retail → pseudocode → port → conformance-test. **No guess-patches.** If a port step resists, STOP and report; do not improvise.
|
||||
|
||||
**Goal:** Make acdream's "which objects collide, and how the engine decides that" verbatim with retail — one DAT-only collision-shape authority + one query predicate + four faithful channels + one anti-staleness invariant.
|
||||
|
||||
**Architecture:** Complete the units that already implement retail's two-layer model (`ShadowShapeBuilder` = shape authority; `CollisionExemption` + dispatch = query predicate; per-cell `ShadowObjectRegistry` = registration; per-apply rebase). Delete the non-faithful mesh-AABB synthesis. Keep terrain/EnvCell/building/object as distinct channels (retail does). Five sequential slices, each build+test green and committed.
|
||||
|
||||
**Tech Stack:** C# .NET 10, xUnit, Silk.NET. DAT via `DatReaderWriter`/`DatCollection`. Named-retail decomp at `docs/research/named-retail/` (main repo).
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-06-24-unified-collision-inclusion-design.md`](../specs/2026-06-24-unified-collision-inclusion-design.md). **Audit:** [`docs/research/2026-06-24-collision-inclusion-audit.md`](../../research/2026-06-24-collision-inclusion-audit.md).
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight (every task)
|
||||
|
||||
- **Read first:** the spec (above), `claude-memory/project_physics_collision_digest.md` (DO-NOT-RETRY table), and the audit row for the deviation you're closing.
|
||||
- **Grep named-retail FIRST** for the cited `Class::method` in `docs/research/named-retail/acclient_2013_pseudo_c.txt` (main repo `C:\Users\erikn\source\repos\acdream\`). Confirm the behavior against the decomp before writing C#. Cross-ref `references/ACE/Source/ACE.Server/Physics/` and `references/ACViewer/Physics/Common/`.
|
||||
- **Build:** `dotnet build` must be green before any commit.
|
||||
- **Tests:** `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj` (Core), plus the named replay harnesses per task. **The replay harnesses (`CellarUpTrajectoryReplayTests`, `HouseExitWalk*`, `CornerFlood*`, `Issue147ArwicBuildingsDumpTests`) must stay green for EVERY task** — they pin the shadow-list/building/cell machinery.
|
||||
- **Register:** any deviation retired deletes its row in `docs/architecture/retail-divergence-register.md` in the SAME commit.
|
||||
- **Visual gates are batched to the end** (user request) — do NOT stop mid-plan for them. Land all five slices, then hand off for testing.
|
||||
|
||||
---
|
||||
|
||||
## Task 1 — DAT-only shape authority + delete mesh-AABB (D1, W3)
|
||||
|
||||
**Retail oracle:** `CPartArray::InitParts@0x00517F40` (parts from Setup only), `CGfxObj::Serialize@0x00534970` (physics_bsp gated on serialized-flags bit-0), `CPhysicsPart::find_obj_collisions@0x0050D8D0` (null physics_bsp ⇒ returns OK = passable). Render-mesh bounds never enter collision.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` — delete the mesh-AABB synthesis block (begins at the comment block `:7397-7425`, the `if (!isPhantomSetup && !isPhantomGfxObj && !_isLandblockStab && (_isOutdoorMesh || (entityBsp==0 && entityCyl==0)) && entity.MeshRefs.Count > 0)` body, through the `ShadowObjects.Register(... ShadowCollisionType.Cylinder ...)` call that closes it ~`:7565`). Also remove the now-dead `isPhantomGfxObj`/`isPhantomSetup` locals (`:7384-7406`) and the scenery `_isOutdoorMesh`-driven synthesis. Route every object-registration site through `ShadowShapeBuilder` for its collision shape.
|
||||
- Modify (if needed for the unified call): `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` — ensure it is the single shape authority; the registration radius comes from the shape's bounding sphere, never a synthetic constant (kills the `2f` default at `:97` and the `1f`/`2f` W3 split).
|
||||
- Possibly delete: `PhysicsDataCache.IsPhantomGfxObjSource` (`:399-403`) becomes dead once the fallback it fenced is gone — remove it and its only call site (`GameWindow.cs:7406`).
|
||||
- Test: `tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderShapeSourceTests.cs` (new).
|
||||
|
||||
- [ ] **Step 1: Write the failing test — shapeless Setup yields NO collision shape**
|
||||
|
||||
```csharp
|
||||
// tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderShapeSourceTests.cs
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using Xunit;
|
||||
|
||||
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 = new List<uint> { 0x01000ABCu }, // a part whose GfxObj has NO physics BSP
|
||||
PlacementFrames = new System.Collections.Generic.Dictionary<Placement, AnimationFrame>(),
|
||||
};
|
||||
|
||||
var shapes = ShadowShapeBuilder.FromSetup(setup, entScale: 1f, hasPhysicsBsp: _ => false);
|
||||
|
||||
Assert.Empty(shapes); // no synthetic mesh-AABB, no fallback cylinder
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it — expect PASS already (authority is already DAT-only); this is the GUARD that locks the invariant**
|
||||
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~ShadowShapeBuilderShapeSourceTests"`
|
||||
Expected: PASS. (If FAIL, `ShadowShapeBuilder` itself synthesizes a fallback — fix it to be DAT-only first.) This test exists to keep it that way after the App-layer deletion.
|
||||
|
||||
- [ ] **Step 3: Delete the mesh-AABB synthesis in GameWindow**
|
||||
|
||||
Open `src/AcDream.App/Rendering/GameWindow.cs`. Read `:7370-7575` to find the exact block boundaries. Delete the entire `VISUAL mesh-bounds collision` synthesis (the `if (!isPhantomSetup && ...)` block and the `worldMin/worldMax` AABB computation and the `ShadowObjects.Register(..., ShadowCollisionType.Cylinder, ...)` it culminates in). Remove `isPhantomGfxObj`/`isPhantomSetup` locals and `PhysicsDataCache.IsPhantomGfxObjSource` (now dead). Ensure each registration site still registers the **real** shapes from `ShadowShapeBuilder.FromSetup(...)` / the existing BSP/CylSphere path — only the synthetic-from-mesh path is removed. **Do not** remove `ComputeVisualBounds`/`GetVisualBounds` usage that feeds CULLING; only the collision use.
|
||||
|
||||
- [ ] **Step 4: Build + run the guard test + the replay harnesses**
|
||||
|
||||
Run: `dotnet build`
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~ShadowShapeBuilderShapeSourceTests|FullyQualifiedName~CellarUp|FullyQualifiedName~HouseExitWalk|FullyQualifiedName~CornerFlood|FullyQualifiedName~Issue147Arwic"`
|
||||
Expected: build green; all PASS. (The replay harnesses prove statics/buildings/cells with REAL DAT shapes still collide — only shape-less objects became passable.)
|
||||
|
||||
- [ ] **Step 5: Retire the register row + commit**
|
||||
|
||||
Delete the **AP-2** row from `docs/architecture/retail-divergence-register.md` (the mesh-AABB fallback divergence is gone). If an AP-row also covers the scenery `GameWindow.cs:7408` site separately, delete that too.
|
||||
|
||||
```bash
|
||||
git add src/AcDream.App/Rendering/GameWindow.cs src/AcDream.Core/Physics/ShadowShapeBuilder.cs src/AcDream.Core/Physics/PhysicsDataCache.cs tests/AcDream.Core.Tests/Physics/ShadowShapeBuilderShapeSourceTests.cs docs/architecture/retail-divergence-register.md
|
||||
git commit -m "fix(physics): D1 — collision shape is DAT-only; delete mesh-AABB synthesis
|
||||
|
||||
Retail gets collision shape from physics_bsp (CGfxObj::Serialize flags bit-0)
|
||||
or CSetup cylsphere/sphere only; no shape => passable (CPhysicsPart::
|
||||
find_obj_collisions@0x0050D8D0). Deletes the scenery/outdoor mesh-AABB
|
||||
synthetic-cylinder fallback (GameWindow.cs) and the IsPhantomGfxObjSource
|
||||
gate that fenced it. Shape-less objects now register no shape and are
|
||||
passable, verbatim with retail. Retires register row AP-2.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2 — true Sphere collision primitive (D3)
|
||||
|
||||
**Retail oracle:** `CSphere::intersects_sphere` (pc:276917) — the swept sphere-vs-sphere test on the `HAS_PHYSICS_BSP` clear → CylSphere-then-Sphere branch. `CPartArray::GetSphere@0x00518070`. **Port the intersection math from the decomp; conformance-test against golden values — do NOT eyeball.**
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core/Physics/ShadowObjectRegistry.cs:537` — add `Sphere` to the enum: `public enum ShadowCollisionType : byte { BSP, Cylinder, Sphere }`.
|
||||
- Modify: `src/AcDream.Core/Physics/ShadowShapeBuilder.cs:68-81` — emit `ShadowCollisionType.Sphere` (not a coerced short cylinder) for `Setup.Spheres`.
|
||||
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs` — narrow-phase dispatch: add a `ShadowCollisionType.Sphere` branch at the broad-phase (`:2503`, use 3-D `Length()` like the BSP/else branch) and at the test dispatch (`:2540` / the cylinder else-branch ~`:2734`) calling the new sphere primitive.
|
||||
- Create/Modify: `src/AcDream.Core/Physics/CollisionPrimitives.cs` — add a swept-sphere-vs-sphere intersection ported from `CSphere::intersects_sphere`.
|
||||
- Test: `tests/AcDream.Core.Tests/Physics/SphereIntersectsSphereConformanceTests.cs` (new).
|
||||
|
||||
- [ ] **Step 1: Grep the oracle, write pseudocode**
|
||||
|
||||
Grep `acclient_2013_pseudo_c.txt` for `CSphere::intersects_sphere` (and `CSphere::collide`). Write the pseudocode to `docs/research/2026-06-24-sphere-intersects-pseudocode.md` (closest-approach of the moving sphere center to the static sphere center over the sweep; collision when min distance ≤ sum of radii; return the time/normal retail returns). Cross-ref ACE `Physics/Common/Sphere.cs`.
|
||||
|
||||
- [ ] **Step 2: Write the failing conformance test (golden values from the decomp/ACE)**
|
||||
|
||||
```csharp
|
||||
// tests/AcDream.Core.Tests/Physics/SphereIntersectsSphereConformanceTests.cs
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
public class SphereIntersectsSphereConformanceTests
|
||||
{
|
||||
// Golden cases derived from CSphere::intersects_sphere (pc:276917) /
|
||||
// ACE Physics/Common/Sphere.cs. Replace EXPECTED with the values the
|
||||
// oracle produces for each case (head-on hit, glancing miss, already-touching).
|
||||
[Theory]
|
||||
// moverCenter, moverRadius, sweep(delta), targetCenter, targetRadius, expectHit
|
||||
[InlineData(0,0,0, 0.5f, /*sweep*/2,0,0, /*target*/1.0f,0,0, 0.5f, true)] // head-on
|
||||
[InlineData(0,0,0, 0.5f, 0,2,0, 1.0f,0,0, 0.5f, false)] // perpendicular miss
|
||||
public void SweptSphere_MatchesRetail(
|
||||
float mx,float my,float mz, float mr,
|
||||
float dx,float dy,float dz,
|
||||
float tx,float ty,float tz, float tr,
|
||||
bool expectHit)
|
||||
{
|
||||
bool hit = CollisionPrimitives.SweptSphereHitsSphere(
|
||||
new Vector3(mx,my,mz), mr, new Vector3(dx,dy,dz),
|
||||
new Vector3(tx,ty,tz), tr, out _);
|
||||
Assert.Equal(expectHit, hit);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run it — expect FAIL (method does not exist)**
|
||||
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~SphereIntersectsSphereConformanceTests"`
|
||||
Expected: FAIL — compile error / `SweptSphereHitsSphere` not defined.
|
||||
|
||||
- [ ] **Step 4: Port the primitive + the enum + builder emission + dispatch**
|
||||
|
||||
Implement `CollisionPrimitives.SweptSphereHitsSphere(...)` from the pseudocode. Add `Sphere` to `ShadowCollisionType`. Emit `Sphere` in `ShadowShapeBuilder` for `Setup.Spheres`. Add the `Sphere` dispatch branches in `TransitionTypes` so dispatch order matches retail: `HAS_PHYSICS_BSP` clear ⇒ CylSphere then Sphere.
|
||||
|
||||
- [ ] **Step 5: Run conformance + replay harnesses + build**
|
||||
|
||||
Run: `dotnet build`
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~SphereIntersectsSphereConformanceTests|FullyQualifiedName~CellarUp|FullyQualifiedName~CornerFlood"`
|
||||
Expected: build green; all PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.Core/Physics/CollisionPrimitives.cs src/AcDream.Core/Physics/ShadowObjectRegistry.cs src/AcDream.Core/Physics/ShadowShapeBuilder.cs src/AcDream.Core/Physics/TransitionTypes.cs tests/AcDream.Core.Tests/Physics/SphereIntersectsSphereConformanceTests.cs docs/research/2026-06-24-sphere-intersects-pseudocode.md
|
||||
git commit -m "feat(physics): D3 — true Sphere collision primitive (CSphere::intersects_sphere)
|
||||
|
||||
Adds ShadowCollisionType.Sphere + a swept sphere-vs-sphere primitive ported
|
||||
from CSphere::intersects_sphere (pc:276917); ShadowShapeBuilder emits Sphere
|
||||
for Setup.Spheres instead of coercing to a short cylinder. Dispatch order
|
||||
matches retail (HAS_PHYSICS_BSP clear => CylSphere then Sphere). Conformance-
|
||||
tested vs golden values.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3 — `obstruction_ethereal` verbatim port (D2 + D5) — HIGHEST CARE
|
||||
|
||||
**Retail oracle:** `CPhysicsObj::FindObjCollisions@0x0050f050` Gate-1 (pc:276782, instant-skip requires `0x4 AND 0x10`), ETHEREAL-alone branch (pc:276795-276806, sets `obstruction_ethereal=1` and CONTINUES), the per-call clear in `CEnvCell::find_env_collisions@0x0052c144` (pc:309580), and the consume sites in the BSP solid-containment gate (pc:321692 / 323742 / 324573: `if (obstruction_ethereal != 0 || insert_type == PLACEMENT_INSERT)`). **Port set + clear + consume TOGETHER** — a partial port hardens or softens walls unpredictably.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:378-383` (SpherePath "Misc flags") — add `public bool ObstructionEthereal;`.
|
||||
- Modify: `src/AcDream.Core/Physics/CollisionExemption.cs:59-131` — change `ShouldSkip` so ETHEREAL-alone no longer returns `true`; instead the caller sets `SpherePath.ObstructionEthereal=true` and continues. (Instant-skip only when `0x4 AND 0x10`.) Update the call site at `TransitionTypes.cs:2519` to honor the new contract (set the flag, do not `continue` on ETHEREAL-alone).
|
||||
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:2064` (top of `FindEnvCollisions`) — clear `sp.ObstructionEthereal = false;` before any dispatch (matches retail's per-call zero). This is **D5**.
|
||||
- Modify: `src/AcDream.Core/Physics/BSPQuery.cs:1717` — the placement/solid-containment gate currently reads `if (path.InsertType == InsertType.Placement || obj.Ethereal)`; extend it to consume `path.ObstructionEthereal` exactly where retail's `obstruction_ethereal` weakens solid containment (pc:321692/323742/324573).
|
||||
- Test: `tests/AcDream.Core.Tests/Physics/ObstructionEtherealTests.cs` (new); reuse `DoorCollisionApparatusTests`.
|
||||
|
||||
- [ ] **Step 1: Grep the oracle + write pseudocode**
|
||||
|
||||
Grep `FindObjCollisions` Gate-1 + the ETHEREAL-alone branch + `CEnvCell::find_env_collisions` clear + the three BSP consume sites. Write the set/clear/consume contract to `docs/research/2026-06-24-obstruction-ethereal-pseudocode.md`. Confirm: ETHEREAL-only target (ACE door open `0x0001000C`) → `obstruction_ethereal=1`, shape test still runs, solid containment weakened → **passable the retail way** (this subsumes the AD-7 shim; no wire-layer compat is added).
|
||||
|
||||
- [ ] **Step 2: Write the failing tests (door passable on ETHEREAL-alone; wall unchanged)**
|
||||
|
||||
```csharp
|
||||
// tests/AcDream.Core.Tests/Physics/ObstructionEtherealTests.cs
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
public class ObstructionEtherealTests
|
||||
{
|
||||
// Gate-1: instant-skip requires BOTH ETHEREAL (0x4) AND IGNORE_COLLISIONS (0x10).
|
||||
[Fact]
|
||||
public void ShouldSkip_RequiresBothEtherealAndIgnoreCollisions()
|
||||
{
|
||||
// both bits → skip
|
||||
Assert.True(CollisionExemption.ShouldSkip(0x14u, EntityCollisionFlags.None, ObjectInfoState.IsPlayer));
|
||||
// ETHEREAL alone → NOT an instant skip (handled via ObstructionEthereal instead)
|
||||
Assert.False(CollisionExemption.ShouldSkip(0x4u, EntityCollisionFlags.None, ObjectInfoState.IsPlayer));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Plus a `SpherePath`-level test (or extend `DoorCollisionApparatusTests`): an ETHEREAL-only door target sets `ObstructionEthereal` and yields a passable transition; a non-ethereal wall in the same cell still blocks (`CollidedWithEnvironment`).
|
||||
|
||||
- [ ] **Step 3: Run — expect FAIL**
|
||||
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~ObstructionEthereal"`
|
||||
Expected: FAIL (`ShouldSkip(0x4...)` currently returns `true`; `ObstructionEthereal` field missing).
|
||||
|
||||
- [ ] **Step 4: Port set + clear + consume (one slice)**
|
||||
|
||||
Add `SpherePath.ObstructionEthereal`. Change `CollisionExemption.ShouldSkip` (remove the `0x4`-alone early return; keep the `0x4 AND 0x10` instant-skip). At `TransitionTypes.cs:2519`, on ETHEREAL-alone set `sp.ObstructionEthereal = true` and continue to the shape test. Clear `sp.ObstructionEthereal=false` at the top of `FindEnvCollisions`. Consume it in `BSPQuery.cs:1717`'s solid-containment gate per the decomp.
|
||||
|
||||
- [ ] **Step 5: Run tests + replay harnesses + build**
|
||||
|
||||
Run: `dotnet build`
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~ObstructionEthereal|FullyQualifiedName~DoorCollision|FullyQualifiedName~CellarUp|FullyQualifiedName~CornerFlood"`
|
||||
Expected: build green; all PASS — **crucially the CellarUp/CornerFlood wall-collision harnesses unchanged** (the consume site must not weaken normal walls).
|
||||
|
||||
- [ ] **Step 6: Retire AD-7 + commit**
|
||||
|
||||
Delete the **AD-7** row from `docs/architecture/retail-divergence-register.md` (ETHEREAL-alone shim retired).
|
||||
|
||||
```bash
|
||||
git add src/AcDream.Core/Physics/TransitionTypes.cs src/AcDream.Core/Physics/CollisionExemption.cs src/AcDream.Core/Physics/BSPQuery.cs tests/AcDream.Core.Tests/Physics/ObstructionEtherealTests.cs docs/research/2026-06-24-obstruction-ethereal-pseudocode.md docs/architecture/retail-divergence-register.md
|
||||
git commit -m "fix(physics): D2+D5 — verbatim obstruction_ethereal; retire ETHEREAL-alone shim
|
||||
|
||||
Gate-1 instant-skip now requires ETHEREAL(0x4) AND IGNORE_COLLISIONS(0x10)
|
||||
(pc:276782). ETHEREAL-alone sets SpherePath.ObstructionEthereal and continues
|
||||
the shape test (pc:276795-276806); FindEnvCollisions clears it per call
|
||||
(pc:309580); BSPQuery consumes it in the solid-containment gate (pc:321692/
|
||||
323742/324573). ACE's ETHEREAL-only opened doors become passable the retail
|
||||
way, subsuming the AD-7 shim (deleted) with no wire-layer compat. Walls on
|
||||
non-ethereal cells unchanged.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — `RemoveCellsForLandblock` (D8) — the anti-staleness invariant
|
||||
|
||||
**Retail oracle:** the #146 pattern (`PhysicsDataCache.RemoveBuildingsForLandblock`, `:472-478`); retail rebuilds cell transforms from live cell pointers (eviction analog `CEnvCell::release`). Invariant: *every cached collision transform rebases to the current streaming center on each landblock apply.*
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core/Physics/PhysicsDataCache.cs` — add `RemoveCellsForLandblock(uint landblockId)` symmetric with `RemoveBuildingsForLandblock`.
|
||||
- Modify: `src/AcDream.Core/Physics/PhysicsEngine.cs:121-141` (`RemoveLandblock`) — call `DataCache?.RemoveCellsForLandblock(landblockId);`.
|
||||
- Test: `tests/AcDream.Core.Tests/Physics/RemoveCellsForLandblockTests.cs` (new).
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```csharp
|
||||
// tests/AcDream.Core.Tests/Physics/RemoveCellsForLandblockTests.cs
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
public class RemoveCellsForLandblockTests
|
||||
{
|
||||
[Fact]
|
||||
public void RemoveCellsForLandblock_EvictsOnlyMatchingPrefix()
|
||||
{
|
||||
var cache = new PhysicsDataCache();
|
||||
// Register two cell structs via the test seam (one in lb 0xAAAA, one in 0xBBBB).
|
||||
cache.RegisterCellStructForTest(0xAAAA0100u, TestCells.MinimalCellPhysics());
|
||||
cache.RegisterCellStructForTest(0xBBBB0100u, TestCells.MinimalCellPhysics());
|
||||
|
||||
cache.RemoveCellsForLandblock(0xAAAA0000u);
|
||||
|
||||
Assert.Null(cache.GetCellStruct(0xAAAA0100u)); // evicted
|
||||
Assert.NotNull(cache.GetCellStruct(0xBBBB0100u)); // untouched
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(If no `MinimalCellPhysics` helper exists, build a `CellPhysics` with a non-null `BSP.Root` inline or via the existing test fixtures used by `CellarUpTrajectoryReplayTests`.)
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL (method missing)**
|
||||
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~RemoveCellsForLandblock"`
|
||||
Expected: FAIL — `RemoveCellsForLandblock` not defined.
|
||||
|
||||
- [ ] **Step 3: Implement**
|
||||
|
||||
```csharp
|
||||
// PhysicsDataCache.cs — symmetric with RemoveBuildingsForLandblock (:472)
|
||||
/// <summary>
|
||||
/// D8 (2026-06-24): drop every cached cell struct belonging to a landblock so
|
||||
/// the next CacheCellStruct re-bases its STREAMING-RELATIVE WorldTransform
|
||||
/// against the current _liveCenter — symmetric with RemoveBuildingsForLandblock
|
||||
/// (#146). Without this, the _cellStruct first-wins guard LOCKS a dungeon cell's
|
||||
/// transform at its first streaming frame; a teleport recenter then leaves the
|
||||
/// cell BSP at a stale world offset.
|
||||
/// </summary>
|
||||
public void RemoveCellsForLandblock(uint landblockId)
|
||||
{
|
||||
uint prefix = landblockId & 0xFFFF0000u;
|
||||
foreach (var key in _cellStruct.Keys)
|
||||
if ((key & 0xFFFF0000u) == prefix)
|
||||
_cellStruct.TryRemove(key, out _);
|
||||
// Mirror the building-channel symmetry: CellGraph eviction is already done
|
||||
// by PhysicsEngine.RemoveLandblock -> CellGraph.RemoveLandblock.
|
||||
}
|
||||
```
|
||||
|
||||
Then in `PhysicsEngine.RemoveLandblock` add `DataCache?.RemoveCellsForLandblock(landblockId);` alongside the existing clears (near `:124`).
|
||||
|
||||
- [ ] **Step 4: Run test + replay harnesses + build**
|
||||
|
||||
Run: `dotnet build`
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~RemoveCellsForLandblock|FullyQualifiedName~CellarUp"`
|
||||
Expected: build green; all PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/AcDream.Core/Physics/PhysicsDataCache.cs src/AcDream.Core/Physics/PhysicsEngine.cs tests/AcDream.Core.Tests/Physics/RemoveCellsForLandblockTests.cs
|
||||
git commit -m "fix(physics): D8 — RemoveCellsForLandblock; cell transforms rebase per apply
|
||||
|
||||
Symmetric with RemoveBuildingsForLandblock (#146): PhysicsEngine.RemoveLandblock
|
||||
now evicts _cellStruct entries for the landblock so the next CacheCellStruct
|
||||
re-bases the cell BSP WorldTransform against the current streaming center. Closes
|
||||
the #146-class stale-frame asymmetry for indoor cells (D8). Completes the
|
||||
invariant: every cached collision transform rebases on each landblock apply.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5 — entry-restrictions hook + predicate wiring (D4 + W1) — conservative
|
||||
|
||||
**Retail oracle:** `CEnvCell::find_env_collisions@0x0052c130` calls `check_entry_restrictions` FIRST (pc:309576); `CObjCell::check_entry_restrictions@0x0052b6d0` returns COLLIDED when an access-locked cell's `restriction_obj` rejects the mover. Dispatch terms at pc:276861 (`HAS_PHYSICS_BSP_PS AND ebp_1==0 AND missile_ignore==0`).
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs` — add a `check_entry_restrictions`-shaped gate at the front of `FindEnvCollisions` (`:2062`). Inert today (returns OK when the cell has no `restriction_obj`; current dev content has none) but structurally present. (D4)
|
||||
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:669` `BspOnlyDispatch` + the call at `:2633` — wire the real `pvpExempt`/`missile_ignore` terms (currently hardcoded false at `:2629`) so dispatch matches `pc:276861`. Keep behavior identical in M1.5 (no PK/missiles) but remove the hardcoded-false smell. (W1)
|
||||
- Test: extend `tests/AcDream.Core.Tests/Physics/` with a dispatch test pinning the door BSP-only case (must not regress A6.P7) and an entry-restriction no-op test.
|
||||
|
||||
- [ ] **Step 1: Write the failing/guard tests**
|
||||
|
||||
A test that `FindEnvCollisions` returns OK for a cell with no `restriction_obj` (entry-restrictions inert), and a dispatch test that a `HAS_PHYSICS_BSP_PS` door still dispatches BSP-only after the W1 wiring (pin A6.P7 behavior).
|
||||
|
||||
- [ ] **Step 2: Run — expect FAIL or PASS-as-guard**
|
||||
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~EntryRestriction|FullyQualifiedName~BspOnlyDispatch"`
|
||||
|
||||
- [ ] **Step 3: Implement the entry-restrictions gate (inert) + W1 wiring**
|
||||
|
||||
Add the gate; wire `pvpExempt`/`missile_ignore` (still resolve to false in M1.5, but via the real predicate terms, not a hardcoded literal).
|
||||
|
||||
- [ ] **Step 4: Run tests + the FULL Core suite + replay harnesses + build**
|
||||
|
||||
Run: `dotnet build`
|
||||
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj`
|
||||
Expected: build green; full Core suite PASS (no behavior change in dev content).
|
||||
|
||||
- [ ] **Step 5: Register note + commit**
|
||||
|
||||
In `docs/architecture/retail-divergence-register.md`: add a one-line D4 row "entry-restrictions ported but inert until access-locked content" if the gate is a stub, OR no row if fully faithful; remove the W1 "hardcoded false" note if one exists.
|
||||
|
||||
```bash
|
||||
git add src/AcDream.Core/Physics/TransitionTypes.cs tests/AcDream.Core.Tests/Physics/ docs/architecture/retail-divergence-register.md
|
||||
git commit -m "feat(physics): D4+W1 — entry-restrictions gate + wired PvP/missile dispatch terms
|
||||
|
||||
Adds CObjCell::check_entry_restrictions gate at the front of FindEnvCollisions
|
||||
(pc:309576), inert until access-locked content. Wires the real pvpExempt/
|
||||
missile_ignore dispatch terms (pc:276861) replacing the hardcoded-false at
|
||||
TransitionTypes.cs:2629 — behavior identical in M1.5, smell removed.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final — batched visual-gate handoff (STOP here, hand to user)
|
||||
|
||||
After all five tasks land green, run the full suites once more and hand off the three feel-gates for the user to test in one session:
|
||||
|
||||
Run: `dotnet build && dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj`
|
||||
Expected: build + full Core suite green.
|
||||
|
||||
Then present the launch command (CLAUDE.md "Running the client") and the three things to check:
|
||||
1. **Slice 1 (scenery feel):** walk Holtburg + open world — objects that became passable must be exactly those retail walks through (no DAT physics shape); real statics/walls/buildings still solid.
|
||||
2. **Slice 3 (doors):** open a door (ACE), walk through (passable), close it, walk into it (solid); confirm no house wall went soft.
|
||||
3. **Slice 4 (dungeon re-entry):** teleport into a dungeon → out → back into the same dungeon; indoor walls collide correctly each time (no stale-offset clip).
|
||||
|
||||
Update `docs/ISSUES.md` / the physics digest with the phase outcome after the user's gate passes.
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes (filled by plan author)
|
||||
|
||||
- **Spec coverage:** D1→T1, D3→T2, D2+D5→T3, D8→T4, D4+W1→T5. D6/D7 refuted (no task). All 6 confirmed deviations + W1 covered. ✓
|
||||
- **Type consistency:** `ShadowCollisionType { BSP, Cylinder, Sphere }` (T2) used consistently; `SpherePath.ObstructionEthereal` (T3) set in CollisionExemption call site, cleared in FindEnvCollisions, consumed in BSPQuery — same name throughout; `RemoveCellsForLandblock` (T4) named identically at definition + call site. ✓
|
||||
- **Sequencing:** T1→T2 (Sphere extends the now-sole authority), T2→T3 (predicate completion after shapes), T3→T4 (independent), T5 last (conservative). Serial — shared files (ShadowShapeBuilder, TransitionTypes, SpherePath, BSPQuery). Do NOT parallelize. ✓
|
||||
- **Port-sensitive steps** (sphere math T2; obstruction_ethereal consume T3) are oracle-driven + conformance-tested, not pre-written — the DO-NOT-RETRY no-guess rule. The implementer derives the exact C# from the decomp. ✓
|
||||
Loading…
Add table
Add a link
Reference in a new issue