using System.Collections.Generic; using AcDream.Core.World; namespace AcDream.App.Rendering.Wb; /// /// Bridges landblock streaming events to 's /// reference-count lifecycle. Tier-aware by design: only atlas-tier /// entities (procedural / dat-hydrated, identified by /// ServerGuid == 0) drive ref counts. Server-spawned entities /// (per-instance tier) are skipped — those go through /// EntitySpawnAdapter + TextureCache.GetOrUploadWithPaletteOverride /// (see Phase N.4 spec, Architecture → Two-tier rendering split). /// /// /// On load: walks the landblock's atlas-tier entities, collects unique /// GfxObj ids from their MeshRefs, calls /// IncrementRefCount per id. Snapshots the id-set per landblock so /// unload can match the load 1:1. /// /// /// /// On unload: looks up the snapshot, calls DecrementRefCount per id, /// drops the snapshot. Unknown / never-loaded landblocks no-op. /// /// /// /// Idempotency: a duplicate load for the same landblock is a no-op on /// ref-counting (the snapshot is already present). Defensive guard against /// streaming-controller bugs. /// /// /// /// Thread safety: the underlying implementation /// uses ConcurrentDictionary, so the streaming worker thread may call /// this safely. The internal snapshot dictionary is NOT thread-safe and must /// be called from a single streaming thread (the same thread that fires /// AddLandblock / RemoveLandblock events). /// /// public sealed class LandblockSpawnAdapter { private readonly IWbMeshAdapter _adapter; // Maps landblock id → unique GfxObj ids registered for that landblock. // Written on load, read+cleared on unload. Single-threaded (streaming worker). private readonly Dictionary> _idsByLandblock = new(); public LandblockSpawnAdapter(IWbMeshAdapter adapter) { System.ArgumentNullException.ThrowIfNull(adapter); _adapter = adapter; } /// /// Called when a landblock finishes streaming in. /// Registers a ref-count increment with WB for each unique atlas-tier /// GfxObj id in the landblock. Duplicate loads for the same landblock id /// are silently ignored. /// public void OnLandblockLoaded(LoadedLandblock landblock) { System.ArgumentNullException.ThrowIfNull(landblock); // Idempotency: already-loaded landblock is a no-op. if (_idsByLandblock.ContainsKey(landblock.LandblockId)) return; var unique = new HashSet(); foreach (var entity in landblock.Entities) { // Atlas-tier filter: server-spawned entities (ServerGuid != 0) // belong to the per-instance path and are NOT registered with WB. if (entity.ServerGuid != 0) continue; foreach (var meshRef in entity.MeshRefs) unique.Add((ulong)meshRef.GfxObjId); } _idsByLandblock[landblock.LandblockId] = unique; foreach (var id in unique) _adapter.IncrementRefCount(id); } /// /// Called when a landblock is unloaded from the streaming window. /// Releases the ref-count for every GfxObj id that was registered on load. /// Unknown landblock ids (never loaded, or already unloaded) are no-ops. /// public void OnLandblockUnloaded(uint landblockId) { if (!_idsByLandblock.TryGetValue(landblockId, out var unique)) return; foreach (var id in unique) _adapter.DecrementRefCount(id); _idsByLandblock.Remove(landblockId); } }