T6 (BR-7) C4: straddle-only outside-add (A6.P5 widening DELETED) + #90 stickiness removed
The two remaining flagged workarounds retired, per the BR-7 plan +
the WF1 [MEDIUM] correction (re-gate, do NOT delete the outside-add):
1. A6.P5 hasExitPortal topology widening DELETED. Outdoor cells enter the
collision cell array ONLY on the retail straddle gate - |dist| <
radius + F_EPSILON against an exterior portal plane
(CEnvCell::find_transit_cells Ghidra 0x0052c820, gate 0052c9d6,
live-binary verified) - the same flag that already gated the
membership pick (#112 rider). The widening existed so outdoor-
registered doors stayed findable from indoor cells under the old flat
registry query; with per-cell shadow lists the door is found in the
straddle-admitted outdoor cell's own list (tick-13558 pin holds).
The hasExitPortal out-param + plumbing deleted from
FindTransitCellsSphere; the AddAllOutsideCells call in
BuildCellSetAndPickContaining re-gated on exitOutsideStraddle
(once-per-walk = retail CELLARRAY.added_outside).
2. #90 ResolveCellId sphere-overlap stickiness REMOVED (the 4ca3596
workaround, deferred-to-A6.P4 in the physics digest). It was dead
code: the method's only caller is FindEnvCollisions' cache-null TEST
fallback, and the indoor branch (where the stickiness lived) required
a non-null DataCache. Production membership flows exclusively through
the collide-then-pick advance whose ordered-array hysteresis (current
cell at index 0, interior-wins-break) is the retail mechanism the
workaround approximated. ResolveCellId reduced to the bare
prefix-preserving outdoor re-derive, documented test-only.
Test updates (pins of the deleted behaviors inverted to retail):
- A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell (asserted the
topology widening verbatim) -> DeepInteriorSphere_NoStraddle_
AddsNoOutdoorCells: a deep-interior sphere admits NO outdoor cells.
- A6P5_BuildCellSetFromAlcove... -> AlcoveSphere_StraddlesExitPortal_
ReachesDoorOutdoorCell (the captured alcove position genuinely
straddles - the retail-positive half).
- Issue112MembershipTests straddle pin + the second-sphere straddle test
updated to the single-flag signature.
Suites: Core 1416/0/2, App 225, UI 420, Net 294 - green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
dbfbf8506c
commit
ca4b482f8b
5 changed files with 101 additions and 205 deletions
|
|
@ -58,20 +58,6 @@ public static class CellTransit
|
||||||
float sphereRadius,
|
float sphereRadius,
|
||||||
ICollection<uint> candidates,
|
ICollection<uint> candidates,
|
||||||
out bool exitOutside)
|
out bool exitOutside)
|
||||||
=> FindTransitCellsSphere(
|
|
||||||
cache, currentCell, currentCellId, worldSphereCenter, sphereRadius,
|
|
||||||
candidates, out exitOutside, out _);
|
|
||||||
|
|
||||||
/// <inheritdoc cref="FindTransitCellsSphere(PhysicsDataCache, CellPhysics, uint, IReadOnlyList{Sphere}, int, ICollection{uint}, out bool, out bool)"/>
|
|
||||||
public static void FindTransitCellsSphere(
|
|
||||||
PhysicsDataCache cache,
|
|
||||||
CellPhysics currentCell,
|
|
||||||
uint currentCellId,
|
|
||||||
Vector3 worldSphereCenter,
|
|
||||||
float sphereRadius,
|
|
||||||
ICollection<uint> candidates,
|
|
||||||
out bool exitOutside,
|
|
||||||
out bool hasExitPortal)
|
|
||||||
{
|
{
|
||||||
var spheres = new[]
|
var spheres = new[]
|
||||||
{
|
{
|
||||||
|
|
@ -84,7 +70,7 @@ public static class CellTransit
|
||||||
|
|
||||||
FindTransitCellsSphere(
|
FindTransitCellsSphere(
|
||||||
cache, currentCell, currentCellId,
|
cache, currentCell, currentCellId,
|
||||||
spheres, spheres.Length, candidates, out exitOutside, out hasExitPortal);
|
spheres, spheres.Length, candidates, out exitOutside);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -98,12 +84,9 @@ public static class CellTransit
|
||||||
/// This is the only condition under which retail's
|
/// This is the only condition under which retail's
|
||||||
/// <c>CEnvCell::find_transit_cells</c> calls <c>add_all_outside_cells</c>
|
/// <c>CEnvCell::find_transit_cells</c> calls <c>add_all_outside_cells</c>
|
||||||
/// (acclient.exe 0052c8e5-0052c92d straddle test, 0052c9d2-0052c9f0 gate;
|
/// (acclient.exe 0052c8e5-0052c92d straddle test, 0052c9d2-0052c9f0 gate;
|
||||||
/// pseudo-C :310070-310120). Drives the membership pick's outdoor branch.</param>
|
/// pseudo-C :310070-310120). Drives the membership pick's outdoor branch
|
||||||
/// <param name="hasExitPortal">Topology-only: this cell has at least one
|
/// AND (BR-7 C4, retail-faithfully) the collision cell-set outside-add —
|
||||||
/// exterior (0xFFFF) portal, regardless of sphere position. NOT retail —
|
/// the former <c>hasExitPortal</c> topology widening is deleted.</param>
|
||||||
/// kept for the A6.P5 collision cell-set widening (outdoor-registered door
|
|
||||||
/// entities must stay findable from indoor cells until the A6.P4 per-cell
|
|
||||||
/// shadow architecture ships; see the exterior-portal branch comment).</param>
|
|
||||||
public static void FindTransitCellsSphere(
|
public static void FindTransitCellsSphere(
|
||||||
PhysicsDataCache cache,
|
PhysicsDataCache cache,
|
||||||
CellPhysics currentCell,
|
CellPhysics currentCell,
|
||||||
|
|
@ -111,11 +94,9 @@ public static class CellTransit
|
||||||
IReadOnlyList<Sphere> worldSpheres,
|
IReadOnlyList<Sphere> worldSpheres,
|
||||||
int numSpheres,
|
int numSpheres,
|
||||||
ICollection<uint> candidates,
|
ICollection<uint> candidates,
|
||||||
out bool exitOutside,
|
out bool exitOutside)
|
||||||
out bool hasExitPortal)
|
|
||||||
{
|
{
|
||||||
exitOutside = false;
|
exitOutside = false;
|
||||||
hasExitPortal = false;
|
|
||||||
|
|
||||||
uint lbPrefix = currentCellId & 0xFFFF0000u;
|
uint lbPrefix = currentCellId & 0xFFFF0000u;
|
||||||
int sphereCount = EffectiveSphereCount(worldSpheres, numSpheres);
|
int sphereCount = EffectiveSphereCount(worldSpheres, numSpheres);
|
||||||
|
|
@ -129,8 +110,6 @@ public static class CellTransit
|
||||||
|
|
||||||
if (portal.OtherCellId == 0xFFFF)
|
if (portal.OtherCellId == 0xFFFF)
|
||||||
{
|
{
|
||||||
hasExitPortal = true;
|
|
||||||
|
|
||||||
// #112 rider (2026-06-10): retail straddle gate, RESTORED and
|
// #112 rider (2026-06-10): retail straddle gate, RESTORED and
|
||||||
// verified against the LIVE 2013 binary (cdb attach, function
|
// verified against the LIVE 2013 binary (cdb attach, function
|
||||||
// 0052c820; x87 decode at 0052c8e5-0052c92d):
|
// 0052c820; x87 decode at 0052c8e5-0052c92d):
|
||||||
|
|
@ -144,19 +123,17 @@ public static class CellTransit
|
||||||
// portal_side / exact_match in this branch — BN's pseudo-C
|
// portal_side / exact_match in this branch — BN's pseudo-C
|
||||||
// invented those (feedback_bn_decomp_field_names).
|
// invented those (feedback_bn_decomp_field_names).
|
||||||
//
|
//
|
||||||
// History: this gate existed pre-A6.P5 and was removed
|
// History: this gate existed pre-A6.P5, was removed 2026-05-25
|
||||||
// 2026-05-25 citing the CALLER (find_cell_list :308775-:308785
|
// citing the CALLER (find_cell_list :308775-:308785 walks every
|
||||||
// walks every array cell unconditionally — true, but each
|
// array cell unconditionally — true, but each callee still
|
||||||
// callee still applies its own straddle gate). The A6.P5
|
// applies its own straddle gate), and was restored for the
|
||||||
// symptom it fixed (outdoor-registered cottage DOORS invisible
|
// membership PICK by the #112 rider. BR-7 / A6.P4 C4
|
||||||
// to collision from indoor cells) is really the missing
|
// (2026-06-11) finished the story: the per-cell shadow
|
||||||
// per-cell shadow_object_list (#99/A6.P4): retail finds those
|
// architecture made the A6.P5 hasExitPortal topology widening
|
||||||
// doors via shadow lists, not via outdoor transit cells. Until
|
// unnecessary (doors are found in the straddle-admitted outdoor
|
||||||
// A6.P4 ships, BuildCellSetAndPickContaining keeps widening the
|
// cell's own list), so this flag now gates BOTH the pick's
|
||||||
// COLLISION cell set on hasExitPortal — but the membership PICK
|
// outdoor branch AND the collision cell-set outside-add —
|
||||||
// gates its outdoor branch on this retail flag, which is what
|
// pure retail.
|
||||||
// keeps deep-interior containment gaps on curr_cell (retail
|
|
||||||
// keep-curr) instead of demoting to outdoor (#112).
|
|
||||||
if (!exitOutside)
|
if (!exitOutside)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < sphereCount; i++)
|
for (int i = 0; i < sphereCount; i++)
|
||||||
|
|
@ -540,7 +517,7 @@ public static class CellTransit
|
||||||
|
|
||||||
FindTransitCellsSphere(
|
FindTransitCellsSphere(
|
||||||
cache, cell, cellId, worldSpheres, sphereCount,
|
cache, cell, cellId, worldSpheres, sphereCount,
|
||||||
candidates, out bool exitStraddle, out _);
|
candidates, out bool exitStraddle);
|
||||||
|
|
||||||
if (exitStraddle && !outdoorAdded)
|
if (exitStraddle && !outdoorAdded)
|
||||||
{
|
{
|
||||||
|
|
@ -787,7 +764,7 @@ public static class CellTransit
|
||||||
|
|
||||||
FindTransitCellsSphere(
|
FindTransitCellsSphere(
|
||||||
cache, cell, cellId, worldSpheres, sphereCount,
|
cache, cell, cellId, worldSpheres, sphereCount,
|
||||||
candidates, out bool exitOutsideStraddle, out bool hasExitPortal);
|
candidates, out bool exitOutsideStraddle);
|
||||||
|
|
||||||
// #112 rider (2026-06-10): the retail straddle flag (live-binary
|
// #112 rider (2026-06-10): the retail straddle flag (live-binary
|
||||||
// verified — see FindTransitCellsSphere) gates the PICK's outdoor
|
// verified — see FindTransitCellsSphere) gates the PICK's outdoor
|
||||||
|
|
@ -795,16 +772,21 @@ public static class CellTransit
|
||||||
// when a path sphere straddles an exterior portal plane.
|
// when a path sphere straddles an exterior portal plane.
|
||||||
outdoorPickAllowed |= exitOutsideStraddle;
|
outdoorPickAllowed |= exitOutsideStraddle;
|
||||||
|
|
||||||
// A6.P5 (kept, NARROWED to the collision cell SET): the first
|
// BR-7 / A6.P4 C4 (2026-06-11): outdoor cells enter the array
|
||||||
// exit-portal cell triggers the outdoor neighbourhood add once, by
|
// on the retail STRADDLE gate — |dist| < radius + F_EPSILON
|
||||||
// TOPOLOGY — wider than retail. This keeps outdoor-registered door
|
// against an exterior portal plane (CEnvCell::find_transit_cells
|
||||||
// entities findable from indoor cells (the 2026-05-25 door capture)
|
// 0x0052c820; gate at 0052c9d6) — replacing the A6.P5
|
||||||
// until #99/A6.P4 ships per-cell shadow lists; the pick no longer
|
// hasExitPortal TOPOLOGY widening. The widening existed to keep
|
||||||
// consumes these cells unless the retail flag fired, so membership
|
// outdoor-registered doors findable from indoor cells under the
|
||||||
// matches retail in both regimes. Appended AFTER the interior cells,
|
// old flat query; with per-cell shadow lists the door is found
|
||||||
// matching retail order (add_all_outside_cells at the end,
|
// in the straddle-admitted outdoor cell's OWN list (and the
|
||||||
// pseudo_c:310120) — interior-wins is preserved.
|
// straddle fires whenever a sphere is genuinely at the
|
||||||
if (hasExitPortal && !outdoorAdded)
|
// threshold — the tick-13558 door pin proves the admission).
|
||||||
|
// Appended AFTER the interior cells, matching retail order
|
||||||
|
// (add_all_outside_cells at the end, pseudo_c:310120) —
|
||||||
|
// interior-wins is preserved. Once-per-walk via outdoorAdded =
|
||||||
|
// retail CELLARRAY.added_outside (0x00533630).
|
||||||
|
if (exitOutsideStraddle && !outdoorAdded)
|
||||||
{
|
{
|
||||||
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, blockOrigin, candidates);
|
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, blockOrigin, candidates);
|
||||||
outdoorAdded = true;
|
outdoorAdded = true;
|
||||||
|
|
|
||||||
|
|
@ -290,84 +290,38 @@ public sealed class PhysicsEngine
|
||||||
cg.CurrCell = cell;
|
cg.CurrCell = cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TEST-ONLY outdoor cell re-derive. The single caller is
|
||||||
|
/// <c>Transition.FindEnvCollisions</c>'s cache-null fallback
|
||||||
|
/// (PhysicsEngineTests run engines without a <see cref="DataCache"/>,
|
||||||
|
/// so <see cref="CellTransit.FindCellSet"/> is unavailable). Production
|
||||||
|
/// membership flows exclusively through the collide-then-pick advance
|
||||||
|
/// (<c>RunCheckOtherCellsAndAdvance</c> → <c>FindCellSet</c>).
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// BR-7 / A6.P4 C4 (2026-06-11): the former indoor branch — including
|
||||||
|
/// the #90 sphere-overlap stickiness workaround (4ca3596) and the
|
||||||
|
/// building-transit promotion — was DEAD CODE on this path (it required
|
||||||
|
/// a non-null DataCache; the only caller guarantees null) and is
|
||||||
|
/// removed. #90's doorway ping-pong concern is owned by the retail
|
||||||
|
/// ordered-pick hysteresis (current cell at array index 0,
|
||||||
|
/// interior-wins-break; CellTransit.BuildCellSetAndPickContaining) —
|
||||||
|
/// the workaround is retired, closing the digest's deferred-removal
|
||||||
|
/// item.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>Preserves the L.2e prefix-preservation fix (always apply the
|
||||||
|
/// matched landblock's high-16 prefix even when
|
||||||
|
/// <paramref name="fallbackCellId"/> arrived bare-low-byte).</para>
|
||||||
|
/// </summary>
|
||||||
internal uint ResolveCellId(Vector3 worldPos, float sphereRadius, uint fallbackCellId)
|
internal uint ResolveCellId(Vector3 worldPos, float sphereRadius, uint fallbackCellId)
|
||||||
{
|
{
|
||||||
if (fallbackCellId == 0) return 0;
|
if (fallbackCellId == 0) return 0;
|
||||||
|
|
||||||
uint fallbackLow = fallbackCellId & 0xFFFFu;
|
// Indoor fallback ids pass through unchanged — identical to the old
|
||||||
|
// dead path's `DataCache is null → return fallbackCellId` outcome.
|
||||||
|
if ((fallbackCellId & 0xFFFFu) >= 0x0100u) return fallbackCellId;
|
||||||
|
|
||||||
if (fallbackLow >= 0x0100u)
|
|
||||||
{
|
|
||||||
// Indoor branch needs DataCache to look up cells; outdoor uses
|
|
||||||
// _landblocks (no DataCache dependency).
|
|
||||||
if (DataCache is null) return fallbackCellId;
|
|
||||||
|
|
||||||
// ── Cell-stickiness REVERTED (A6.P3 slice 3 v3, 2026-05-22) ──
|
|
||||||
// Slice 3 v1 (sphere-overlap, 8898166) over-corrected — held
|
|
||||||
// player in cellar even when transitioning out at the ramp top.
|
|
||||||
// Slice 3 v2 (point-in, 3e140cf) closed the ping-pong at the
|
|
||||||
// inn doorway (data confirmed) BUT prevented the player from
|
|
||||||
// reaching the top of the cellar ramp (the stuck spot
|
|
||||||
// transitioned from "ping-pong at top" to "never reach top").
|
|
||||||
//
|
|
||||||
// Reverting to no-stickiness for now. The ping-pong at the inn
|
|
||||||
// doorway returns but is a lesser evil than blocking cellar-up
|
|
||||||
// entirely. Issue #98 cellar-up has a deeper bug that needs
|
|
||||||
// separate investigation (BSP step-physics or AdjustOffset
|
|
||||||
// slope-projection at the cottage main floor boundary).
|
|
||||||
//
|
|
||||||
// Slice 3 work remains valuable as research evidence; the fix
|
|
||||||
// shape was wrong. Issue #90 stays as workaround until a
|
|
||||||
// better stickiness mechanism is designed (probably needs to
|
|
||||||
// be GATED by some "near cell boundary" check rather than
|
|
||||||
// applied unconditionally).
|
|
||||||
|
|
||||||
// Fallback cell no longer valid → re-resolve via portal-graph BFS.
|
|
||||||
uint indoorResult = CellTransit.FindCellList(DataCache, worldPos, sphereRadius, fallbackCellId);
|
|
||||||
|
|
||||||
// ISSUES #83 / Phase A1.7 (2026-05-21): verify the indoor result
|
|
||||||
// actually contains the player. CellTransit.FindCellList falls back
|
|
||||||
// to currentCellId when no candidate cell's CellBSP contains the
|
|
||||||
// sphere center — but this happens even when the player has walked
|
|
||||||
// OUTSIDE the entire portal-connected indoor cell graph (e.g.,
|
|
||||||
// exited through an unblocked wall or doorway gap). In that state
|
|
||||||
// the player's CellId is stuck on an indoor cell whose BSP is
|
|
||||||
// far away, every indoor-bsp query returns OK (NodeIntersects
|
|
||||||
// fails at root), and no walls block.
|
|
||||||
//
|
|
||||||
// If the resolved indoor cell's BSP does NOT contain the sphere
|
|
||||||
// center, fall through to the outdoor cell resolution below — it
|
|
||||||
// will compute the correct landcell from the terrain grid and
|
|
||||||
// optionally re-enter an indoor cell via CheckBuildingTransit.
|
|
||||||
var indoorCell = DataCache.GetCellStruct(indoorResult);
|
|
||||||
if (indoorCell?.CellBSP?.Root is null)
|
|
||||||
return indoorResult; // render root (CurrCell) set by the player's UpdateCellId // Can't verify (no CellBSP); trust FindCellList.
|
|
||||||
|
|
||||||
// Issue #90 fix (2026-05-20): use SPHERE-overlap instead of POINT-in
|
|
||||||
// for the indoor verification. The previous point-only check caused
|
|
||||||
// a per-frame ping-pong at the inn doorway: indoor BSP push-back
|
|
||||||
// moved the sphere CENTER a few cm outside the indoor CellBSP
|
|
||||||
// volume → point-only check returned false → fell through to outdoor
|
|
||||||
// → next tick re-promoted to indoor → wall hit → push-back →
|
|
||||||
// outdoor → repeat. Net visual behavior: "walls walk through"
|
|
||||||
// because outdoor ticks bypass indoor BSP entirely. With sphere-
|
|
||||||
// overlap, the player stays classified indoor as long as ANY part
|
|
||||||
// of the foot sphere still overlaps the indoor cell volume.
|
|
||||||
//
|
|
||||||
// Retail oracle: CCellStruct::sphere_intersects_cell at
|
|
||||||
// acclient_2013_pseudo_c.txt:317666 →
|
|
||||||
// BSPTREE::sphere_intersects_cell_bsp at :323267.
|
|
||||||
var localCenter = Vector3.Transform(worldPos, indoorCell.InverseWorldTransform);
|
|
||||||
if (BSPQuery.SphereIntersectsCellBsp(indoorCell.CellBSP.Root, localCenter, sphereRadius))
|
|
||||||
return indoorResult; // render root (CurrCell) set by the player's UpdateCellId
|
|
||||||
|
|
||||||
// Fall through to outdoor resolution: player has FULLY left the
|
|
||||||
// indoor portal-connected graph (sphere no longer overlaps).
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outdoor seed: use terrain grid to compute the prefixed cell id.
|
|
||||||
// Preserves the L.2e prefix-preservation fix (always apply the matched
|
|
||||||
// landblock's high-16 prefix even when fallbackCellId arrived bare-low-byte).
|
|
||||||
foreach (var kvp in _landblocks)
|
foreach (var kvp in _landblocks)
|
||||||
{
|
{
|
||||||
var lb = kvp.Value;
|
var lb = kvp.Value;
|
||||||
|
|
@ -376,29 +330,7 @@ public sealed class PhysicsEngine
|
||||||
if (localX >= 0f && localX < 192f && localY >= 0f && localY < 192f)
|
if (localX >= 0f && localX < 192f && localY >= 0f && localY < 192f)
|
||||||
{
|
{
|
||||||
uint lowCellId = lb.Terrain.ComputeOutdoorCellId(localX, localY);
|
uint lowCellId = lb.Terrain.ComputeOutdoorCellId(localX, localY);
|
||||||
uint outdoorCellId = (kvp.Key & 0xFFFF0000u) | lowCellId;
|
return (kvp.Key & 0xFFFF0000u) | lowCellId;
|
||||||
|
|
||||||
// Outdoor→indoor entry: if this landcell has a cached building,
|
|
||||||
// check whether the sphere has crossed into one of its interior
|
|
||||||
// EnvCells via the building's portals.
|
|
||||||
if (DataCache is not null)
|
|
||||||
{
|
|
||||||
var building = DataCache.GetBuilding(outdoorCellId);
|
|
||||||
if (building is not null)
|
|
||||||
{
|
|
||||||
var candidates = new System.Collections.Generic.HashSet<uint>();
|
|
||||||
CellTransit.CheckBuildingTransit(
|
|
||||||
DataCache, building, worldPos, sphereRadius, candidates);
|
|
||||||
if (candidates.Count > 0)
|
|
||||||
{
|
|
||||||
// First candidate wins — building portal containment is
|
|
||||||
// mutually exclusive in retail (one interior cell per portal).
|
|
||||||
foreach (var c in candidates) return c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return outdoorCellId; // render root (CurrCell) set by the player's UpdateCellId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,21 +101,22 @@ public sealed class Issue112MembershipTests
|
||||||
var cell102 = cache.GetCellStruct(0xA9B30102u)!;
|
var cell102 = cache.GetCellStruct(0xA9B30102u)!;
|
||||||
|
|
||||||
// (a) Deep inside the room, 3 m from the door plane: the cell HAS an
|
// (a) Deep inside the room, 3 m from the door plane: the cell HAS an
|
||||||
// exterior portal (topology flag) but no straddle → no outdoor
|
// exterior portal but no straddle → no outdoor admission flag
|
||||||
// admission flag (retail: var_44 stays 0, add_all_outside skipped).
|
// (retail: var_44 stays 0, add_all_outside skipped). (BR-7 C4
|
||||||
|
// deleted the non-retail hasExitPortal topology output — the
|
||||||
|
// straddle flag is the only outdoor-admission signal, like
|
||||||
|
// retail.)
|
||||||
var farCandidates = new List<uint>();
|
var farCandidates = new List<uint>();
|
||||||
CellTransit.FindTransitCellsSphere(
|
CellTransit.FindTransitCellsSphere(
|
||||||
cache, cell102, 0xA9B30102u, new Vector3(183.0f, 86.5f, 117.0f),
|
cache, cell102, 0xA9B30102u, new Vector3(183.0f, 86.5f, 117.0f),
|
||||||
FootRadius, farCandidates, out bool farStraddle, out bool farHasExit);
|
FootRadius, farCandidates, out bool farStraddle);
|
||||||
Assert.True(farHasExit);
|
|
||||||
Assert.False(farStraddle);
|
Assert.False(farStraddle);
|
||||||
|
|
||||||
// (b) At the door plane (0.30 m away < 0.48 radius): straddle fires.
|
// (b) At the door plane (0.30 m away < 0.48 radius): straddle fires.
|
||||||
var nearCandidates = new List<uint>();
|
var nearCandidates = new List<uint>();
|
||||||
CellTransit.FindTransitCellsSphere(
|
CellTransit.FindTransitCellsSphere(
|
||||||
cache, cell102, 0xA9B30102u, new Vector3(185.70f, 85.5f, 117.0f),
|
cache, cell102, 0xA9B30102u, new Vector3(185.70f, 85.5f, 117.0f),
|
||||||
FootRadius, nearCandidates, out bool nearStraddle, out bool nearHasExit);
|
FootRadius, nearCandidates, out bool nearStraddle);
|
||||||
Assert.True(nearHasExit);
|
|
||||||
Assert.True(nearStraddle);
|
Assert.True(nearStraddle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ public class CellTransitFindTransitCellsSphereTests
|
||||||
|
|
||||||
CellTransit.FindTransitCellsSphere(
|
CellTransit.FindTransitCellsSphere(
|
||||||
cache, exitCell, currentCellId: 0xA9B40100u,
|
cache, exitCell, currentCellId: 0xA9B40100u,
|
||||||
spheres, spheres.Length, candidates, out bool exitOutside, out _);
|
spheres, spheres.Length, candidates, out bool exitOutside);
|
||||||
|
|
||||||
Assert.True(exitOutside);
|
Assert.True(exitOutside);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,37 +16,29 @@ namespace AcDream.Core.Tests.Physics;
|
||||||
/// Tests for <see cref="CellTransit"/> portal-graph expansion.
|
/// Tests for <see cref="CellTransit"/> portal-graph expansion.
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// A6.P5 (2026-05-25) — unit-tests the cellSet build for the cottage door
|
/// History: A6.P5 (2026-05-25) widened the collision cell set on exit-
|
||||||
/// scenario, where pre-fix the <c>exitOutside</c> gate produced over-
|
/// portal TOPOLOGY (outdoor cells added whenever an overlapped cell had a
|
||||||
/// penetration ticks (sphere advanced 0.27 m INTO the door slab because
|
/// 0xFFFF portal) so outdoor-registered doors stayed findable from indoor
|
||||||
/// the door's outdoor cell wasn't in the cellSet during a cell-crossing
|
/// cells under the flat registry query. BR-7 / A6.P4 C4 (2026-06-11)
|
||||||
/// substep).
|
/// removed the widening: outdoor cells enter the array only on the retail
|
||||||
/// </para>
|
/// STRADDLE gate (|dist| < radius + F_EPSILON vs the exterior portal
|
||||||
///
|
/// plane — CEnvCell::find_transit_cells, Ghidra 0x0052c820, live-binary
|
||||||
/// <para>
|
/// verified), and doors are found per-cell via registration-time
|
||||||
/// Loads cells <c>0xA9B4013F</c> (player's starting indoor cell at the
|
/// membership instead. These tests pin BOTH halves of the retail
|
||||||
/// over-penetration tick) and <c>0xA9B40150</c> (alcove cell adjacent to
|
/// semantics on real Holtburg dat geometry.
|
||||||
/// the doorway) from the real dat. Asserts that BFS expansion from
|
|
||||||
/// <c>0xA9B4013F</c> reaches the door's outdoor cell <c>0xA9B40029</c>
|
|
||||||
/// via the portal chain. The fix makes this hold unconditionally;
|
|
||||||
/// pre-fix it only held when the sphere physically straddled an exit
|
|
||||||
/// portal.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CellTransitTests
|
public class CellTransitTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell()
|
public void DeepInteriorSphere_NoStraddle_AddsNoOutdoorCells()
|
||||||
{
|
{
|
||||||
var datDir = ResolveDatDir();
|
var datDir = ResolveDatDir();
|
||||||
if (datDir is null) return;
|
if (datDir is null) return;
|
||||||
|
|
||||||
// Hydrate the cells in the portal chain.
|
// Hydrate the cells in the portal chain.
|
||||||
// 0xA9B4013F — start cell at the over-penetration tick
|
// 0xA9B4013F — deep interior cell (the old over-penetration tick)
|
||||||
// 0xA9B40150 — alcove cell adjacent to doorway (has exit portal)
|
// 0xA9B40150 — alcove cell adjacent to the doorway (exit portal)
|
||||||
// The door lives in outdoor cell 0xA9B40029 (registered as a
|
|
||||||
// shadow entry on the landblock — not loaded here; this test
|
|
||||||
// only asserts the cellSet membership, not collision).
|
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
HydrateCell(cache, datDir, 0xA9B4013Fu);
|
HydrateCell(cache, datDir, 0xA9B4013Fu);
|
||||||
HydrateCell(cache, datDir, 0xA9B40150u);
|
HydrateCell(cache, datDir, 0xA9B40150u);
|
||||||
|
|
@ -54,16 +46,15 @@ public class CellTransitTests
|
||||||
Assert.NotNull(cache.GetCellStruct(0xA9B4013Fu));
|
Assert.NotNull(cache.GetCellStruct(0xA9B4013Fu));
|
||||||
Assert.NotNull(cache.GetCellStruct(0xA9B40150u));
|
Assert.NotNull(cache.GetCellStruct(0xA9B40150u));
|
||||||
|
|
||||||
// Sphere at the over-penetration tick start position: world
|
// Sphere deep in 0x13F's volume — no path sphere straddles
|
||||||
// (132.594, 16.350, 94.48 sphere center). Foot Z = 94, sphere
|
// 0x150's exit portal plane from here (the A6.P5 pin's own
|
||||||
// radius 0.48 → center Z = 94 + radius? Actually the live
|
// message documented exactly that). Retail adds NO outdoor cells
|
||||||
// capture's sphere center is at Z = 94.48 since foot Z = 94
|
// in this state; the door (outdoor 0xA9B40029) is reached only
|
||||||
// and sphere is foot-anchored. For this test the Z value
|
// when the sphere actually straddles the threshold — pinned by
|
||||||
// doesn't matter — only XY portal membership.
|
// the alcove test below and the tick-13558 door pin.
|
||||||
var sphereWorld = new Vector3(132.5935f, 16.350428f, 94.48f);
|
var sphereWorld = new Vector3(132.5935f, 16.350428f, 94.48f);
|
||||||
const float sphereRadius = 0.48f;
|
const float sphereRadius = 0.48f;
|
||||||
const uint startCellId = 0xA9B4013Fu;
|
const uint startCellId = 0xA9B4013Fu;
|
||||||
const uint doorOutdoorCell = 0xA9B40029u;
|
|
||||||
|
|
||||||
_ = CellTransit.FindCellSet(
|
_ = CellTransit.FindCellSet(
|
||||||
cache,
|
cache,
|
||||||
|
|
@ -72,37 +63,25 @@ public class CellTransitTests
|
||||||
startCellId,
|
startCellId,
|
||||||
out var cellSet);
|
out var cellSet);
|
||||||
|
|
||||||
Assert.True(
|
Assert.DoesNotContain(cellSet, c => (c & 0xFFFFu) < 0x0100u);
|
||||||
cellSet.Contains(doorOutdoorCell),
|
Assert.Contains(startCellId, cellSet);
|
||||||
$"A6.P5: BFS portal expansion from indoor start cell " +
|
|
||||||
$"0x{startCellId:X8} (sphere world XY = " +
|
|
||||||
$"({sphereWorld.X:F3}, {sphereWorld.Y:F3})) should include " +
|
|
||||||
$"the door's outdoor cell 0x{doorOutdoorCell:X8} via the " +
|
|
||||||
$"portal chain 0x{startCellId:X8} → 0xA9B40150 → outdoor. " +
|
|
||||||
$"Pre-fix: the exitOutside gate required the sphere to " +
|
|
||||||
$"straddle 0xA9B40150's exit portal — sphere is in " +
|
|
||||||
$"0x{startCellId:X8}'s volume, so the gate didn't fire and " +
|
|
||||||
$"the door's cell wasn't added. Post-fix: gate removed; " +
|
|
||||||
$"exit-portal topology adds outdoor cells unconditionally. " +
|
|
||||||
$"Actual cellSet: {string.Join(",", cellSet.Select(c => $"0x{c:X8}"))}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void A6P5_BuildCellSetFromAlcove_AlsoReachesDoorOutdoorCell()
|
public void AlcoveSphere_StraddlesExitPortal_ReachesDoorOutdoorCell()
|
||||||
{
|
{
|
||||||
var datDir = ResolveDatDir();
|
var datDir = ResolveDatDir();
|
||||||
if (datDir is null) return;
|
if (datDir is null) return;
|
||||||
|
|
||||||
// The alcove cell 0xA9B40150 IS the cell with the exit portal.
|
// The alcove cell 0xA9B40150 IS the cell with the exit portal, and
|
||||||
// Pre-fix this worked SOMETIMES (when the sphere straddled the
|
// at this captured position the foot sphere straddles its plane
|
||||||
// portal). Post-fix it works ALWAYS. This is a regression guard
|
// (|dist| < 0.48 + ε — same geometry the tick-13558 straddle pin
|
||||||
// for the previously-sometimes-working case.
|
// proves). Retail admits the outdoor cells exactly here, which is
|
||||||
|
// how the indoor-side sphere reaches the outdoor-registered door's
|
||||||
|
// per-cell shadow list.
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
HydrateCell(cache, datDir, 0xA9B40150u);
|
HydrateCell(cache, datDir, 0xA9B40150u);
|
||||||
|
|
||||||
// Sphere at the stuck position — sphere center is NOT at the
|
|
||||||
// exit portal plane (sphere is in the alcove volume, away from
|
|
||||||
// the doorway threshold geometrically).
|
|
||||||
var sphereWorld = new Vector3(132.4014f, 16.761757f, 94.48f);
|
var sphereWorld = new Vector3(132.4014f, 16.761757f, 94.48f);
|
||||||
const float sphereRadius = 0.48f;
|
const float sphereRadius = 0.48f;
|
||||||
|
|
||||||
|
|
@ -115,8 +94,10 @@ public class CellTransitTests
|
||||||
|
|
||||||
Assert.True(
|
Assert.True(
|
||||||
cellSet.Contains(0xA9B40029u),
|
cellSet.Contains(0xA9B40029u),
|
||||||
$"A6.P5: BFS from alcove cell 0xA9B40150 should always " +
|
$"Straddle-gated outside-add: the alcove sphere straddles " +
|
||||||
$"include the door's outdoor cell 0xA9B40029. " +
|
$"0xA9B40150's exit portal, so the door's outdoor cell " +
|
||||||
|
$"0xA9B40029 must enter the set (retail " +
|
||||||
|
$"CEnvCell::find_transit_cells straddle, Ghidra 0x0052c820). " +
|
||||||
$"Actual: {string.Join(",", cellSet.Select(c => $"0x{c:X8}"))}");
|
$"Actual: {string.Join(",", cellSet.Select(c => $"0x{c:X8}"))}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue