fix(phys): A6.P3 #98 — gate outdoor shadow radial sweep on indoor primary cell
The cellar-up cap was caused by ShadowObjectRegistry.GetNearbyObjects
running its outdoor 24m-grid radial query unconditionally — including
when the moving sphere's primary cell is indoor. The landblock-baked
cottage GfxObj 0x01000A2B (registered with cellScope=0u, i.e.
landblock-wide) was returned for a sphere inside the cellar EnvCell,
and its downward-facing cottage-floor poly at world Z=94 head-bumped
the sphere from below, capping ascent at foot Z=92.74.
Diagnosis this session via the live capture in
a6-issue98-resolve-capture-2.jsonl (92K records, 132 cap events all
with body on the ramp polygon) FALSIFIED the prior "stale ramp
contact plane" hypothesis: the contact plane is correctly the ramp's
plane because the sphere IS on the ramp at the cap. The cap is a
proximate consequence of the cottage GfxObj being queried at all from
an indoor primary cell.
Retail decomp anchor (acclient_2013_pseudo_c.txt):
- 308751-308769: CObjCell::find_cell_list branches on the moving
object's m_position.objcell_id — INDOOR adds only that cell +
portal-visible neighbors via CELLARRAY::add_cell; OUTDOOR adds
all overlapping outdoor cells via CLandCell::add_all_outside_cells.
Object-position-driven, not sphere-radius-driven.
- 309560: CEnvCell::find_collisions calls find_env_collisions
(own cell BSP only) THEN CObjCell::find_obj_collisions on `this`.
- 308916: CObjCell::find_obj_collisions iterates this->shadow_object_list
— strictly per-cell, never landblock-wide.
Combined: a landblock-baked static like the cottage building is added
to outdoor cells' shadow_object_list only (its m_position resolves to
an outdoor cell). An indoor EnvCell's shadow_object_list never
contains the cottage. CEnvCell::find_collisions therefore never tests
the sphere against the cottage. Retail-faithful behavior.
Falsification spike (this session): scoping the cottage to a single
distant outdoor cell instead of landblock-wide caused the harness
LiveCompare_FirstCap test to stop reproducing the cn=(0,0,-1) cap,
confirming the cap is caused by the radial sweep returning the
cottage to an indoor primary.
The fix:
- Add optional `primaryCellId` parameter to
ShadowObjectRegistry.GetNearbyObjects. When indoor (>= 0x0100),
skip the outdoor radial sweep entirely after the indoorCellIds
branch runs. Default 0u preserves prior behavior for
cell-unaware callers (existing tests pass unchanged).
- Transition.FindObjCollisions passes sp.CheckCellId.
- Harness LiveCompare_FirstCap_* flipped to documents-the-fix form
(asserts the downward-facing cottage-floor cap does NOT fire).
Deletes the residual-X-motion test that documented a post-cap
edge-slide — irrelevant once the cap is gone.
This same gate should close the other "Finding 3 family" indoor/outdoor
collision bugs (#97 phantom collisions, indoor sling-out). Visual
verification by the user is the remaining acceptance check before
closing #98.
Verification:
- 11/11 CellarUpTrajectoryReplayTests pass in isolation
- 55 ShadowObjectRegistry + TransitionTypes + PhysicsEngine
+ CellPhysics + CellTransit tests pass
- 8 pre-existing static-state-leakage failures in serial physics
suite are unchanged (verified by stash + retest on baseline)
- dotnet build clean, 0 warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf6d97625c
commit
b3ce505ca8
3 changed files with 85 additions and 103 deletions
|
|
@ -254,11 +254,37 @@ public sealed class ShadowObjectRegistry
|
|||
/// grid lookup (cell ids like `0xA9B40029`) never queries the indoor key.
|
||||
/// Net effect pre-fix: interior items don't block movement.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Issue #98 (2026-05-24): the optional <paramref name="primaryCellId"/>
|
||||
/// parameter gates the outdoor radial sweep on the SPHERE's primary cell
|
||||
/// type. Mirrors retail's <c>CObjCell::find_cell_list</c> at
|
||||
/// <c>acclient_2013_pseudo_c.txt:308751-308769</c>: when the sphere's
|
||||
/// position is in an indoor cell (id ≥ 0x0100), retail only adds THAT
|
||||
/// cell + portal-visible neighbors to the cell array — never outdoor
|
||||
/// cells. Combined with <c>CEnvCell::find_collisions</c> only iterating
|
||||
/// <c>this->shadow_object_list</c>, retail's indoor cells never test
|
||||
/// against outdoor statics like landblock-baked buildings.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Pre-fix bug shape (issue #98): the cellar EnvCell's player sphere
|
||||
/// queried the outdoor 24-m grid via the radial sweep and picked up the
|
||||
/// landblock-wide cottage GfxObj (registered with <c>cellScope=0</c>);
|
||||
/// the head sphere bumped the cottage's downward-facing floor poly from
|
||||
/// below at world Z=94 and capped the ascent. With this gate, the
|
||||
/// outdoor sweep is skipped when the primary cell is indoor, so the
|
||||
/// cottage is only seen from outdoor primary cells (the building's
|
||||
/// own outdoor footprint). Default <c>primaryCellId=0</c> preserves
|
||||
/// the pre-fix radial-only behavior for callers that don't know /
|
||||
/// care about cell type (existing tests).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void GetNearbyObjects(Vector3 worldPos, float queryRadius,
|
||||
float worldOffsetX, float worldOffsetY, uint landblockId,
|
||||
List<ShadowEntry> results,
|
||||
System.Collections.Generic.IReadOnlyCollection<uint>? indoorCellIds = null)
|
||||
System.Collections.Generic.IReadOnlyCollection<uint>? indoorCellIds = null,
|
||||
uint primaryCellId = 0u)
|
||||
{
|
||||
results.Clear();
|
||||
var seen = new HashSet<uint>();
|
||||
|
|
@ -279,6 +305,17 @@ public sealed class ShadowObjectRegistry
|
|||
}
|
||||
}
|
||||
|
||||
// Issue #98 (2026-05-24): when the sphere is in an INDOOR cell, skip
|
||||
// the outdoor radial sweep entirely — retail's CEnvCell::find_collisions
|
||||
// only iterates this->shadow_object_list, never outdoor cells'. Indoor
|
||||
// statics are reached via indoorCellIds above. This closes the
|
||||
// cottage-cellar Z-cap (head sphere bumping cottage floor from below
|
||||
// because the landblock-wide cottage GfxObj was returned by the
|
||||
// unconditional radial sweep). Callers that don't pass primaryCellId
|
||||
// (or pass 0) keep the pre-fix radial-only behavior.
|
||||
if ((primaryCellId & 0xFFFFu) >= 0x0100u)
|
||||
return;
|
||||
|
||||
// Extract landblock X/Y from the ID.
|
||||
int lbX = (int)((landblockId >> 24) & 0xFF);
|
||||
int lbY = (int)((landblockId >> 16) & 0xFF);
|
||||
|
|
|
|||
|
|
@ -2177,11 +2177,18 @@ public sealed class Transition
|
|||
_ = CellTransit.FindCellSet(engine.DataCache, currPos, sphereRadius,
|
||||
sp.CheckCellId, out var indoorCellIds);
|
||||
|
||||
// Issue #98 (2026-05-24): pass primary cellId so the radial outdoor
|
||||
// sweep is skipped when sphere is in an indoor cell. Mirrors retail's
|
||||
// CObjCell::find_cell_list indoor/outdoor branch
|
||||
// (acclient_2013_pseudo_c.txt:308751-308769) — indoor cells only
|
||||
// iterate their own shadow lists + portal-visible neighbors, never
|
||||
// outdoor cells' shadow lists. Closes the cottage cellar-up cap.
|
||||
engine.ShadowObjects.GetNearbyObjects(
|
||||
currPos, queryRadius,
|
||||
worldOffsetX, worldOffsetY, landblockId,
|
||||
nearbyObjs,
|
||||
indoorCellIds);
|
||||
indoorCellIds,
|
||||
primaryCellId: sp.CheckCellId);
|
||||
|
||||
foreach (var obj in nearbyObjs)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue