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;
}