Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ services:
environment:
GameRoot: ${GAME_ROOT}
DistributedTransactions: ${DISTRIBUTED_TRANSACTIONS}
# .NET 8 DATAS: dynamically scales GC heaps to reduce idle memory usage
DOTNET_GCDynamicAdaptationMode: 1
depends_on:
migration:
condition: service_completed_successfully
Expand Down
58 changes: 58 additions & 0 deletions src/Perpetuum.Tests/Unit/CompactPassabilityMaskTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Perpetuum.Zones.Terrains;
using Xunit;

namespace Perpetuum.Tests.Unit
{
public class CompactPassabilityMaskTests
{
[Fact]
public void Bitmask_get_set_and_bounds()
{
var mask = new CompactPassabilityMask(64, 64);

// Default is false (0)
Assert.False(mask.IsWalkable(0, 0));
Assert.False(mask.IsWalkable(10, 10));

// Set some bits
mask.SetWalkable(0, 0, true);
mask.SetWalkable(15, 20, true);
mask.SetWalkable(31, 31, true);
mask.SetWalkable(32, 31, true); // cross 32-bit boundary
mask.SetWalkable(63, 63, true);

Assert.True(mask.IsWalkable(0, 0));
Assert.True(mask.IsWalkable(15, 20));
Assert.True(mask.IsWalkable(31, 31));
Assert.True(mask.IsWalkable(32, 31));
Assert.True(mask.IsWalkable(63, 63));

// Unset a bit
mask.SetWalkable(15, 20, false);
Assert.False(mask.IsWalkable(15, 20));
Assert.True(mask.IsWalkable(0, 0));

// Out of bounds
Assert.False(mask.IsWalkable(-1, 0));
Assert.False(mask.IsWalkable(0, -1));
Assert.False(mask.IsWalkable(64, 0));
Assert.False(mask.IsWalkable(0, 64));
}

[Fact]
public void SetAll_fills_entire_grid()
{
var mask = new CompactPassabilityMask(100, 100);
mask.SetAll(true);

Assert.True(mask.IsWalkable(0, 0));
Assert.True(mask.IsWalkable(50, 50));
Assert.True(mask.IsWalkable(99, 99));

mask.SetAll(false);
Assert.False(mask.IsWalkable(0, 0));
Assert.False(mask.IsWalkable(50, 50));
Assert.False(mask.IsWalkable(99, 99));
}
}
}
264 changes: 264 additions & 0 deletions src/Perpetuum.Tests/Unit/HeightfieldMetadataTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
using Perpetuum.Zones.Terrains;
using Xunit;

namespace Perpetuum.Tests.Unit
{
public class HeightfieldMetadataTests
{
[Fact]
public void Chunk_bounds_and_ray_above_chunk_queries()
{
var rawData = new ushort[64 * 64];
var altLayer = new AltitudeLayer(rawData, 64, 64);

// Fill a chunk (0,0 to 15,15) with height 10.0 (raw: 10 * 32 = 320)
for (int y = 0; y < 16; y++)
{
for (int x = 0; x < 16; x++)
{
altLayer[x, y] = (ushort)(10 * 32);
}
}

// Fill another chunk (16,0 to 31,15) with height 50.0 (raw: 50 * 32 = 1600)
for (int y = 0; y < 16; y++)
{
for (int x = 16; x < 32; x++)
{
altLayer[x, y] = (ushort)(50 * 32);
}
}

var metadata = HeightfieldMetadata.ExtractFrom(altLayer, null, chunkSize: 16);

Assert.Equal(4, metadata.ChunksX);
Assert.Equal(4, metadata.ChunksY);

// Chunk (0,0) has max height 10.0
metadata.GetChunkBounds(0, 0, out float min0, out float max0);
Assert.Equal(10.0f, min0);
Assert.Equal(10.0f, max0);

// Chunk (1,0) has max height 50.0
metadata.GetChunkBounds(1, 0, out float min1, out float max1);
Assert.Equal(50.0f, min1);
Assert.Equal(50.0f, max1);

// Ray at Z=20 is strictly above chunk (0,0), but NOT above chunk (1,0)
Assert.True(metadata.CanRayPassAboveChunk(0, 0, rayMinZ: 20.0f));
Assert.False(metadata.CanRayPassAboveChunk(1, 0, rayMinZ: 20.0f));

// Ray at Z=60 is above both
Assert.True(metadata.CanRayPassAboveChunk(0, 0, rayMinZ: 60.0f));
Assert.True(metadata.CanRayPassAboveChunk(1, 0, rayMinZ: 60.0f));
}

[Fact]
public void Coordinates_mapping()
{
var metadata = new HeightfieldMetadata(2048, 2048, chunkSize: 16);
Assert.Equal(128, metadata.ChunksX);
Assert.Equal(128, metadata.ChunksY);

metadata.GetChunkCoordinates(0, 0, out int cx0, out int cy0);
Assert.Equal(0, cx0);
Assert.Equal(0, cy0);

metadata.GetChunkCoordinates(15, 15, out int cx1, out int cy1);
Assert.Equal(0, cx1);
Assert.Equal(0, cy1);

metadata.GetChunkCoordinates(16, 32, out int cx2, out int cy2);
Assert.Equal(1, cx2);
Assert.Equal(2, cy2);

metadata.GetChunkCoordinates(2047, 2047, out int cx3, out int cy3);
Assert.Equal(127, cx3);
Assert.Equal(127, cy3);
}

[Fact]
public void Mark_dirty_tile_flags_only_its_chunk_and_deduplicates()
{
var metadata = new HeightfieldMetadata(64, 64, chunkSize: 16);

Assert.False(metadata.HasDirtyChunks);

metadata.MarkDirtyTile(5, 5);
Assert.Equal(1, metadata.DirtyChunkCount);

// Same chunk again: no double count.
metadata.MarkDirtyTile(15, 15);
Assert.Equal(1, metadata.DirtyChunkCount);

// Different chunk.
metadata.MarkDirtyTile(20, 20);
Assert.Equal(2, metadata.DirtyChunkCount);

// Out of bounds: ignored.
metadata.MarkDirtyTile(-1, 0);
metadata.MarkDirtyTile(64, 0);
metadata.MarkDirtyTile(0, -5);
metadata.MarkDirtyTile(0, 100);
Assert.Equal(2, metadata.DirtyChunkCount);
}

[Fact]
public void Mark_dirty_area_covers_exactly_the_intersecting_chunks()
{
var metadata = new HeightfieldMetadata(64, 64, chunkSize: 16);

// Area spanning chunks (0,0), (1,0), (0,1), (1,1) and clipping chunk (2,0)/(0,2).
metadata.MarkDirtyArea(new Area(10, 10, 37, 37));
Assert.Equal(9, metadata.DirtyChunkCount);

// Fully outside: ignored.
metadata.MarkDirtyArea(new Area(100, 100, 110, 110));
metadata.MarkDirtyArea(new Area(-50, -50, -10, -10));
Assert.Equal(9, metadata.DirtyChunkCount);

// Negative area that still intersects the map: clamped, no out-of-range chunks.
var clamped = new HeightfieldMetadata(64, 64, chunkSize: 16);
clamped.MarkDirtyArea(new Area(-30, -30, 5, 5));
Assert.Equal(1, clamped.DirtyChunkCount);

// Fully outside (all negative): ignored.
clamped.MarkDirtyArea(new Area(-30, -30, -1, -1));
Assert.Equal(1, clamped.DirtyChunkCount);

// Area beyond the edge: clamped to the last chunk row/column.
var edge = new HeightfieldMetadata(64, 64, chunkSize: 16);
edge.MarkDirtyArea(new Area(60, 60, 200, 200));
Assert.Equal(1, edge.DirtyChunkCount);
edge.GetChunkBounds(3, 3, out float min, out float max);
Assert.Equal(0f, min);
}

[Fact]
public void Recompute_dirty_rebakes_only_flagged_chunks_and_clears_the_flags()
{
var rawData = new ushort[64 * 64];
var altLayer = new AltitudeLayer(rawData, 64, 64);
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
altLayer[x, y] = (ushort)(10 * 32);
}
}

var metadata = HeightfieldMetadata.ExtractFrom(altLayer, null, chunkSize: 16);
Assert.False(metadata.HasDirtyChunks);
Assert.Equal(0, metadata.RecomputeDirty(altLayer));

// Raise one tile in chunk (0,0) and mark it.
altLayer[5, 5] = (ushort)(100 * 32);
metadata.MarkDirtyTile(5, 5);
Assert.Equal(1, metadata.DirtyChunkCount);

// Until the re-bake the chunk bound is stale.
metadata.GetChunkBounds(0, 0, out _, out float staleMax);
Assert.Equal(10f, staleMax);

int rebaked = metadata.RecomputeDirty(altLayer);
Assert.Equal(1, rebaked);
Assert.False(metadata.HasDirtyChunks);

metadata.GetChunkBounds(0, 0, out float min, out float max);
Assert.Equal(10f, min);
Assert.Equal(100f, max);

// Untouched chunks keep their bounds and nothing is re-baked on a second drain.
metadata.GetChunkBounds(1, 0, out _, out float otherMax);
Assert.Equal(10f, otherMax);
Assert.Equal(0, metadata.RecomputeDirty(altLayer));
}

[Fact]
public void Plant_blocking_height_is_picked_up_by_dirty_recompute()
{
var rawData = new ushort[64 * 64];
var altLayer = new AltitudeLayer(rawData, 64, 64);
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
altLayer[x, y] = (ushort)(10 * 32);
}
}

var blockLayer = new Layer<BlockingInfo>(LayerType.Blocks, 64, 64);
var metadata = HeightfieldMetadata.ExtractFrom(altLayer, blockLayer, chunkSize: 16);

metadata.GetChunkBounds(2, 2, out _, out float before);
Assert.Equal(10f, before);

// A plant grows on tile (40, 40) (chunk (2,2)) with blocking height 15, the way
// NatureCube commits it: a single SetArea over the scanned cube.
blockLayer[40, 40] = new BlockingInfo(BlockingFlags.Plant, 15);
metadata.MarkDirtyArea(new Area(32, 32, 47, 47));
Assert.Equal(1, metadata.DirtyChunkCount);

metadata.RecomputeDirty(altLayer, blockLayer);

metadata.GetChunkBounds(2, 2, out float min, out float max);
Assert.Equal(10f, min);
Assert.Equal(25f, max);

// The plant wilts: the same area is re-scanned with no blocking height.
blockLayer[40, 40] = BlockingInfo.None;
metadata.MarkDirtyArea(new Area(32, 32, 47, 47));
metadata.RecomputeDirty(altLayer, blockLayer);

metadata.GetChunkBounds(2, 2, out _, out float after);
Assert.Equal(10f, after);
}

[Fact]
public void Global_max_height_tracks_full_and_incremental_rebakes()
{
var rawData = new ushort[64 * 64];
var altLayer = new AltitudeLayer(rawData, 64, 64);
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
altLayer[x, y] = (ushort)(10 * 32);
}
}

var metadata = HeightfieldMetadata.ExtractFrom(altLayer, null, chunkSize: 16);
Assert.Equal(10f, metadata.GlobalMaxHeight);

// Raise one tile, re-bake incrementally: the global max follows it up...
altLayer[5, 5] = (ushort)(100 * 32);
metadata.MarkDirtyTile(5, 5);
metadata.RecomputeDirty(altLayer);
Assert.Equal(100f, metadata.GlobalMaxHeight);

// ...and back down when the tile is levelled again.
altLayer[5, 5] = (ushort)(10 * 32);
metadata.MarkDirtyTile(5, 5);
metadata.RecomputeDirty(altLayer);
Assert.Equal(10f, metadata.GlobalMaxHeight);
}

[Fact]
public void Recompute_all_clears_dirty_flags()
{
var rawData = new ushort[64 * 64];
var altLayer = new AltitudeLayer(rawData, 64, 64);
var metadata = HeightfieldMetadata.ExtractFrom(altLayer, null, chunkSize: 16);

metadata.MarkDirtyTile(0, 0);
metadata.MarkDirtyTile(63, 63);
Assert.Equal(2, metadata.DirtyChunkCount);

metadata.RecomputeAll(altLayer);

Assert.False(metadata.HasDirtyChunks);
Assert.Equal(0, metadata.DirtyChunkCount);
Assert.Equal(0, metadata.RecomputeDirty(altLayer));
}
}
}
Loading
Loading