using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using Server.Engines.Pathing.Cache; namespace Server.Tests.Terrain; /// /// Stamps land and statics into the synthetic terrain. Felucca and Trammel share one terrain, so every write /// goes to both. Once has run, writes are only allowed inside a /// , which puts back every block it touched when disposed. /// public static class TestTerrain { // Blocks this class allocated, keyed by matrix and block. Anything else is the matrix's shared default // block, which must never be written to. private static readonly Dictionary<(TileMatrix, int, int), LandTile[]> _land = []; private static readonly Dictionary<(TileMatrix, int, int), StaticTile[][][]> _statics = []; private static TerrainScope _scope; internal static bool Sealed { get; set; } /// Filled in by the compiler; names the opener if the scope is left open. /// Filled in by the compiler. public static TerrainScope Scope([CallerMemberName] string caller = null, [CallerFilePath] string callerFile = null) { if (_scope != null) { throw new InvalidOperationException( $"A terrain scope opened by {_scope.Opener} is still open; dispose it (a using) first." ); } // Cached step masks describe the terrain as it was; drop them so nothing serves the old cells. StepCache.Instance.Clear(); return _scope = new TerrainScope($"{Path.GetFileNameWithoutExtension(callerFile)}.{caller}"); } public static void SetLand(int x, int y, short id, sbyte z) { var facets = Facets(); for (var i = 0; i < facets.Length; i++) { LandBlock(facets[i].Tiles, x >> 3, y >> 3)[((y & 7) << 3) + (x & 7)] = new LandTile(id, z); } } public static void FillLand(Rectangle2D area, short id, sbyte z) { for (var x = area.Start.X; x < area.End.X; x++) { for (var y = area.Start.Y; y < area.End.Y; y++) { SetLand(x, y, id, z); } } } public static void AddStatic(int x, int y, ushort id, sbyte z) { var facets = Facets(); for (var i = 0; i < facets.Length; i++) { var column = StaticBlock(facets[i].Tiles, x >> 3, y >> 3)[x & 7]; column[y & 7] = [..column[y & 7], new StaticTile(id, z)]; } } public static void ClearStatics(int x, int y) { var facets = Facets(); for (var i = 0; i < facets.Length; i++) { StaticBlock(facets[i].Tiles, x >> 3, y >> 3)[x & 7][y & 7] = []; } } internal static void EndScope(TerrainScope scope) { foreach (var ((tiles, bx, by), original) in scope.Land) { if (original == null) { _land.Remove((tiles, bx, by)); } else { _land[(tiles, bx, by)] = original; } tiles.SetLandBlock(bx, by, original); } foreach (var ((tiles, bx, by), original) in scope.Statics) { if (original == null) { _statics.Remove((tiles, bx, by)); } else { _statics[(tiles, bx, by)] = original; } tiles.SetStaticBlock(bx, by, original); } _scope = null; StepCache.Instance.Clear(); } private static Map[] Facets() { if (Sealed && _scope == null) { throw new InvalidOperationException("Stamp terrain inside TestTerrain.Scope() so it is put back afterwards."); } // A test may query the cache and then stamp more; masks built before the stamp must not survive it. if (_scope != null) { StepCache.Instance.Clear(); } return [Map.Felucca, Map.Trammel]; } private static LandTile[] LandBlock(TileMatrix tiles, int bx, int by) { var key = (tiles, bx, by); _land.TryGetValue(key, out var owned); // The scope keeps the block as it was and the stamp goes into a copy, so disposing is a swap back. if (_scope != null && !_scope.Land.ContainsKey(key)) { _scope.Land[key] = owned; owned = (LandTile[])owned?.Clone(); } else if (owned != null) { return owned; } var block = owned ?? new LandTile[64]; _land[key] = block; tiles.SetLandBlock(bx, by, block); return block; } private static StaticTile[][][] StaticBlock(TileMatrix tiles, int bx, int by) { var key = (tiles, bx, by); _statics.TryGetValue(key, out var owned); if (_scope != null && !_scope.Statics.ContainsKey(key)) { _scope.Statics[key] = owned; owned = owned == null ? null : CopyStatics(owned); } else if (owned != null) { return owned; } var block = owned ?? CopyStatics(tiles.EmptyStaticBlock); _statics[key] = block; tiles.SetStaticBlock(bx, by, block); return block; } private static StaticTile[][][] CopyStatics(StaticTile[][][] source) { var copy = new StaticTile[8][][]; for (var x = 0; x < 8; x++) { copy[x] = new StaticTile[8][]; for (var y = 0; y < 8; y++) { copy[x][y] = source[x][y]; } } return copy; } } /// /// Bespoke terrain for one test. Dispose it (a using) to put back every block it touched. /// public sealed class TerrainScope : IDisposable { // The block each key held before this scope first wrote to it; null means it was the default. internal Dictionary<(TileMatrix, int, int), LandTile[]> Land { get; } = []; internal Dictionary<(TileMatrix, int, int), StaticTile[][][]> Statics { get; } = []; private bool _disposed; internal TerrainScope(string opener) => Opener = opener; /// The test that opened this scope, as Class.Method. public string Opener { get; } public void Dispose() { if (_disposed) { return; } _disposed = true; TestTerrain.EndScope(this); } }