GpuWorldState.RemoveEntitiesFromLandblock now invokes an optional
Action<uint> callback before zeroing the entity list. GameWindow wires
this to EntityClassificationCache.InvalidateLandblock so cache entries
get swept on LB demote (Near to Far) and unload. Per spec section 5.3 W3b.
The callback receives the canonicalized landblock id (low 16 bits forced
to 0xFFFF), matching the LandblockHint stored at Populate time. Trace:
GpuWorldState._loaded keys are canonical (set by AppendLiveEntity),
LandblockEntries yields kvp.Key as LandblockId, WalkEntitiesInto
propagates entry.LandblockId into _walkScratch, the dispatcher's
populateLandblockId reads that tuple and stores it as LandblockHint.
Phase 3 (invalidation hooks) complete. The cache now stays correct across
all spec-identified mutation events: despawn, ObjDescEvent (despawn+
respawn), LB demote, LB unload.
Two integration tests added:
- RemoveEntitiesFromLandblock_FiresUnloadCallbackWithCanonicalId asserts
the callback fires once with the canonical id even when called with a
cell-resolved input (low 16 bits non-FFFF).
- RemoveEntitiesFromLandblock_NotLoaded_DoesNotFireCallback asserts the
early-return path doesn't fire the callback for unknown landblocks.
Tests: 1706 passed / 8 failed (baseline). Sentinel: 110/110.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new methods on GpuWorldState, used by two-tier streaming (T13):
- RemoveEntitiesFromLandblock(id): drop all entities from an LB while
keeping the terrain. Used for Near->Far demote (player walks past the
inner ring; LB stays loaded but entities leave).
- AddEntitiesToExistingLandblock(id, entities): merge new entities into
an already-loaded LB record. Used for Far->Near promote (terrain is
already on the GPU; just streaming the entity layer in).
Falls back to the pending bucket if the LB hasn't loaded yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>