using System; using System.Collections.Generic; using AcDream.App.Rendering.Wb; using AcDream.Core.Terrain; using AcDream.Core.World; namespace AcDream.App.Streaming; /// /// Called once per frame from GameWindow.OnUpdate. Owns the /// and uses delegates into /// so tests can inject fakes. All work /// happens on the render thread; the streamer itself is background. /// /// /// Threading: not thread-safe. All calls must happen on the render thread. /// /// public sealed class StreamingController { private readonly Action _enqueueLoad; private readonly Action _enqueueUnload; private readonly Func> _drainCompletions; private readonly Action _applyTerrain; private readonly Action? _removeTerrain; private readonly Action? _demoteNearLayer; private readonly Action? _clearPendingLoads; /// /// #138: fired after a landblock's entity layer (re)loads — both the full /// path (initial / dungeon-exit /// expand) and the path /// (Far→Near). GameWindow wires it to re-hydrate server objects whose /// render entities were dropped by the collapse/demote but whose parsed /// spawns it still retains. Receives the canonical landblock id. Null in /// tests that don't exercise re-hydration. /// private readonly Action? _onLandblockLoaded; // Invoked only after LandblockSpawnAdapter has pinned every synthetic // EnvCell geometry id. This re-arms the schema-aware preparation path if // an unowned cached mesh was evicted between worker schedule and publish. private readonly Action? _ensureEnvCellMeshes; private readonly GpuWorldState _state; private StreamingRegion? _region; // Hard streaming boundaries advance this token. Worker completions carry // the generation captured at enqueue time, so an old overlapping load or // unload cannot mutate the replacement window after a portal recenter. private ulong _generation; // True while streaming is collapsed to the single dungeon landblock the // player stands in (the dungeon gate, #133 FPS). AC dungeons have NO // adjacent landblocks — neighbors are unrelated ocean-grid dungeons that // are never visible, so we stop loading the 25×25 window entirely. private bool _collapsed; // The dungeon landblock id we collapsed onto. Once collapsed we key the // gate on this STABLE landblock, not the per-frame insideDungeon signal: // CurrCell can momentarily resolve to null/outdoor mid-frame, and gating // expand on that flicker thrashes collapse↔expand (reload storms + a light // leak). We only expand when the observer actually moves to a different // landblock (teleport/portal out). private uint _collapsedCenter; /// /// Near-tier radius (LBs from observer that load full detail: terrain + /// scenery + entities). Set at construction; readable thereafter. /// /// /// Mutating after the first has no effect — the /// internal snapshots both radii on its /// constructor. Treat as init-only post-Tick. /// public int NearRadius { get; } /// /// Far-tier radius (LBs from observer that load terrain only). Set at /// construction; readable thereafter. /// /// /// Mutating after the first has no effect — see . /// public int FarRadius { get; } /// /// Cap on completions drained per call. The cap is /// the GPU upload budget for one frame: terrain mesh + per-entity GfxObj /// sub-mesh uploads + texture uploads for one landblock take a few ms; /// applying 25 of them in a single frame produces a memory spike /// (observed: out-of-memory crash on the 5×5 first-frame load). /// /// /// 4 is the original async-streamer value; it spreads a 5×5 first-frame /// load over ~7 frames (~116ms at 60fps), which is below the human /// perception threshold. Spawn races that previously dropped entities /// while landblocks were in flight are now handled by /// 's pending-spawn list, so spreading /// completions doesn't lose any data. /// /// public int MaxCompletionsPerFrame { get; set; } = 4; /// /// #138: the teleport destination landblock id. When non-zero, /// applies this landblock's /// or /// completion immediately /// — even if it sits past the /// position in the outbox — so the player can materialise at the /// destination without waiting for every earlier-queued landblock to /// drain first. Non-priority completions drained past it are buffered /// in and applied over subsequent frames, /// so no completions are lost and there is no GPU spike. /// /// Set by GameWindow when a teleport starts; reset to 0 /// once the destination landblock has been applied (or when the /// teleport destination changes). /// public uint PriorityLandblockId { get; set; } /// /// 2026-06-22: the radius (in landblocks, Chebyshev) around /// that eager-applies /// ahead of the per-frame budget. 0 (default) = only the single center landblock — the /// original priority behaviour. A teleport sets this to the near ring so the player's /// IMMEDIATE SURROUNDINGS (terrain + collision + scenery) are resident on arrival, not /// just the one landblock they stand on. Without it, only the destination landblock /// applies immediately and everything around it drains at , /// so the player arrives to a near-empty world (the "Fort Tethana only one landblock /// loaded" symptom) and their cell-walk can't root into neighbour cells yet (the /// transient walk-through-walls). The far ring still drains at the budget. The eager /// apply runs while the portal viewport replaces the world, so the GPU spike is hidden. /// public int PriorityRadius { get; set; } // [FRAME-DIAG] (read by GameWindow's ACDREAM_WB_DIAG rollup): the standing // deferred-LOAD backlog. A non-zero value during/after a teleport is the // GPU-upload tail draining at MaxCompletionsPerFrame. Removable probe surface. public int DeferredApplyBacklog => _deferredApply.Count; /// /// True once every in-bounds landblock in the requested Chebyshev ring has /// crossed the render-thread publication barrier. Worker completion and /// world-state registration are not sufficient: all static GfxObj and /// EnvCell shell meshes must have completed their render-thread upload. /// Portal-space exit uses this alongside physics residency so the world /// cannot be revealed while its render slots are still absent. /// public bool IsRenderNeighborhoodResident(uint cellOrLandblockId, int radius) { if (radius < 0) throw new ArgumentOutOfRangeException(nameof(radius)); // LandDefs::InboundValidCellId validates both map axes and the low-word // class (outdoor cell, EnvCell, or canonical landblock sentinel). if (!AcDream.Core.Physics.LandDefs.InboundValidCellId(cellOrLandblockId)) return false; int cx = (int)((cellOrLandblockId >> 24) & 0xFFu); int cy = (int)((cellOrLandblockId >> 16) & 0xFFu); for (int dx = -radius; dx <= radius; dx++) for (int dy = -radius; dy <= radius; dy++) { int nx = cx + dx; int ny = cy + dy; // Match PhysicsEngine.IsNeighborhoodTerrainResident: the outer // 0xFF coordinate has no loadable neighbour beyond it. if (nx < 0 || nx > 254 || ny < 0 || ny > 254) continue; uint canonical = ((uint)nx << 24) | ((uint)ny << 16) | 0xFFFFu; if (!_state.IsNearTier(canonical) || !_state.IsRenderReady(canonical)) return false; } return true; } // [FRAME-DIAG]: how many times ForceReloadWindow has fired (each one drops + // re-uploads the WHOLE window) and the landblock count it dropped last time. public int ForceReloadCount { get; private set; } public int LastForceReloadDropCount { get; private set; } // Completions that were drained past a priority item get buffered here // so they still apply over subsequent frames without loss. private readonly List _deferredApply = new(); public StreamingController( Action enqueueLoad, Action enqueueUnload, Func> drainCompletions, Action applyTerrain, GpuWorldState state, int nearRadius, int farRadius, Action? removeTerrain = null, Action? demoteNearLayer = null, Action? clearPendingLoads = null, Action? onLandblockLoaded = null, Action? ensureEnvCellMeshes = null) { _enqueueLoad = enqueueLoad; _enqueueUnload = enqueueUnload; _drainCompletions = drainCompletions; _applyTerrain = applyTerrain; _removeTerrain = removeTerrain; _demoteNearLayer = demoteNearLayer; _clearPendingLoads = clearPendingLoads; _onLandblockLoaded = onLandblockLoaded; _ensureEnvCellMeshes = ensureEnvCellMeshes; _state = state; NearRadius = nearRadius; FarRadius = farRadius; } /// /// Compatibility constructor for deterministic tests and callers that do /// not own an asynchronous worker. Production must use the generation-aware /// overload above so hard-recenter results can be rejected by incarnation. /// internal StreamingController( Action enqueueLoad, Action enqueueUnload, Func> drainCompletions, Action applyTerrain, GpuWorldState state, int nearRadius, int farRadius, Action? removeTerrain = null, Action? demoteNearLayer = null, Action? clearPendingLoads = null, Action? onLandblockLoaded = null, Action? ensureEnvCellMeshes = null) : this( (id, kind, _) => enqueueLoad(id, kind), (id, _) => enqueueUnload(id), drainCompletions, applyTerrain, state, nearRadius, farRadius, removeTerrain, demoteNearLayer, clearPendingLoads, onLandblockLoaded, ensureEnvCellMeshes) { } private void EnqueueLoad(uint id, LandblockStreamJobKind kind) => _enqueueLoad(id, kind, _generation); private void EnqueueUnload(uint id) => _enqueueUnload(id, _generation); private void DemoteLandblock(uint id) { uint canonical = (id & 0xFFFF0000u) | 0xFFFFu; // App-owned Near resources need the still-present static entity list // for exact light/translucency owner cleanup. Retire them before // GpuWorldState drops the static layer and its render pins. _demoteNearLayer?.Invoke(canonical); _state.RemoveEntitiesFromLandblock(canonical); } private void AdvanceGeneration() { _generation = unchecked(_generation + 1); } /// /// Advance one frame. / /// are landblock coordinates (0..255) of the current viewer — the camera /// in offline mode, the server-sent player position in live. /// /// Two-tier model (Phase A.5 T13): /// /// → enqueue LoadFar (terrain only, no entities) /// → enqueue LoadNear (terrain + entities) /// → enqueue PromoteToNear (entity layer for already-loaded terrain) /// → drop entities on render thread immediately (terrain stays) /// → enqueue full unload /// /// public void Tick(int observerCx, int observerCy, bool insideDungeon = false) { uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy); if (_collapsed) { // Hysteresis. Cases: // - Still in the SAME dungeon landblock → hold (sweep stragglers). // - In a DIFFERENT dungeon cell (multi-landblock dungeon / new dungeon) // → re-collapse onto it. // - CurrCell flickered null but the player hasn't gone anywhere: the // observer landblock reverts to the position-derived value, which for a // dungeon is only ever the ADJACENT off-by-one landblock (negative cell- // local Y). Hold — never expand on an adjacent flicker. // - Genuinely left to a DISTANT landblock (portal/teleport out, always far // from the ocean-grid dungeon block) → expand. if (insideDungeon && centerId != _collapsedCenter) EnterDungeonCollapse(observerCx, observerCy, centerId); else if (!insideDungeon && ChebyshevLandblocks(centerId, _collapsedCenter) > 1) ExitDungeonExpand(observerCx, observerCy); else SweepCollapsed(); } else if (insideDungeon) { EnterDungeonCollapse(observerCx, observerCy, centerId); } else { NormalTick(observerCx, observerCy); } DrainAndApply(); } /// /// #135: collapse to a single dungeon landblock IMMEDIATELY, before the first /// has a chance to bootstrap the full 25×25 window. Called /// from the login / teleport spawn path the instant the streaming center is /// recentered onto a SEALED dungeon landblock. /// /// The per-frame insideDungeon gate keys on the physics /// CurrCell, which is only set once the player is PLACED — and placement /// waits for the dungeon landblock to hydrate. So for the whole hydration window /// (tens of seconds for a ~200-cell dungeon) the gate reads false and /// would enqueue the ~24 unrelated ocean-grid neighbor /// dungeons (+ ~19k entities each); the collapse then only mops them up after /// placement. That mop-up is the 10→high FPS ramp users see at a dungeon login. /// /// Pre-collapsing means the EXPENSIVE dungeon-neighbour window is never /// enqueued. On teleport nothing is enqueued at all (this fires before the next /// Tick recenters). On login a brief Holtburg outdoor window may be enqueued by the /// frame-1 NormalTick (before the player's spawn arrives) and is immediately /// cancelled by _clearPendingLoads here — cheap outdoor terrain, not the /// ocean-grid dungeons, and a handful of already-dequeued loads get swept next /// frame. Idempotent: a no-op when already collapsed onto this same landblock, so a /// re-sent spawn or a same-frame double call costs nothing. Render-thread only, /// same as . /// public void InitializeKnownLoginCenter(int cx, int cy, bool isSealedDungeon) { // StreamingReadinessGate keeps the worker stopped until the player's // real spawn supplies this center. There is therefore no guessed-center // window to force-reload here. A sealed dungeon is the one special case: // pin radius zero before the first Tick so no ocean-grid neighbours are // ever enqueued. if (isSealedDungeon) PreCollapseToDungeon(cx, cy); } /// /// Pins streaming to a sealed dungeon before the first normal Tick. /// public void PreCollapseToDungeon(int cx, int cy) { uint centerId = StreamingRegion.EncodeLandblockId(cx, cy); if (_collapsed && _collapsedCenter == centerId) return; EnterDungeonCollapse(cx, cy, centerId); } /// /// Outdoor / building-interior streaming — the original two-tier model. /// private void NormalTick(int observerCx, int observerCy) { if (_region is null) { _region = new StreamingRegion(observerCx, observerCy, NearRadius, FarRadius); var bootstrap = _region.ComputeFirstTickDiff(); foreach (var id in bootstrap.ToLoadNear) EnqueueLoad(id, LandblockStreamJobKind.LoadNear); foreach (var id in bootstrap.ToLoadFar) EnqueueLoad(id, LandblockStreamJobKind.LoadFar); _region.MarkResidentFromBootstrap(); } else if (_region.CenterX != observerCx || _region.CenterY != observerCy) { var diff = _region.RecenterTo(observerCx, observerCy); foreach (var id in diff.ToPromote) EnqueueLoad(id, LandblockStreamJobKind.PromoteToNear); foreach (var id in diff.ToLoadNear) EnqueueLoad(id, LandblockStreamJobKind.LoadNear); foreach (var id in diff.ToLoadFar) EnqueueLoad(id, LandblockStreamJobKind.LoadFar); foreach (var id in diff.ToDemote) DemoteLandblock(id); foreach (var id in diff.ToUnload) EnqueueUnload(id); } } /// /// Dungeon-entry edge: cancel the in-flight window load, unload every /// resident neighbor, and pin streaming to the player's single dungeon /// landblock. Retail-faithful — AC dungeons have no adjacent landblocks /// (ACE LandblockManager.GetAdjacentIDs returns empty for a dungeon); /// the 25×25 window was pulling in ~129 unrelated ocean-grid dungeons and /// their thousands of emitters (#133 FPS). Unloading them also tears down /// their lights, shrinking the static-light set toward retail's ≤40. /// private void EnterDungeonCollapse(int cx, int cy, uint centerId) { if (!_collapsed || _collapsedCenter != centerId) Console.WriteLine($"streaming: dungeon collapse -> 0x{centerId:X8}"); _collapsed = true; _collapsedCenter = centerId; AdvanceGeneration(); _clearPendingLoads?.Invoke(); _deferredApply.Clear(); foreach (var id in _state.LoadedLandblockIds) if (id != centerId) EnqueueUnload(id); // Pin a radius-0 region so RecenterTo never re-expands while inside, // and so the post-exit rebuild starts from a clean, consistent state. _region = new StreamingRegion(cx, cy, 0, 0); _region.MarkResidentFromBootstrap(); // The dungeon landblock itself must be (or become) loaded. If a prior // ClearPendingLoads cancelled its queued load, re-enqueue it. if (!_state.IsLoaded(centerId)) EnqueueLoad(centerId, LandblockStreamJobKind.LoadNear); else if (!_state.IsNearTier(centerId)) EnqueueLoad(centerId, LandblockStreamJobKind.PromoteToNear); } /// /// While collapsed, unload any landblock that finished loading after the /// collapse edge — a Load the worker had already dequeued before the /// control job took /// effect. At steady state only the dungeon landblock is resident, so this /// is a no-op. /// private void SweepCollapsed() { // Always preserve the true dungeon landblock (_collapsedCenter), never the // per-frame observer landblock — a CurrCell flicker must not unload the dungeon. foreach (var id in _state.LoadedLandblockIds) if (id != _collapsedCenter) EnqueueUnload(id); } /// Chebyshev distance in landblock cells between two landblock ids. private static int ChebyshevLandblocks(uint a, uint b) { int ax = (int)((a >> 24) & 0xFFu), ay = (int)((a >> 16) & 0xFFu); int bx = (int)((b >> 24) & 0xFFu), by = (int)((b >> 16) & 0xFFu); return Math.Max(Math.Abs(ax - bx), Math.Abs(ay - by)); } /// /// True when is the priority center or within /// landblocks of it (Chebyshev) — i.e. inside the teleport /// near ring that eager-applies. With the default radius 0 /// this reduces to an exact match on (the original /// single-landblock priority behaviour). /// private bool IsWithinPriorityRing(uint id) => PriorityLandblockId != 0u && ChebyshevLandblocks(id, PriorityLandblockId) <= PriorityRadius; /// /// Dungeon-exit edge (portal to outdoors / teleport): rebuild the full /// two-tier window at the new center and unload anything resident from the /// collapsed state that falls outside it. /// private void ExitDungeonExpand(int observerCx, int observerCy) { Console.WriteLine( $"streaming: dungeon EXIT-expand -> ({observerCx},{observerCy}) " + $"(was collapsed on 0x{_collapsedCenter:X8})"); _collapsed = false; AdvanceGeneration(); var rebuilt = new StreamingRegion(observerCx, observerCy, NearRadius, FarRadius); foreach (var id in _state.LoadedLandblockIds) if (!rebuilt.Resident.Contains(id)) EnqueueUnload(id); var boot = rebuilt.ComputeFirstTickDiff(); foreach (var id in boot.ToLoadNear) if (!_state.IsLoaded(id)) EnqueueLoad(id, LandblockStreamJobKind.LoadNear); foreach (var id in boot.ToLoadFar) if (!_state.IsLoaded(id)) EnqueueLoad(id, LandblockStreamJobKind.LoadFar); rebuilt.MarkResidentFromBootstrap(); _region = rebuilt; } /// /// 2026-06-22: an OUTDOOR teleport moved the render origin (_liveCenter). Every /// resident terrain block was baked relative to the OLD origin, so any block that survives /// an INCREMENTAL recenter — which happens on a NEARBY jump where the old and new windows /// overlap, e.g. (170,168)→(169,180) — renders shifted by the jump distance: the confirmed /// "terrain in the sky" arcs (the stale slots were all offset by exactly deltaLB×192). /// SYNCHRONOUSLY drop every resident landblock (render slot + physics + state) so none /// survives into the next frame stale, and no async unload can race a re-bake, then null /// the region so the next re-bootstraps the WHOLE window fresh at /// the new origin (the near ring is priority-applied during portal travel; the rest streams in). /// A sealed-dungeon destination uses instead. /// public void ForceReloadWindow() { _collapsed = false; AdvanceGeneration(); _clearPendingLoads?.Invoke(); _deferredApply.Clear(); // Snapshot — RemoveLandblock mutates the loaded set we're iterating. var ids = new List(_state.LoadedLandblockIds); ForceReloadCount++; // [FRAME-DIAG] churn counter LastForceReloadDropCount = ids.Count; // = upcoming re-upload volume foreach (var id in ids) { _state.RemoveLandblock(id); _removeTerrain?.Invoke(id); // frees the render slot + physics + cell registries } _region = null; // NormalTick re-creates + bootstraps the full window next frame } /// /// Apply streamed completions for this frame. LOADS (terrain mesh GPU uploads) are the /// expensive part, so they are metered at to avoid a /// GPU-upload spike; the overflow buffers in and drains over /// subsequent frames. UNLOADS are cheap (they free GPU buffers — no upload) and are applied /// IMMEDIATELY, never throttled: a teleport produces a whole window of unloads (~600), and /// metering them at the load rate left the previous location's terrain resident for seconds /// (rendering at its old world position as "floating terrain at the horizon"), and rapid /// hops accumulated them faster than they cleared — a runaway resident count (951 observed /// vs a 625 window) that also dragged FPS. Loads inside the teleport near ring /// (, applied during portal travel) likewise bypass the budget so the /// player materialises in a loaded world. /// private void DrainAndApply() { // --- Step 1: drain the outbox in bounded chunks. Apply unloads + priority near-ring // loads immediately; defer every other (budget-metered) load. Draining the whole // outbox each frame (bounded by MaxDrainIterations) is what lets unloads flush // promptly regardless of the load backlog — the throttle is on GPU UPLOADS, not on // freeing them. The drain cap must NOT be gated behind the per-frame load budget // (the prior version returned once the budget hit 0, stranding the outbox). const int MaxDrainIterations = 64; // cap at 64 * MaxCompletionsPerFrame drained/frame int iter = 0; while (iter++ < MaxDrainIterations) { var chunk = _drainCompletions(MaxCompletionsPerFrame); if (chunk.Count == 0) break; foreach (var result in chunk) { // Reject obsolete hard-window work before it can occupy the // metered deferred queue. ApplyResult repeats this invariant // for direct callers and future drain paths. if (IsStaleGeneration(result)) continue; if (result is LandblockStreamResult.Unloaded || IsWithinPriorityRing(ResultLandblockId(result))) ApplyResult(result); // free (unload) or behind-the-fade near ring else _deferredApply.Add(result); // a GPU-upload load — meter it in step 2 } } // --- Step 2: apply the deferred LOAD backlog at the per-frame budget (FIFO, so // earlier-queued landblocks win). Caps GPU upload per frame so a big diff doesn't // spike. _deferredApply now only ever holds loads — unloads were applied in step 1. int budget = MaxCompletionsPerFrame; int i = 0; while (i < budget && i < _deferredApply.Count) { ApplyResult(_deferredApply[i]); i++; } if (i > 0) _deferredApply.RemoveRange(0, i); } /// /// Apply a single with the full side- /// effects: terrain upload, GPU state, and the re-hydration callback. /// Extracted from the inline switch in the original DrainAndApply /// so both the priority-hunt path and the normal drain path share it. /// private void ApplyResult(LandblockStreamResult result) { if (IsStaleGeneration(result)) { // A worker can finish one old load/unload after a hard recenter. // Landblock id membership cannot distinguish overlapping windows; // generation is the logical streaming incarnation boundary. return; } if (result is LandblockStreamResult.Unloaded && _region?.TryGetDesiredTier(result.LandblockId, out _) == true) { // Normal recenter does not advance the hard-window generation. A // rapid away->back can therefore re-own an id while its same- // generation unload is already in the worker outbox. Current // region ownership wins; the old unload must not tear it down. return; } if (result is LandblockStreamResult.Loaded or LandblockStreamResult.Promoted) { if (_region is null || !_region.TryGetDesiredTier(result.LandblockId, out var desiredTier)) { // A hard recenter/collapse can leave one worker job already in // flight. Its completion belongs to the old region and must not // resurrect a landblock the new StreamingRegion never owns. return; } bool isNearCompletion = result is LandblockStreamResult.Promoted or LandblockStreamResult.Loaded { Tier: LandblockStreamTier.Near }; if (isNearCompletion && _state.IsNearTier(result.LandblockId)) { // Normal recenter can enqueue a second high-priority Near job // while an earlier one is already in flight. The streamer // supersedes Far/Unload work but intentionally does not dedupe // high-priority jobs. Publication is idempotent at this owner: // never append statics, replay defaults, or reapply the complete // Near transaction to an already-Near landblock. return; } if (desiredTier == LandblockStreamTier.Far && isNearCompletion) { // Recenter can demote a Near job before its first completion. // StreamingRegion already considers that landblock Far, so it // deliberately emits no second LoadFar job. If no terrain is // resident yet, publish the completed heightmap/mesh as a Far // result while discarding its entity and EnvCell layers. This // is the same payload a fresh LoadFar would have produced and // closes the in-flight Near -> desired Far lifecycle without a // duplicate worker read or a permanent terrain hole. if (!_state.IsLoaded(result.LandblockId)) { switch (result) { case LandblockStreamResult.Loaded loaded: ApplyAsFar(loaded.Build, loaded.MeshData); break; case LandblockStreamResult.Promoted promoted: ApplyAsFar(promoted.Build, promoted.MeshData); break; } } return; } } switch (result) { case LandblockStreamResult.Loaded loaded: if (loaded.Tier == LandblockStreamTier.Far && _state.IsNearTier(loaded.LandblockId)) { // This Far completion was queued before a newer Near load // or promotion. Applying it would erase the entity/cell // layer that now owns the landblock. break; } _applyTerrain(loaded.Build, loaded.MeshData); _state.AddLandblock( loaded.Landblock, loaded.Build.EnvCells?.Shells.Select(shell => shell.GeometryId), loaded.Tier); EnsureEnvCellMeshesAfterPin(loaded.Build); // #138: after the landblock is in _loaded (so live projection // hot-paths), restore any retained server objects ACE won't // re-send. Fired AFTER AddLandblock, never before. _onLandblockLoaded?.Invoke(loaded.Landblock.LandblockId); break; case LandblockStreamResult.Promoted promoted: // PromoteToNear carries a complete build and mesh because the // streamer deliberately lets it supersede a queued LoadFar. If // that Far job never started, publish this as the real Near // landblock; if the base is already resident, merge only the // Near layer so existing live projections retain identity. _applyTerrain(promoted.Build, promoted.MeshData); var promotedRenderIds = promoted.Build.EnvCells?.Shells.Select(shell => shell.GeometryId); if (_state.IsLoaded(promoted.LandblockId)) { _state.AddEntitiesToExistingLandblock( promoted.LandblockId, promoted.Entities, promotedRenderIds); } else { _state.AddLandblock( promoted.Landblock, promotedRenderIds, LandblockStreamTier.Near); } EnsureEnvCellMeshesAfterPin(promoted.Build); _onLandblockLoaded?.Invoke(promoted.LandblockId); break; case LandblockStreamResult.Unloaded unloaded: _state.RemoveLandblock(unloaded.LandblockId); _removeTerrain?.Invoke(unloaded.LandblockId); break; case LandblockStreamResult.Failed failed: Console.WriteLine( $"streaming: load failed for 0x{failed.LandblockId:X8}: {failed.Error}"); break; case LandblockStreamResult.WorkerCrashed crashed: Console.WriteLine( $"streaming: worker CRASHED: {crashed.Error}"); break; } } private void ApplyAsFar(LandblockBuild completedBuild, LandblockMeshData meshData) { var completed = completedBuild.Landblock; var farLandblock = new LoadedLandblock( completed.LandblockId, completed.Heightmap, Array.Empty(), PhysicsDatBundle.Empty); var farBuild = new LandblockBuild(farLandblock); _applyTerrain(farBuild, meshData); _state.AddLandblock(farLandblock, tier: LandblockStreamTier.Far); _onLandblockLoaded?.Invoke(farLandblock.LandblockId); } private void EnsureEnvCellMeshesAfterPin(LandblockBuild build) { if (build.EnvCells is { Shells.Length: > 0 } envCells) _ensureEnvCellMeshes?.Invoke(envCells); } private bool IsStaleGeneration(LandblockStreamResult result) => result is not LandblockStreamResult.WorkerCrashed && result.Generation != _generation; /// /// Returns the landblock id associated with . /// For this is 0 by /// convention (not tied to a specific landblock). /// private static uint ResultLandblockId(LandblockStreamResult result) => result.LandblockId; }