From 8941447204a0edd81c545516811df660aa979f6d Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 11 Apr 2026 23:10:18 +0200 Subject: [PATCH] =?UTF-8?q?fix(app):=20Phase=20A.1=20=E2=80=94=20canonical?= =?UTF-8?q?ize=20live=20spawn=20landblockId=20in=20AppendLiveEntity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/Streaming/GpuWorldState.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/AcDream.App/Streaming/GpuWorldState.cs b/src/AcDream.App/Streaming/GpuWorldState.cs index 1e7702b..0d5b647 100644 --- a/src/AcDream.App/Streaming/GpuWorldState.cs +++ b/src/AcDream.App/Streaming/GpuWorldState.cs @@ -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. + /// + /// + /// The server's landblockId is in cell-resolved form + /// (0xAAAA00CC: high byte X, second byte Y, low 16 bits cell + /// index within the landblock). The streaming system stores landblocks + /// keyed by their canonical 0xAAAA0xFFFF form. Canonicalize on + /// the way in so callers don't have to think about it — masking with + /// 0xFFFF0000u | 0xFFFFu drops the cell index and selects the + /// LandBlock dat terminator. + /// /// 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(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(); }