fix(app): Phase A.1 — canonicalize live spawn landblockId in AppendLiveEntity

After the 0xFFFF terminator fix in f83a8c1, terrain renders correctly
but live NPCs and weenies disappeared. Root cause: the server's
ServerPosition.LandblockId is in cell-resolved form (0xAAAA00CC where
the low 16 bits are the cell index within the landblock), but the
streaming system stores landblocks in GpuWorldState keyed by their
canonical 0xAAAAFFFF form. AppendLiveEntity was passing the raw
server id straight into the dict TryGetValue, missing every time, and
silently dropping the spawn.

Fix: canonicalize at the GpuWorldState boundary by masking with
(0xFFFF0000u | 0xFFFFu). The XML doc on the method explains the two
forms so future callers don't have to guess. Calling code stays
unchanged.

212 tests green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 23:10:18 +02:00
parent f83a8c1674
commit 8941447204

View file

@ -51,10 +51,21 @@ public sealed class GpuWorldState
/// Append an entity to a specific landblock's slot. Used by the live
/// CreateObject path where the server spawns entities into an already-
/// loaded landblock after the initial hydration pass.
///
/// <para>
/// The server's <c>landblockId</c> is in cell-resolved form
/// (<c>0xAAAA00CC</c>: high byte X, second byte Y, low 16 bits cell
/// index within the landblock). The streaming system stores landblocks
/// keyed by their canonical <c>0xAAAA0xFFFF</c> form. Canonicalize on
/// the way in so callers don't have to think about it — masking with
/// <c>0xFFFF0000u | 0xFFFFu</c> drops the cell index and selects the
/// LandBlock dat terminator.
/// </para>
/// </summary>
public void AppendLiveEntity(uint landblockId, WorldEntity entity)
{
if (!_loaded.TryGetValue(landblockId, out var lb))
uint canonicalLandblockId = (landblockId & 0xFFFF0000u) | 0xFFFFu;
if (!_loaded.TryGetValue(canonicalLandblockId, out var lb))
return;
// LoadedLandblock.Entities is an IReadOnlyList. Rebuild the
@ -64,7 +75,7 @@ public sealed class GpuWorldState
var newEntities = new List<WorldEntity>(lb.Entities.Count + 1);
newEntities.AddRange(lb.Entities);
newEntities.Add(entity);
_loaded[landblockId] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, newEntities);
_loaded[canonicalLandblockId] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, newEntities);
RebuildFlatView();
}