File size: 4,279 Bytes
3ce1a40 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | using static Repoverse.Core.WorldConstants;
namespace Repoverse.Core.Voxels;
/// <summary>
/// Keeps the chunks around a focus point resident (§11, §13). Each <see cref="Update"/>
/// starts generation for the nearest missing chunks on the thread pool (bounded by
/// <c>maxInFlight</c>), collects finished ones, and unloads chunks beyond radius + hysteresis,
/// cancelling their pending work. Memory is bounded by the radius, not by world size.
/// </summary>
public sealed class ChunkStreamer(WorldGenerator generator, WorldEdits edits, int radius = StreamRadiusChunks, int maxInFlight = 8)
{
private readonly Dictionary<ChunkCoord, Chunk> _loaded = new();
private readonly Dictionary<ChunkCoord, (Task<Chunk> Task, CancellationTokenSource Cts)> _pending = new();
private readonly object _statsGate = new();
public IReadOnlyDictionary<ChunkCoord, Chunk> 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);
}
/// <summary>Runs updates until everything wanted around the focus is resident.</summary>
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);
}
}
/// <summary>Chunks within the radius, nearest first (§13 prioritisation).</summary>
public IEnumerable<ChunkCoord> Wanted(ChunkCoord center)
{
var list = new List<ChunkCoord>();
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));
/// <summary>World-space voxel lookup across loaded chunks; null when the chunk is not resident.</summary>
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;
}
|