using static Repoverse.Core.WorldConstants;
namespace Repoverse.Core.Voxels;
///
/// Keeps the chunks around a focus point resident (§11, §13). Each
/// starts generation for the nearest missing chunks on the thread pool (bounded by
/// maxInFlight), collects finished ones, and unloads chunks beyond radius + hysteresis,
/// cancelling their pending work. Memory is bounded by the radius, not by world size.
///
public sealed class ChunkStreamer(WorldGenerator generator, WorldEdits edits, int radius = StreamRadiusChunks, int maxInFlight = 8)
{
private readonly Dictionary _loaded = new();
private readonly Dictionary Task, CancellationTokenSource Cts)> _pending = new();
private readonly object _statsGate = new();
public IReadOnlyDictionary Loaded => _loaded;
public int InFlight => _pending.Count;
public long ChunksGenerated { get; private set; }
public TimeSpan TotalGenerationTime { get; private set; }
public sealed record Tick(int Started, int Completed, int Unloaded, int Cancelled);
public Tick Update(int focusX, int focusZ)
{
var center = ChunkCoord.FromVoxel(focusX, GroundLevel, focusZ);
int completed = 0, unloaded = 0, cancelled = 0, started = 0;
foreach (var (c, p) in _pending.Where(kv => kv.Value.Task.IsCompleted).ToList())
{
_pending.Remove(c);
p.Cts.Dispose();
if (p.Task.IsCompletedSuccessfully) { _loaded[c] = p.Task.Result; completed++; }
}
int keep = radius + StreamHysteresisChunks;
foreach (var c in _loaded.Keys.Where(c => HorizontalDistance(c, center) > keep).ToList())
{
_loaded.Remove(c);
unloaded++;
}
foreach (var (c, p) in _pending.Where(kv => HorizontalDistance(kv.Key, center) > keep).ToList())
{
p.Cts.Cancel();
_pending.Remove(c);
cancelled++;
}
foreach (var c in Wanted(center))
{
if (_pending.Count >= maxInFlight) break;
if (_loaded.ContainsKey(c) || _pending.ContainsKey(c)) continue;
var cts = new CancellationTokenSource();
var token = cts.Token;
_pending[c] = (Task.Run(() =>
{
var start = System.Diagnostics.Stopwatch.GetTimestamp();
var chunk = generator.Generate(c, edits, token);
lock (_statsGate) { ChunksGenerated++; TotalGenerationTime += System.Diagnostics.Stopwatch.GetElapsedTime(start); }
return chunk;
}, token), cts);
started++;
}
return new Tick(started, completed, unloaded, cancelled);
}
/// Runs updates until everything wanted around the focus is resident.
public async Task SettleAsync(int focusX, int focusZ, CancellationToken ct = default)
{
while (true)
{
Update(focusX, focusZ);
if (_pending.Count == 0) return;
await Task.WhenAny(_pending.Values.Select(p => p.Task)).WaitAsync(ct);
}
}
/// Chunks within the radius, nearest first (§13 prioritisation).
public IEnumerable Wanted(ChunkCoord center)
{
var list = new List();
for (int dx = -radius; dx <= radius; dx++)
for (int dz = -radius; dz <= radius; dz++)
for (int y = 0; y <= WorldGenerator.MaxChunkY; y++)
list.Add(new ChunkCoord(center.X + dx, y, center.Z + dz));
return list.OrderBy(c => HorizontalDistance(c, center)).ThenBy(c => Math.Abs(c.Y - center.Y)).ThenBy(c => c.X).ThenBy(c => c.Z);
}
private static int HorizontalDistance(ChunkCoord a, ChunkCoord b) => Math.Max(Math.Abs(a.X - b.X), Math.Abs(a.Z - b.Z));
/// World-space voxel lookup across loaded chunks; null when the chunk is not resident.
public Voxel? Sample(int x, int y, int z) =>
_loaded.TryGetValue(ChunkCoord.FromVoxel(x, y, z), out var chunk)
? chunk[x - chunk.Coord.OriginX, y - chunk.Coord.OriginY, z - chunk.Coord.OriginZ]
: null;
}