feat(A.5 T14): GpuWorldState RemoveEntitiesFromLandblock + AddEntitiesToExisting

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>
This commit is contained in:
Erik 2026-05-10 07:53:34 +02:00
parent 774a7070a8
commit fb10c3fa8c
2 changed files with 121 additions and 0 deletions

View file

@ -339,6 +339,51 @@ public sealed class GpuWorldState
bucket.Add(entity);
}
/// <summary>
/// Drop all entities from a landblock without removing the terrain. Used
/// by two-tier streaming when a landblock crosses Near→Far hysteresis.
/// Per Phase A.5 spec §4.4.
/// </summary>
public void RemoveEntitiesFromLandblock(uint landblockId)
{
if (!_loaded.TryGetValue(landblockId, out var lb)) return;
if (_wbSpawnAdapter is not null)
_wbSpawnAdapter.OnLandblockUnloaded(landblockId);
_loaded[landblockId] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, System.Array.Empty<WorldEntity>());
_pendingByLandblock.Remove(landblockId);
RebuildFlatView();
}
/// <summary>
/// Merge entities into an existing-loaded landblock. Used by two-tier
/// streaming for the Far→Near promotion case (terrain already loaded;
/// entity layer streaming in). Falls back to the pending bucket if the
/// landblock isn't loaded yet (handles the rare "promote arrives before
/// far load completes" race).
/// Per Phase A.5 spec §4.4.
/// </summary>
public void AddEntitiesToExistingLandblock(uint landblockId, System.Collections.Generic.IReadOnlyList<WorldEntity> entities)
{
if (!_loaded.TryGetValue(landblockId, out var lb))
{
// Park as pending — same pattern as AppendLiveEntity for not-yet-loaded LBs.
if (!_pendingByLandblock.TryGetValue(landblockId, out var bucket))
{
bucket = new List<WorldEntity>();
_pendingByLandblock[landblockId] = bucket;
}
bucket.AddRange(entities);
return;
}
var merged = new List<WorldEntity>(lb.Entities.Count + entities.Count);
merged.AddRange(lb.Entities);
merged.AddRange(entities);
_loaded[landblockId] = new LoadedLandblock(lb.LandblockId, lb.Heightmap, merged);
if (_wbSpawnAdapter is not null)
_wbSpawnAdapter.OnLandblockLoaded(_loaded[landblockId]);
RebuildFlatView();
}
private void RebuildFlatView()
{
_flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray();