diff --git a/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs b/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs index 45416ee..5f42087 100644 --- a/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs +++ b/VoxelEngine.Tests/Streaming/ChunkStreamerTests.cs @@ -317,4 +317,38 @@ public class ChunkStreamerTests // Only the chunk in the load box was created; no spurious remesh. Assert.Single(created); } + + [Fact] + public void Tick_GeneratorThrows_LogsAndDoesNotRetry() + { + var cfg = new Configuration + { + RenderDistanceHorizontal = 0, + RenderDistanceVertical = 0, + MaxGenerateJobsInFlight = 100, + MaxMeshUploadsPerFrame = 100, + }; + var registry = new BlockRegistry(); + var store = new InMemoryChunkModificationStore(); + var world = new World.World(registry, store); + var mesher = new FakeMesher(); + var executor = new SynchronousJobExecutor(); + int generateCalls = 0; + Func generate = _ => + { + generateCalls++; + throw new InvalidOperationException("boom"); + }; + Func factory = m => new FakeRenderer(m); + + var streamer = new ChunkStreamer(cfg, world, generate, mesher, registry, executor, factory); + + streamer.Tick(new Vector3i(0, 0, 0)); + Assert.Equal(1, generateCalls); + Assert.False(streamer.IsReady(new Vector3i(0, 0, 0))); + + // Tick again — same chunk, same position. Should NOT call generate again. + streamer.Tick(new Vector3i(0, 0, 0)); + Assert.Equal(1, generateCalls); + } } diff --git a/World/Streaming/ChunkStreamer.cs b/World/Streaming/ChunkStreamer.cs index a07de75..c9a61ca 100644 --- a/World/Streaming/ChunkStreamer.cs +++ b/World/Streaming/ChunkStreamer.cs @@ -29,6 +29,7 @@ public sealed class ChunkStreamer : IDisposable private readonly PriorityQueue _pendingLoad = new(); private readonly ConcurrentQueue _completed = new(); private readonly Queue _pendingUnload = new(); + private readonly HashSet _failed = new(); private Vector3i _lastPlayerChunk = new(int.MinValue, int.MinValue, int.MinValue); private int _inFlight; @@ -118,6 +119,7 @@ public sealed class ChunkStreamer : IDisposable foreach (var pos in target) { + if (this._failed.Contains(pos)) continue; if (this._tracked.Add(pos)) { int priority = (pos.X - p.X) * (pos.X - p.X) @@ -162,6 +164,7 @@ public sealed class ChunkStreamer : IDisposable } this._world.RemoveChunk(pos); this._ready.Remove(pos); + this._failed.Remove(pos); Log.Debug("Unloaded {Pos}", pos); } } @@ -190,6 +193,7 @@ public sealed class ChunkStreamer : IDisposable { if (result.Error != null || result.Mesh == null) { + this._failed.Add(result.Pos); Log.Error(result.Error, "Chunk {Pos} failed to load", result.Pos); return; }