fix(physics): #91 — query indoor cell shadows in FindObjCollisions

Interior items (fireplaces, tables, chests) registered via A1.5's
ShadowObjectRegistry.Register `cellScope` parameter (commit 4d3bf6f)
are stored under their ParentCellId key (e.g. 0xA9B40121). But
GetNearbyObjects's broad-phase only iterates outdoor 24m landcell
keys (0xA9B40029 etc) and never looks up indoor cell keys, so
interior shadows were registered but unreachable. User-visible
symptom: tables/boxes/fireplaces don't block movement, while walls
DO block (the indoor BSP path is separate).

Fix: GetNearbyObjects accepts an optional indoorCellIds parameter
and additionally queries _cells[indoorCellId] for each entry with
low-byte >= 0x0100u. FindObjCollisions computes the set via
CellTransit.FindCellSet (same set A4 uses for multi-cell BSP
iteration) and passes it through. Outdoor seeds typically produce
sets containing only outdoor land-cells which the new branch filters
out, so the outdoor-only behavior is preserved.

1147 + 8 baseline maintained. Closes the user-reported regression
"walls block now correct but interior items such as tables and boxes
or fireplaces do not block."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-21 09:03:51 +02:00
parent 4ca35966f8
commit c0d84057cb
2 changed files with 50 additions and 2 deletions

View file

@ -242,14 +242,43 @@ public sealed class ShadowObjectRegistry
/// Get all objects near a world position. Searches the given landblock plus
/// all 8 adjacent landblocks to handle objects near cell/landblock boundaries.
/// Within each landblock, queries only the cells the query sphere overlaps.
///
/// <para>
/// Issue #91 (2026-05-20): the optional <paramref name="indoorCellIds"/>
/// parameter is the candidate set of indoor cells the foot-sphere overlaps
/// (from <see cref="CellTransit.FindCellSet"/>). When supplied, indoor
/// shadows registered via <see cref="Register"/>'s <c>cellScope</c>
/// parameter (A1.5 fix at `4d3bf6f`) are ALSO included in the result.
/// Without this, interior statics (fireplaces, tables, chests) registered
/// against e.g. `0xA9B40121` are stored under that key but the outdoor-
/// grid lookup (cell ids like `0xA9B40029`) never queries the indoor key.
/// Net effect pre-fix: interior items don't block movement.
/// </para>
/// </summary>
public void GetNearbyObjects(Vector3 worldPos, float queryRadius,
float worldOffsetX, float worldOffsetY, uint landblockId,
List<ShadowEntry> results)
List<ShadowEntry> results,
System.Collections.Generic.IReadOnlyCollection<uint>? indoorCellIds = null)
{
results.Clear();
var seen = new HashSet<uint>();
// Indoor-scoped shadows (A1.5 cellScope). Query first so the
// outdoor-grid lookup below skips duplicates via `seen`.
if (indoorCellIds is not null)
{
foreach (uint indoorCellId in indoorCellIds)
{
if ((indoorCellId & 0xFFFFu) < 0x0100u) continue; // skip outdoor ids
if (!_cells.TryGetValue(indoorCellId, out var list)) continue;
foreach (var entry in list)
{
if (seen.Add(entry.EntityId))
results.Add(entry);
}
}
}
// Extract landblock X/Y from the ID.
int lbX = (int)((landblockId >> 24) & 0xFF);
int lbY = (int)((landblockId >> 16) & 0xFF);

View file

@ -1911,10 +1911,29 @@ public sealed class Transition
// iteration. Allocate per call (cheap — typically 0-5 entries).
var nearbyObjs = new List<ShadowEntry>();
float queryRadius = sphereRadius + movement.Length() + 5f;
// Issue #91 (2026-05-20): interior items (fireplaces, tables, chests)
// are registered with `cellScope = ParentCellId` per A1.5's fix at
// `4d3bf6f`. They're stored under the indoor cell key (e.g.
// `0xA9B40121`), but GetNearbyObjects's outdoor-grid lookup (cells
// like `0xA9B40029`) never queries that key. Compute the indoor
// candidate set via CellTransit.FindCellSet — same set A4 uses for
// multi-cell BSP iteration — and pass it through so indoor shadows
// are also picked up. When the seed is outdoor the set typically
// contains only outdoor land-cells which the new branch in
// GetNearbyObjects skips via the `< 0x0100u` filter, so behavior
// matches the prior outdoor-only path.
// (engine.DataCache is non-null per the early-return at top of
// FindObjCollisions; redundant inner check would confuse nullable
// flow analysis.)
_ = CellTransit.FindCellSet(engine.DataCache, currPos, sphereRadius,
sp.CheckCellId, out var indoorCellIds);
engine.ShadowObjects.GetNearbyObjects(
currPos, queryRadius,
worldOffsetX, worldOffsetY, landblockId,
nearbyObjs);
nearbyObjs,
indoorCellIds);
foreach (var obj in nearbyObjs)
{