File size: 5,274 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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
using Repoverse.Core.Generation;
using Repoverse.Core.Model;
using Repoverse.Core.Voxels;
using static Repoverse.Core.WorldConstants;

namespace Repoverse.Core.Tests;

public class VoxelTests
{
    [Fact]
    public void Voxel_packs_type_material_and_state()
    {
        var v = new Voxel(BlockType.Window, MaterialFamily.Glass, 3);
        Assert.Equal(BlockType.Window, v.Type);
        Assert.Equal(MaterialFamily.Glass, v.Material);
        Assert.Equal(3, v.State);
        Assert.False(v.IsOpaque);
    }

    [Theory]
    [InlineData(0, 0)]
    [InlineData(31, 0)]
    [InlineData(32, 1)]
    [InlineData(-1, -1)]
    [InlineData(-32, -1)]
    [InlineData(-33, -2)]
    public void Chunk_coordinates_floor_toward_negative_infinity(int voxel, int chunk) =>
        Assert.Equal(chunk, ChunkCoord.FloorDiv(voxel));

    [Fact]
    public void Solid_box_meshes_to_six_quads()
    {
        var chunk = new Chunk(new ChunkCoord(0, 0, 0));
        for (int x = 4; x < 12; x++) for (int y = 4; y < 9; y++) for (int z = 4; z < 20; z++)
            chunk[x, y, z] = new Voxel(BlockType.Wall);
        var mesh = GreedyMesher.Build(chunk);
        Assert.Equal(6, mesh.Quads.Count);
        Assert.Equal(2 * (8 * 5 + 8 * 16 + 5 * 16), mesh.VisibleFaces);
    }

    [Fact]
    public void Greedy_quads_cover_exactly_the_visible_faces()
    {
        var rng = new SeededRandom(99);
        var chunk = new Chunk(new ChunkCoord(0, 0, 0));
        BlockType[] types = [BlockType.Air, BlockType.Air, BlockType.Wall, BlockType.Floor, BlockType.Window];
        for (int i = 0; i < ChunkVolume; i++)
            chunk[i % ChunkSize, i / (ChunkSize * ChunkSize), i / ChunkSize % ChunkSize] = new Voxel(rng.Pick(types));
        var mesh = GreedyMesher.Build(chunk);
        Assert.Equal(mesh.VisibleFaces, mesh.Quads.Sum(q => q.Width * q.Height));
        Assert.True(mesh.Quads.Count < mesh.VisibleFaces);
    }

    [Fact]
    public void Faces_against_a_loaded_solid_neighbour_are_culled()
    {
        var chunk = new Chunk(new ChunkCoord(0, 0, 0));
        for (int y = 0; y < ChunkSize; y++) for (int z = 0; z < ChunkSize; z++) chunk[ChunkSize - 1, y, z] = new Voxel(BlockType.Wall);
        var open = GreedyMesher.Build(chunk);
        var closed = GreedyMesher.Build(chunk, (x, _, _) => x >= ChunkSize ? new Voxel(BlockType.Wall) : null);
        Assert.Equal(open.VisibleFaces - ChunkSize * ChunkSize, closed.VisibleFaces);
    }

    [Fact]
    public void Generated_chunks_are_deterministic_and_contain_buildings()
    {
        var manifest = ManifestBuilder.Build(TestRepos.Slice(), new Seed(5));
        var b = manifest.Buildings[0];
        var c = ChunkCoord.FromVoxel(b.Footprint.X + 2, GroundLevel + 1, b.Footprint.Z + 2);
        var gen = new WorldGenerator(manifest);
        var one = gen.Generate(c);
        var two = new WorldGenerator(manifest).Generate(c);
        Assert.True(one.Raw.SequenceEqual(two.Raw));
        var local = one[b.Footprint.X + 2 - c.OriginX, GroundLevel - c.OriginY, b.Footprint.Z + 2 - c.OriginZ];
        Assert.Equal(BlockType.Floor, local.Type);
    }

    [Fact]
    public void Chunks_outside_the_world_height_are_empty()
    {
        var gen = new WorldGenerator(ManifestBuilder.Build(TestRepos.Slice(), new Seed(5)));
        Assert.True(gen.Generate(new ChunkCoord(0, -1, 0)).IsEmpty);
        Assert.True(gen.Generate(new ChunkCoord(0, WorldGenerator.MaxChunkY + 1, 0)).IsEmpty);
    }

    [Fact]
    public async Task Streaming_memory_is_bounded_by_radius_while_travelling()
    {
        var manifest = ManifestBuilder.Build(TestRepos.Slice(), new Seed(5));
        var streamer = new ChunkStreamer(new WorldGenerator(manifest), new WorldEdits(), radius: 2);
        int layers = WorldGenerator.MaxChunkY + 1;
        int maxResident = (2 * (2 + StreamHysteresisChunks) + 1) * (2 * (2 + StreamHysteresisChunks) + 1) * layers;

        for (int step = 0; step < 12; step++)
        {
            await streamer.SettleAsync(step * 3 * ChunkSize, 100);
            Assert.InRange(streamer.Loaded.Count, 1, maxResident);
        }
        Assert.Equal(0, streamer.InFlight);
    }

    [Fact]
    public async Task Player_edits_survive_chunk_unload_and_reload()
    {
        var manifest = ManifestBuilder.Build(TestRepos.Slice(), new Seed(5));
        var edits = new WorldEdits();
        var streamer = new ChunkStreamer(new WorldGenerator(manifest), edits, radius: 1);
        edits.Set(5, GroundLevel + 3, 5, new Voxel(BlockType.Wall, MaterialFamily.Wood));

        await streamer.SettleAsync(0, 0);
        Assert.Equal(BlockType.Wall, streamer.Sample(5, GroundLevel + 3, 5)!.Value.Type);
        await streamer.SettleAsync(50 * ChunkSize, 0);
        Assert.Null(streamer.Sample(5, GroundLevel + 3, 5));
        await streamer.SettleAsync(0, 0);
        Assert.Equal(MaterialFamily.Wood, streamer.Sample(5, GroundLevel + 3, 5)!.Value.Material);
    }

    [Fact]
    public void Generation_honours_cancellation()
    {
        var gen = new WorldGenerator(ManifestBuilder.Build(TestRepos.Slice(), new Seed(5)));
        using var cts = new CancellationTokenSource();
        cts.Cancel();
        Assert.Throws<OperationCanceledException>(() => gen.Generate(new ChunkCoord(0, 0, 0), ct: cts.Token));
    }
}