Replaces GameWindow._entities flat list with a per-landblock dict keyed by landblock id. The flat entity view is rebuilt on add/remove so the renderer keeps its simple "iterate Entities" loop. Also provides AppendLiveEntity for the CreateObject path that spawns entities into an already-loaded landblock after hydration. Not thread-safe — all mutation is render-thread only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
75 lines
2.7 KiB
C#
75 lines
2.7 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Streaming;
|
|
|
|
/// <summary>
|
|
/// Render-thread-owned registry of currently-loaded landblocks and their
|
|
/// entities. All mutation happens in <see cref="StreamingController.Tick"/>
|
|
/// on the render thread; the renderer reads <see cref="Entities"/> once per
|
|
/// frame.
|
|
///
|
|
/// <para>
|
|
/// Replaces <c>GameWindow._entities</c>, which was a flat list updated in
|
|
/// multiple places. This class is the single point of truth for "what's in
|
|
/// the world right now" and the only thing that mutates it.
|
|
/// </para>
|
|
///
|
|
/// <remarks>
|
|
/// Threading: not thread-safe. All calls must happen on the render thread.
|
|
/// The streaming worker never touches this type.
|
|
/// </remarks>
|
|
/// </summary>
|
|
public sealed class GpuWorldState
|
|
{
|
|
private readonly Dictionary<uint, LoadedLandblock> _loaded = new();
|
|
|
|
// Cached flat view over all entities across all loaded landblocks,
|
|
// rebuilt on each add/remove. The renderer holds a reference to this
|
|
// list, so rebuilding it replaces the reference atomically.
|
|
private IReadOnlyList<WorldEntity> _flatEntities = System.Array.Empty<WorldEntity>();
|
|
|
|
public IReadOnlyList<WorldEntity> Entities => _flatEntities;
|
|
public IReadOnlyCollection<uint> LoadedLandblockIds => _loaded.Keys;
|
|
|
|
public bool IsLoaded(uint landblockId) => _loaded.ContainsKey(landblockId);
|
|
|
|
public void AddLandblock(LoadedLandblock landblock)
|
|
{
|
|
_loaded[landblock.LandblockId] = landblock;
|
|
RebuildFlatView();
|
|
}
|
|
|
|
public void RemoveLandblock(uint landblockId)
|
|
{
|
|
if (_loaded.Remove(landblockId))
|
|
RebuildFlatView();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void AppendLiveEntity(uint landblockId, WorldEntity entity)
|
|
{
|
|
if (!_loaded.TryGetValue(landblockId, out var lb))
|
|
return;
|
|
|
|
// LoadedLandblock.Entities is an IReadOnlyList. Rebuild the
|
|
// landblock record with the new entity appended. We accept the
|
|
// allocation here because live spawns are rare compared to frame
|
|
// iteration.
|
|
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);
|
|
RebuildFlatView();
|
|
}
|
|
|
|
private void RebuildFlatView()
|
|
{
|
|
_flatEntities = _loaded.Values.SelectMany(lb => lb.Entities).ToArray();
|
|
}
|
|
}
|