using System;
using System.Collections.Generic;
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 GpuWorldState _state;
private StreamingRegion? _region;
public int Radius { get; set; }
///
/// Cap on completions drained per call. Defaults to
/// effectively unlimited because the current LandblockStreamer
/// is synchronous — every EnqueueLoad writes to the outbox on
/// the same thread, so by the time we drain there's no backlog
/// to spread, and the cap only serves to *delay* applying landblocks
/// the user is already trying to look at.
///
///
/// The original async design used a small cap (4) to limit per-frame
/// GPU upload spikes. That reasoning becomes relevant again if/when
/// the streamer moves back to async loading; lower this knob then.
/// Crucially, dropping completions to a lower frame is what was
/// silently breaking live spawns: the post-login spawn flood would
/// arrive on a frame where only 4 of the 25 visible-window landblocks
/// had been applied, the spawns for the other 21 hit
/// AppendLiveEntity with no matching loaded slot, and got
/// dropped (now: parked in the pending bucket).
///
///
public int MaxCompletionsPerFrame { get; set; } = int.MaxValue;
public StreamingController(
Action enqueueLoad,
Action enqueueUnload,
Func> drainCompletions,
Action applyTerrain,
GpuWorldState state,
int radius)
{
_enqueueLoad = enqueueLoad;
_enqueueUnload = enqueueUnload;
_drainCompletions = drainCompletions;
_applyTerrain = applyTerrain;
_state = state;
Radius = radius;
}
///
/// Advance one frame. /
/// are landblock coordinates (0..255) of the current viewer — the camera
/// in offline mode, the server-sent player position in live.
///
public void Tick(int observerCx, int observerCy)
{
// First-tick bootstrap: no region yet, so the whole visible window
// is a load diff.
if (_region is null)
{
_region = new StreamingRegion(observerCx, observerCy, Radius);
foreach (var id in _region.Visible)
_enqueueLoad(id);
}
else if (_region.CenterX != observerCx || _region.CenterY != observerCy)
{
var diff = _region.RecenterTo(observerCx, observerCy);
foreach (var id in diff.ToLoad) _enqueueLoad(id);
foreach (var id in diff.ToUnload) _enqueueUnload(id);
}
// Drain up to N completions per frame so a big diff doesn't spike
// GPU upload time. Remaining completions wait for the next frame.
var drained = _drainCompletions(MaxCompletionsPerFrame);
foreach (var result in drained)
{
switch (result)
{
case LandblockStreamResult.Loaded loaded:
_applyTerrain(loaded.Landblock);
_state.AddLandblock(loaded.Landblock);
break;
case LandblockStreamResult.Unloaded unloaded:
_state.RemoveLandblock(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;
}
}
}
}