diff --git a/CentrED/Data/Languages/Czech.txt b/CentrED/Data/Languages/Czech.txt index ccffece6..0565b0cd 100644 --- a/CentrED/Data/Languages/Czech.txt +++ b/CentrED/Data/Languages/Czech.txt @@ -235,4 +235,11 @@ USE_MAIN_CLIENT_TOOLTIP1=Aplikace se pozastaví po dobu operace USE_MAIN_CLIENT_TOOLTIP2=Výkon bude horší než při použití druhého účtu SECONDARY_CREDENTIALS=Pomocný účet TEST_CONNECTION=Otestovat připojení +LSO_IMPORT_COLORED_HEIGHTMAP=Importovat barevnou výškovou mapu +IMPORT_BIOME_MAP=Soubor mapy biomů (barevný): +IMPORT_VEGETATION=Importovat vegetaci +IMPORT_VEGETATION_TOOLTIP=Importovat také vegetaci ze samostatného souboru mapy vegetace +IMPORT_VEGETATION_MAP=Soubor mapy vegetace: +CLEAR_EXISTING_STATICS=Vymazat existující objekty +CLEAR_EXISTING_STATICS_TOOLTIP=Odstranit existující statické objekty před umístěním vegetace SNAP_TO_TERRAIN=Přichytit k terénu diff --git a/CentrED/Data/Languages/English.txt b/CentrED/Data/Languages/English.txt index ab17b4d8..7568b2aa 100644 --- a/CentrED/Data/Languages/English.txt +++ b/CentrED/Data/Languages/English.txt @@ -260,4 +260,11 @@ SMOOTH_EDGE=Smooth edge SMOOTH_EDGE_TOOLTIP=Edges of set width will be smoothened according to surrounding terrain BLEND_FACTOR=Blend factor BLEND_FACTOR_TOOLTIP=How much should we blend with current terrain altitude -SNAP_TO_TERRAIN=Snap to terrain \ No newline at end of file +LSO_IMPORT_COLORED_HEIGHTMAP=Import colored heightmap +IMPORT_BIOME_MAP=Biome map file (colored): +IMPORT_VEGETATION=Import vegetation +IMPORT_VEGETATION_TOOLTIP=Also import vegetation from a separate vegetation map file +IMPORT_VEGETATION_MAP=Vegetation map file: +CLEAR_EXISTING_STATICS=Clear existing statics +CLEAR_EXISTING_STATICS_TOOLTIP=Remove existing static objects before placing vegetation +SNAP_TO_TERRAIN=Snap to terrain diff --git a/CentrED/Data/Languages/Polski.txt b/CentrED/Data/Languages/Polski.txt index 4c65849d..74cfd1e1 100644 --- a/CentrED/Data/Languages/Polski.txt +++ b/CentrED/Data/Languages/Polski.txt @@ -243,4 +243,11 @@ OVERWRITE=Nadpisz SAVE_BLUEPRINT_OVERWRITE_TOOLTIP=Gdy zaznaczone, a szablon już istnieje, zostanie on nadpisany EMPTY_NAME_ERROR=Nazwa nie może być pusta FILE_ALREADY_EXISTS=Plik już istnieje -SNAP_TO_TERRAIN=Przyciągnij do terenu \ No newline at end of file +LSO_IMPORT_COLORED_HEIGHTMAP=Importuj kolorową mapę wysokości +IMPORT_BIOME_MAP=Plik mapy biomów (kolorowy): +IMPORT_VEGETATION=Importuj roślinność +IMPORT_VEGETATION_TOOLTIP=Importuj również roślinność z osobnego pliku mapy roślinności +IMPORT_VEGETATION_MAP=Plik mapy roślinności: +CLEAR_EXISTING_STATICS=Wyczyść istniejące obiekty +CLEAR_EXISTING_STATICS_TOOLTIP=Usuń istniejące obiekty statyczne przed umieszczeniem roślinności +SNAP_TO_TERRAIN=Przyciągnij do terenu diff --git a/CentrED/Languages/LangEntry.cs b/CentrED/Languages/LangEntry.cs index 4972f746..9eaee503 100644 --- a/CentrED/Languages/LangEntry.cs +++ b/CentrED/Languages/LangEntry.cs @@ -264,5 +264,12 @@ public enum LangEntry SMOOTH_EDGE_TOOLTIP, BLEND_FACTOR, BLEND_FACTOR_TOOLTIP, + LSO_IMPORT_COLORED_HEIGHTMAP, + IMPORT_BIOME_MAP, + IMPORT_VEGETATION, + IMPORT_VEGETATION_TOOLTIP, + IMPORT_VEGETATION_MAP, + CLEAR_EXISTING_STATICS, + CLEAR_EXISTING_STATICS_TOOLTIP, SNAP_TO_TERRAIN, } \ No newline at end of file diff --git a/CentrED/Tools/CoastlineTool.cs b/CentrED/Tools/CoastlineTool.cs index be938d50..8744ed21 100644 --- a/CentrED/Tools/CoastlineTool.cs +++ b/CentrED/Tools/CoastlineTool.cs @@ -329,4 +329,273 @@ public override void GrabZ(sbyte z) { _waterZ = z; } + + /// + /// Result from ApplyCoastlineAt containing static tile, terrain modifications, and push info. + /// + public record CoastlineResult( + StaticTile? StaticTile, + sbyte? NewTerrainZ, + ushort? NewLandTileId = null, // Replace current tile with this ID (e.g., shore tile 0x0042) + (ushort x, ushort y, ushort id)? PushTile = null // Push original tile to this location + ); + + /// + /// Apply coastline to a tile. Can be called from Large Scale Operations. + /// Returns the static tile to add and optional terrain Z modification. + /// + public static CoastlineResult ApplyCoastlineAt(CentrED.Client.CentrEDClient client, ushort x, ushort y, sbyte waterZ = -5, bool tweakTerrain = true) + { + var landTile = client.GetLandTile(x, y); + + // Water terrain tiles + HashSet terrainWaterTiles = [0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x0136, 0x0137]; + + // Shore/bottom tiles + HashSet terrainBottomTiles = Enumerable.Range(0x004C, 0x006F - 0x004C + 1) + .Select(i => (ushort)i).ToHashSet(); + + // Border tiles - land tiles placed on water pixels (all biomes) + HashSet borderTiles = + [ + // Grass border tiles + 0x0093, 0x0096, 0x0098, 0x0099, 0x008E, 0x0095, 0x00A0, + // Forest border tiles + 0x02EE, 0x02EF, 0x02F0, 0x02F1, 0x02F2, 0x02F3, 0x00C5 + ]; + + // Skip if this tile IS water - coastline statics go on LAND tiles adjacent to water + bool isWaterTile = terrainWaterTiles.Contains(landTile.Id) || terrainBottomTiles.Contains(landTile.Id); + if (isWaterTile) + return new CoastlineResult(null, null); + + // Transition tiles - directions indicate where WATER is relative to land tile + List transitionTiles = + [ + new(Direction.West, Direction.Left | Direction.Up, [0x179D, 0x179E]), + new(Direction.South, Direction.Left | Direction.Down, [0x179F, 0x17A0]), + new(Direction.North, Direction.Up | Direction.Right, [0x17A1, 0x17A2]), + new(Direction.East, Direction.Right | Direction.Down, [0x17A3, 0x17A4]), + new(Direction.Left, Direction.None, [0x17A5]), + new(Direction.Down, Direction.None, [0x17A6]), + new(Direction.Up, Direction.None, [0x17A7]), + new(Direction.Right, Direction.None, [0x17A8]), + new(Direction.South | Direction.Left | Direction.West, Direction.Down | Direction.Up, [0x17A9]), + new(Direction.West | Direction.Up | Direction.North, Direction.Left | Direction.Right, [0x17AA]), + new(Direction.North | Direction.Right | Direction.East, Direction.Down | Direction.Up, [0x17AB]), + new(Direction.East | Direction.Down | Direction.South, Direction.Left | Direction.Right, [0x17AC]) + ]; + + ushort[] objectWaterTiles = [0x1797, 0x1798, 0x1799, 0x179A, 0x179B, 0x179C]; + + Direction[] sideUpEdge = + [ + Direction.Left | Direction.West, + Direction.Right | Direction.North + ]; + + // Get water direction - find which directions have PURE water adjacent to this land tile + // Only check for actual water tiles, not shore/bottom tiles (to avoid cascade effect) + Direction GetWaterDir(ushort tx, ushort ty) + { + var result = Direction.None; + foreach (var dir in DirectionHelper.All) + { + var offset = dir.Offset(); + var nx = tx + offset.Item1; + var ny = ty + offset.Item2; + + // Skip if out of bounds + if (nx < 0 || ny < 0 || nx > ushort.MaxValue || ny > ushort.MaxValue) + continue; + + try + { + var neighbor = client.GetLandTile((ushort)nx, (ushort)ny); + // Only check for PURE water tiles, not shore/bottom tiles + bool isWater = terrainWaterTiles.Contains(neighbor.Id); + if (isWater) + { + result |= dir; + } + } + catch + { + // Skip if coordinates are out of map bounds + } + } + return result; + } + + var selectedDirection = GetWaterDir(x, y); + if (selectedDirection == Direction.None) + return new CoastlineResult(null, null); + + // Calculate terrain Z modification (same logic as GhostApply lines 194-228) + sbyte? newLandZ = null; + if (tweakTerrain) + { + var currentZ = landTile.Z; + sbyte calculatedZ = currentZ; + + if (terrainBottomTiles.Contains(landTile.Id) || + selectedDirection.HasFlag(Direction.West | Direction.Up | Direction.North) || + (!selectedDirection.HasFlag(Direction.Up) && sideUpEdge.Any(e => selectedDirection.HasFlag(e)))) + { + calculatedZ = (sbyte)(waterZ - 10); + } + else if (selectedDirection == Direction.Up) + { + calculatedZ = waterZ; // Otherwise water sticks through terrain on up facing land edge + } + else if (selectedDirection.HasFlag(Direction.Up) || sideUpEdge.Any(e => e == selectedDirection)) + { + calculatedZ = (sbyte)(waterZ - 4); + } + else if (!selectedDirection.HasFlag(Direction.Down) && sideUpEdge.Any(c => selectedDirection.HasFlag(c))) + { + calculatedZ = (sbyte)(waterZ - 10); + } + else if (selectedDirection is Direction.Left or Direction.Right) + { + calculatedZ = (sbyte)(waterZ + 2); // To not hide statics because of terrain + } + + if (calculatedZ != currentZ) + { + newLandZ = calculatedZ; + } + } + + // Get context tile (tile "above" in UO terms) + var contextOffset = Direction.Up.Offset(); + var ctxX = x + contextOffset.Item1; + var ctxY = y + contextOffset.Item2; + + // Check bounds for context tile + if (ctxX < 0 || ctxY < 0 || ctxX > ushort.MaxValue || ctxY > ushort.MaxValue) + return new CoastlineResult(null, null); + + var contextX = (ushort)ctxX; + var contextY = (ushort)ctxY; + LandTile contextTile; + try + { + contextTile = client.GetLandTile(contextX, contextY); + } + catch + { + return new CoastlineResult(null, null); + } + + var contextDirection = GetWaterDir(contextX, contextY); + + // Context adjustment logic from CoastlineTool + if (contextDirection.HasFlag(Direction.Up) || sideUpEdge.Any(e => e == contextDirection || e == selectedDirection)) + { + contextDirection = selectedDirection; + contextTile = landTile; + } + + // Determine biome tile for pushing inland + // Grass biome: 0x0003-0x0006, 0x037B-0x037E -> push 0x0004 + // Forest biome: 0x00C4-0x00C7, 0x02EE-0x02F3 -> push 0x00C5 + // Shore tile at water edge is ALWAYS 0x0095 + HashSet grassTiles = + [ + 0x0003, 0x0004, 0x0005, 0x0006, // Grass base tiles + 0x037B, 0x037C, 0x037D, 0x037E, // Grass variants + 0x0093, 0x0096, 0x0098, 0x0099, 0x008E, 0x0095, 0x00A0 // Grass border tiles + ]; + HashSet forestTiles = + [ + 0x00C4, 0x00C5, 0x00C6, 0x00C7, // Forest base tiles + 0x02EE, 0x02EF, 0x02F0, 0x02F1, 0x02F2, 0x02F3 // Forest border tiles + ]; + + // Shore tile at water edge is always 0x0095 + const ushort shoreTile = 0x0095; + + // Biome tile to push inland + ushort biomeTile; + if (forestTiles.Contains(landTile.Id)) + { + biomeTile = 0x00C5; // Forest biome tile + } + else if (grassTiles.Contains(landTile.Id)) + { + biomeTile = 0x0004; // Grass biome tile + } + else + { + // Default to grass for unknown biomes + biomeTile = 0x0004; + } + + // Find primary water direction for push calculation + Direction? primaryDir = null; + foreach (var dir in new[] { Direction.West, Direction.South, Direction.North, Direction.East, + Direction.Left, Direction.Down, Direction.Up, Direction.Right }) + { + if (selectedDirection.HasFlag(dir)) + { + primaryDir = dir; + break; + } + } + + if (primaryDir == null) + return new CoastlineResult(null, null); + + // Calculate push direction (opposite of water direction) + var oppositeDir = primaryDir.Value switch + { + Direction.West => Direction.East, + Direction.East => Direction.West, + Direction.North => Direction.South, + Direction.South => Direction.North, + Direction.Up => Direction.Down, + Direction.Down => Direction.Up, + Direction.Left => Direction.Right, + Direction.Right => Direction.Left, + _ => Direction.None + }; + + // Get push target coordinates - push BIOME tile inland (not original tile) + (ushort x, ushort y, ushort id)? pushTile = null; + if (oppositeDir != Direction.None) + { + var pushOffset = oppositeDir.Offset(); + var pushX = x + pushOffset.Item1; + var pushY = y + pushOffset.Item2; + + if (pushX >= 0 && pushY >= 0 && pushX <= ushort.MaxValue && pushY <= ushort.MaxValue) + { + // Push the biome-appropriate tile (0x0004 grass, 0x00C5 forest) inland + pushTile = ((ushort)pushX, (ushort)pushY, biomeTile); + } + } + + // Get wave static based on water direction + ushort waveStaticId = transitionTiles + .Where(m => selectedDirection.HasFlag(m.fullMatch)) + .Where(m => (selectedDirection & ~(m.fullMatch | m.partialMatch)) == Direction.None) + .Select(m => m.tileIds[Random.Shared.Next(m.tileIds.Length)]) + .FirstOrDefault((ushort)0); + + // If no transition tile matches, use plain water objects as fallback + if (waveStaticId == 0 && selectedDirection != Direction.None) + { + waveStaticId = objectWaterTiles[Random.Shared.Next(objectWaterTiles.Length)]; + } + + // Return result: replace current tile with shore tile, add wave static on top of shore + // Shore tile goes at Z=-15 (below water level), wave static on shore tile at Z=-5 + return new CoastlineResult( + waveStaticId != 0 ? new StaticTile(waveStaticId, x, y, waterZ, 0) : null, // Wave static on shore tile + (sbyte)(waterZ - 10), // Set Z to -15 (waterZ is -5, so -5 - 10 = -15) + shoreTile, // Replace with shore tile 0x0066 + pushTile // Push original tile inland + ); + } } \ No newline at end of file diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Biomes.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Biomes.cs new file mode 100644 index 00000000..89836583 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Biomes.cs @@ -0,0 +1,342 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Partial class containing biome classification logic. +/// +public partial class ImportColoredHeightmap +{ + private enum Biome + { + // Natural terrain + Water, Sand, Grass, Dirt, Jungle, Forest, Swamp, Rock, Snow, Lava, Cave, Void, + // Stone/dungeon floors (new) + SandstoneFloor, MarbleBrown, MarbleBlue, Cobblestone, StoneGray, Brick, MarbleWhite, MarbleGreen, Acid, + // Border tiles for grass/forest coastlines - ALL are land tiles on WATER PIXELS + // All directions place tiles on water pixels adjacent to land: + GrassBorderN, GrassBorderS, GrassBorderE, GrassBorderW, GrassBorderNW, GrassBorderSE, + ForestBorderN, ForestBorderS, ForestBorderE, ForestBorderW, ForestBorderNW, ForestBorderSE, + // Corner tiles (0x0095) - SW, NE, ES directions: + CornerTileSW, CornerTileNE, CornerTileES, + Unknown + } + + #region Border Tile Detection + + /// + /// Border tile color patterns from UOMapMorph: + /// ALL border tiles use R=150, B=95, G varies by direction and biome + /// All tiles are placed on WATER PIXELS adjacent to land. + /// + private const int BorderTileR = 150; + private const int BorderTileB = 95; + + /// + /// Check if color is a border tile (placed on water pixel). + /// ALL border tiles use color scheme: R=150, B=95, G varies. + /// - Grass tiles: G=140(N), 142(S), 144(E), 130(W), 150(NW), 160(SE) + /// - Forest tiles: G=141(N), 143(S), 145(E), 131(W), 151(NW), 161(SE) + /// - Corner tiles (0x0095): G=100(SW), 110(NE), 120(ES) + /// + private static Biome? CheckBorderTileColor(byte r, byte g, byte b) + { + if (Math.Abs(r - BorderTileR) > 5 || Math.Abs(b - BorderTileB) > 5) + return null; + + return g switch + { + // Corner tiles (0x0095) - same for grass and forest + 100 => Biome.CornerTileSW, // G=100 + 110 => Biome.CornerTileNE, // G=110 + 120 => Biome.CornerTileES, // G=120 + // Forest tiles (exact G values) - must come before grass ranges + 131 => Biome.ForestBorderW, // G=131 + 151 => Biome.ForestBorderNW, // G=151 + 141 => Biome.ForestBorderN, // G=141 + 143 => Biome.ForestBorderS, // G=143 + 145 => Biome.ForestBorderE, // G=145 + 161 => Biome.ForestBorderSE, // G=161 + // Grass tiles (exact G values) + 130 => Biome.GrassBorderW, // G=130 + 150 => Biome.GrassBorderNW, // G=150 + 140 => Biome.GrassBorderN, // G=140 + 142 => Biome.GrassBorderS, // G=142 + 144 => Biome.GrassBorderE, // G=144 + 160 => Biome.GrassBorderSE, // G=160 + _ => null + }; + } + + #endregion + + private (Biome biome, sbyte altitude) ClassifyBiomeColor(byte r, byte g, byte b) + { + // Check for void/black first (r,g,b all very low) + if (r < 15 && g < 15 && b < 15) + { + return (Biome.Void, 0); + } + + // Check for border tile colors FIRST (all placed on water pixels) + // Border tiles use water Z level (-5) since they replace water tiles + var borderTile = CheckBorderTileColor(r, g, b); + if (borderTile.HasValue) + { + return (borderTile.Value, -5); + } + + // Check for water - blue dominant + if (b > 100 && b > r + 50 && b > g + 50) + { + return (Biome.Water, -5); + } + + // Check for EXACT arena floor tile colors BEFORE grayscale check + if (r == 105 && g == 105 && b == 105) + return (Biome.Cobblestone, 0); + if (r == 128 && g == 128 && b == 128) + return (Biome.StoneGray, 0); + if (r == 245 && g == 245 && b == 245) + return (Biome.MarbleWhite, 0); + if (r == 160 && g == 160 && b == 160) + return (Biome.StoneGray, 0); + + // Pure black (0,0,0) is void + if (r == 0 && g == 0 && b == 0) + return (Biome.Void, 0); + + // Check for grayscale - snow/rock for world maps + if (IsGrayscale(r, g, b)) + { + var gray = (r + g + b) / 3; + + if (gray >= 200) + { + return (Biome.Snow, 0); + } + + if (gray >= 64 && gray <= 127) + { + return (Biome.Rock, 0); + } + } + + // Match against biome colors + var biome = MatchBiomeColor(r, g, b); + sbyte landAltitude = (biome == Biome.Water) ? (sbyte)-5 : (sbyte)0; + + return (biome, landAltitude); + } + + /// + /// Classify biome from flat color only (for use with separate heightmap). + /// + private static Biome ClassifyBiomeColorOnly(byte r, byte g, byte b) + { + // Check for void/black first + if (r < 15 && g < 15 && b < 15) + { + return Biome.Void; + } + + // Check for border tile colors FIRST (all placed on water pixels) + var borderTile = CheckBorderTileColor(r, g, b); + if (borderTile.HasValue) + { + return borderTile.Value; + } + + // Check for water FIRST - blue dominant + if (b > 100 && b > r + 50 && b > g + 50) + { + return Biome.Water; + } + + // Check for EXACT arena floor colors FIRST + if (r == 105 && g == 105 && b == 105) + return Biome.Cobblestone; + if (r == 128 && g == 128 && b == 128) + return Biome.StoneGray; + if (r == 160 && g == 160 && b == 160) + return Biome.StoneGray; + if (r == 245 && g == 245 && b == 245) + return Biome.MarbleWhite; + + // Check for grayscale colors (rock/snow for world maps) + if (IsGrayscale(r, g, b)) + { + var gray = (r + g + b) / 3; + + if (gray >= 200) + return Biome.Snow; + + if (gray >= 64 && gray <= 127) + return Biome.Rock; + } + + // Use color distance matching against exact colors + var biomeColors = new (Biome biome, int cr, int cg, int cb)[] + { + (Biome.Water, 0, 50, 180), + (Biome.Forest, 80, 120, 50), + (Biome.Grass, 60, 140, 60), + (Biome.Swamp, 70, 90, 50), + (Biome.Jungle, 120, 180, 40), + (Biome.Dirt, 140, 100, 60), + (Biome.Sand, 210, 180, 120), + (Biome.Snow, 220, 220, 230), + (Biome.Lava, 200, 60, 20), + (Biome.Cave, 60, 50, 45), + (Biome.Rock, 100, 100, 100), + (Biome.SandstoneFloor, 194, 178, 128), + (Biome.MarbleBrown, 139, 90, 43), + (Biome.MarbleBlue, 70, 130, 180), + (Biome.Cobblestone, 105, 105, 105), + (Biome.StoneGray, 128, 128, 128), + (Biome.StoneGray, 160, 160, 160), + (Biome.Brick, 178, 34, 34), + (Biome.MarbleWhite, 245, 245, 245), + (Biome.MarbleGreen, 60, 179, 113), + (Biome.Acid, 127, 255, 0), + }; + + Biome bestMatch = Biome.Grass; + double bestDistance = double.MaxValue; + + foreach (var (biome, cr, cg, cb) in biomeColors) + { + var distance = Math.Sqrt( + Math.Pow(r - cr, 2) + + Math.Pow(g - cg, 2) + + Math.Pow(b - cb, 2) + ); + + if (distance < bestDistance) + { + bestDistance = distance; + bestMatch = biome; + } + } + + return bestMatch; + } + + private static bool IsGrayscale(byte r, byte g, byte b) + { + const int tolerance = 20; + var maxDiff = Math.Max(Math.Max(Math.Abs(r - g), Math.Abs(g - b)), Math.Abs(r - b)); + var isGreenDominant = g > r + 20 && g > b + 20; + var max = Math.Max(Math.Max(r, g), b); + var min = Math.Min(Math.Min(r, g), b); + var saturation = max > 0 ? (max - min) / (float)max : 0; + + return maxDiff <= tolerance && !isGreenDominant && saturation < 0.3f; + } + + private static Biome MatchBiomeColor(byte r, byte g, byte b) + { + if (IsGrayscale(r, g, b)) + { + var gray = (r + g + b) / 3; + if (gray < 128) + return Biome.Rock; + else + return Biome.Snow; + } + + var biomeColors = new (Biome biome, int r, int g, int b)[] + { + (Biome.Water, 0, 50, 180), + (Biome.Sand, 210, 180, 120), + (Biome.Grass, 60, 140, 60), + (Biome.Dirt, 140, 100, 60), + (Biome.Jungle, 120, 180, 40), + (Biome.Forest, 80, 120, 50), + (Biome.Swamp, 70, 90, 50), + (Biome.Lava, 200, 60, 20), + (Biome.Cave, 60, 50, 45), + (Biome.Rock, 100, 100, 100), + (Biome.Snow, 220, 220, 230), + (Biome.SandstoneFloor, 194, 178, 128), + (Biome.MarbleBrown, 139, 90, 43), + (Biome.MarbleBlue, 70, 130, 180), + (Biome.Cobblestone, 105, 105, 105), + (Biome.StoneGray, 128, 128, 128), + (Biome.Brick, 178, 34, 34), + (Biome.MarbleWhite, 245, 245, 245), + (Biome.MarbleGreen, 60, 179, 113), + (Biome.Acid, 127, 255, 0), + }; + + Biome bestMatch = Biome.Grass; + double bestDistance = double.MaxValue; + + foreach (var (biome, br, bg, bb) in biomeColors) + { + var distance = Math.Sqrt( + Math.Pow(r - br, 2) + + Math.Pow(g - bg, 2) + + Math.Pow(b - bb, 2) + ); + + if (distance < bestDistance) + { + bestDistance = distance; + bestMatch = biome; + } + } + + if (bestDistance > 100) + return Biome.Unknown; + + return bestMatch; + } + + private ushort GetLandTileForBiome(Biome biome) + { + var tiles = biome switch + { + Biome.Water => WaterTiles, + Biome.Sand => SandTiles, + Biome.Grass => GrassTiles, + Biome.Dirt => DirtTiles, + Biome.Jungle => JungleTiles, + Biome.Forest => ForestTiles, + Biome.Swamp => SwampTiles, + Biome.Rock => RockTiles, + Biome.Snow => SnowTiles, + Biome.Lava => LavaTiles, + Biome.Cave => CaveTiles, + Biome.Void => VoidTiles, + Biome.SandstoneFloor => SandstoneFloorTiles, + Biome.MarbleBrown => MarbleBrownFloorTiles, + Biome.MarbleBlue => MarbleBlueFloorTiles, + Biome.Cobblestone => CobblestoneFloorTiles, + Biome.StoneGray => StoneGrayFloorTiles, + Biome.Brick => BrickFloorTiles, + Biome.MarbleWhite => MarbleWhiteFloorTiles, + Biome.MarbleGreen => MarbleGreenFloorTiles, + Biome.Acid => AcidFloorTiles, + // Grass border tiles - ALL placed on water pixels + Biome.GrassBorderN => GrassBorderNTiles, + Biome.GrassBorderS => GrassBorderSTiles, + Biome.GrassBorderE => GrassBorderETiles, + Biome.GrassBorderW => GrassBorderWTiles, + Biome.GrassBorderNW => GrassBorderNWTiles, + Biome.GrassBorderSE => GrassBorderSETiles, + // Forest border tiles - ALL placed on water pixels + Biome.ForestBorderN => ForestBorderNTiles, + Biome.ForestBorderS => ForestBorderSTiles, + Biome.ForestBorderE => ForestBorderETiles, + Biome.ForestBorderW => ForestBorderWTiles, + Biome.ForestBorderNW => ForestBorderNWTiles, + Biome.ForestBorderSE => ForestBorderSETiles, + // Corner tiles (0x0095) - placed on water pixels + Biome.CornerTileSW => CornerTiles, + Biome.CornerTileNE => CornerTiles, + Biome.CornerTileES => CornerTiles, + _ => GrassTiles + }; + + return tiles[_random.Next(tiles.Length)]; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Coastline.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Coastline.cs new file mode 100644 index 00000000..fa815caa --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Coastline.cs @@ -0,0 +1,177 @@ +using CentrED.Client; +using CentrED.IO.Models; +using CentrED.Network; +using CentrED.UI; +using CentrED.Utils; + +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Coastline generation for ImportColoredHeightmap. +/// Pattern: [G|F] → [D(transição)] → [0x0095 + Onda] → [W] +/// +/// Instead of shrinking the land, we shrink the water: +/// Find water tiles adjacent to land and replace them with shore (0x0095) + wave. +/// +public partial class ImportColoredHeightmap +{ + private bool _applyCoastline = true; + private int _coastlineProcessed = 0; + private int _coastlineAdded = 0; + private int _coastlineTerrainModified = 0; + + // Water tiles to process (convert to shore + wave) + private static readonly HashSet CoastWaterTiles = [0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x0136, 0x0137]; + + // Tiles to ignore when checking for land (water + shore = not land) + private static readonly HashSet CoastNonLandTiles = [0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x0136, 0x0137, 0x0095]; + + // Wave statics mapped by simplified direction (where water is) + // Cardinal directions + private static readonly Dictionary CardinalWaves = new() + { + { Direction.West, [0x179D, 0x179E] }, // Water to West + { Direction.South, [0x179F, 0x17A0] }, // Water to South + { Direction.North, [0x17A1, 0x17A2] }, // Water to North + { Direction.East, [0x17A3, 0x17A4] }, // Water to East + { Direction.Left, [0x17A9] }, // Water to Left (NW in iso) + { Direction.Down, [0x17AC] }, // Water to Down (SW in iso) + { Direction.Up, [0x17A7] }, // Water to Up (NE in iso) + { Direction.Right, [0x17AB] }, // Water to Right (SE in iso) + }; + + // Corner waves - for when water is in a diagonal direction + // private static readonly ushort[] CornerSW = [0x17A9]; // Water to SW + private static readonly ushort[] CornerNW = [0x17AC]; // Water to NW (was 0x17AA) + private static readonly ushort[] CornerNE = [0x17AB]; // Water to NE + private static readonly ushort[] CornerSE = [0x17A6]; // Water to SE (was 0x17AC) + + // Fallback water object statics + private static readonly ushort[] FallbackWaveStatics = [0x1797, 0x1798, 0x1799, 0x179A, 0x179B, 0x179C]; + + private void ApplyCoastlineToArea(CentrEDClient client, RectU16 area) + { + if (!_applyCoastline) + return; + + Console.WriteLine($"Coastline: Starting processing area {area.X1},{area.Y1} to {area.X2},{area.Y2}"); + + foreach (var (x, y) in new TileRange(area)) + { + ApplyCoastline(client, x, y); + + // Progress update every 1000 tiles + if (_coastlineProcessed % 1000 == 0) + { + Console.WriteLine($"Coastline: processed {_coastlineProcessed}, modified {_coastlineTerrainModified}, waves {_coastlineAdded}"); + client.Update(); + } + } + + Console.WriteLine($"Coastline: Finished. processed {_coastlineProcessed}, modified {_coastlineTerrainModified}, waves {_coastlineAdded}"); + } + + private void ApplyCoastline(CentrEDClient client, ushort x, ushort y) + { + _coastlineProcessed++; + + var landTile = client.GetLandTile(x, y); + var tileId = landTile.Id; + + // Only process water tiles + if (!CoastWaterTiles.Contains(tileId)) + return; + + // Get direction where LAND is (opposite of water direction detection) + var landDirection = GetLandDirection(client, x, y); + if (landDirection == Direction.None) + return; + + // Replace water with shore tile (0x0095) at Z=-15 + landTile.ReplaceLand(0x0095, -15); + _coastlineTerrainModified++; + + // Get appropriate wave static based on land direction + ushort waveStaticId = GetWaveStaticForDirection(landDirection); + + if (waveStaticId != 0) + { + var waveTile = new StaticTile(waveStaticId, x, y, -5, 0); + client.Add(waveTile); + _coastlineAdded++; + } + } + + /// + /// Get direction flags indicating where LAND tiles are relative to this water tile. + /// + private Direction GetLandDirection(CentrEDClient client, ushort x, ushort y) + { + var result = Direction.None; + + foreach (var dir in DirectionHelper.All) + { + var offset = dir.Offset(); + var nx = x + offset.Item1; + var ny = y + offset.Item2; + + if (nx < 0 || ny < 0 || nx > ushort.MaxValue || ny > ushort.MaxValue) + continue; + + try + { + var neighbor = client.GetLandTile((ushort)nx, (ushort)ny); + // If neighbor is NOT water/shore, it's land + if (!CoastNonLandTiles.Contains(neighbor.Id)) + { + result |= dir; + } + } + catch { } + } + + return result; + } + + /// + /// Get appropriate wave static ID based on direction where land is. + /// We detect where LAND is, but wave tiles are designed for "where WATER is from land". + /// So we need to reverse the direction. + /// + private ushort GetWaveStaticForDirection(Direction landDirection) + { + // Reverse: land direction -> water direction (opposite) + var waveDir = landDirection.Reverse(); + + // Check for corner cases first (diagonal directions) + // These corner tiles fill the diagonal gaps + // if (waveDir.HasFlag(Direction.South) && waveDir.HasFlag(Direction.West)) + // return CornerSW[Random.Shared.Next(CornerSW.Length)]; + + if (waveDir.HasFlag(Direction.West) && waveDir.HasFlag(Direction.North)) + return CornerNW[Random.Shared.Next(CornerNW.Length)]; + + if (waveDir.HasFlag(Direction.North) && waveDir.HasFlag(Direction.East)) + return CornerNE[Random.Shared.Next(CornerNE.Length)]; + + if (waveDir.HasFlag(Direction.South) && waveDir.HasFlag(Direction.East)) + return CornerSE[Random.Shared.Next(CornerSE.Length)]; + + // Check cardinal directions + foreach (var (dir, tiles) in CardinalWaves) + { + if (waveDir.HasFlag(dir)) + { + return tiles[Random.Shared.Next(tiles.Length)]; + } + } + + // Fallback to generic water wave + if (waveDir != Direction.None) + { + return FallbackWaveStatics[Random.Shared.Next(FallbackWaveStatics.Length)]; + } + + return 0; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Dirt.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Dirt.cs new file mode 100644 index 00000000..400bd1b6 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Dirt.cs @@ -0,0 +1,208 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Dirt biome transitions from DragonMod. +/// +public partial class ImportColoredHeightmap +{ + /// + /// Add Dirt->Cobblestone transitions based on DragonMod dirt2cobble.txt + /// + private void AddDirtToCobblestoneTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Dirt, ToBiome = Biome.Cobblestone }; + + // === EDGES === + // W edge: 0x400 + table.PatternToTiles["BAAAAABB"] = [0x0400]; + table.PatternToTiles["BAAAAAAB"] = [0x0400]; + table.PatternToTiles["AAAAAABB"] = [0x0400]; + table.PatternToTiles["AAAAAAAB"] = [0x0400]; + table.PatternToTiles["BAAAAABA"] = [0x0400]; + + // E edge: 0x3FE + table.PatternToTiles["AABBBAAA"] = [0x03FE]; + table.PatternToTiles["AAABBAAA"] = [0x03FE]; + table.PatternToTiles["AABBAAAA"] = [0x03FE]; + table.PatternToTiles["AAABAAAA"] = [0x03FE]; + table.PatternToTiles["AABABAAA"] = [0x03FE]; + + // S edge: 0x401 + table.PatternToTiles["AAAABBBA"] = [0x0401]; + table.PatternToTiles["AAAAABBA"] = [0x0401]; + table.PatternToTiles["AAAABBAA"] = [0x0401]; + table.PatternToTiles["AAAAABAA"] = [0x0401]; + table.PatternToTiles["AAAABABA"] = [0x0401]; + + // N edge: 0x3FF + table.PatternToTiles["BBBAAAAA"] = [0x03FF]; + table.PatternToTiles["ABBAAAAA"] = [0x03FF]; + table.PatternToTiles["BBAAAAAA"] = [0x03FF]; + table.PatternToTiles["ABAAAAAA"] = [0x03FF]; + table.PatternToTiles["BABAAAAA"] = [0x03FF]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x0402]; // SW + table.PatternToTiles["BAAAAAAA"] = [0x0403]; // NW + table.PatternToTiles["AAAABAAA"] = [0x0405]; // SE + table.PatternToTiles["AABAAAAA"] = [0x0404]; // NE + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x03F9]; // NW + table.PatternToTiles["AAABBBBA"] = [0x03F9]; + table.PatternToTiles["AABBBBAA"] = [0x03F9]; + table.PatternToTiles["AAABBBAA"] = [0x03F9]; + + table.PatternToTiles["BBBBBAAA"] = [0x03F8]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x03F8]; + table.PatternToTiles["ABBBBAAA"] = [0x03F8]; + table.PatternToTiles["ABBBAAAA"] = [0x03F8]; + + table.PatternToTiles["BBBAAABB"] = [0x03F6]; // SE + table.PatternToTiles["BBAAAABB"] = [0x03F6]; + table.PatternToTiles["BBBAAAAB"] = [0x03F6]; + table.PatternToTiles["BBAAAAAB"] = [0x03F6]; + + table.PatternToTiles["BAAABBBB"] = [0x03F7]; // NE + table.PatternToTiles["AAAABBBB"] = [0x03F7]; + table.PatternToTiles["BAAAABBB"] = [0x03F7]; + table.PatternToTiles["AAAAABBB"] = [0x03F7]; + + _dragonTransitions![(Biome.Dirt, Biome.Cobblestone)] = table; + } + + /// + /// Add Dirt->Rock transitions based on DragonMod dirt2mountain.txt + /// + private void AddDirtToRockTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Dirt, ToBiome = Biome.Rock }; + + // === EDGES === + // W edge: 0x6ED, 0x6F1 + table.PatternToTiles["BAAAAABB"] = [0x06ED, 0x06F1]; + table.PatternToTiles["BAAAAAAB"] = [0x06ED, 0x06F1]; + table.PatternToTiles["AAAAAABB"] = [0x06ED, 0x06F1]; + table.PatternToTiles["AAAAAAAB"] = [0x06ED, 0x06F1]; + table.PatternToTiles["BAAAAABA"] = [0x06ED, 0x06F1]; + + // E edge: 0x6EC, 0x6F0 + table.PatternToTiles["AABBBAAA"] = [0x06EC, 0x06F0]; + table.PatternToTiles["AAABBAAA"] = [0x06EC, 0x06F0]; + table.PatternToTiles["AABBAAAA"] = [0x06EC, 0x06F0]; + table.PatternToTiles["AAABAAAA"] = [0x06EC, 0x06F0]; + table.PatternToTiles["AABABAAA"] = [0x06EC, 0x06F0]; + + // S edge: 0x6EB, 0x6EF + table.PatternToTiles["AAAABBBA"] = [0x06EB, 0x06EF]; + table.PatternToTiles["AAAAABBA"] = [0x06EB, 0x06EF]; + table.PatternToTiles["AAAABBAA"] = [0x06EB, 0x06EF]; + table.PatternToTiles["AAAAABAA"] = [0x06EB, 0x06EF]; + table.PatternToTiles["AAAABABA"] = [0x06EB, 0x06EF]; + + // N edge: 0x6EE, 0x6F2 + table.PatternToTiles["BBBAAAAA"] = [0x06EE, 0x06F2]; + table.PatternToTiles["ABBAAAAA"] = [0x06EE, 0x06F2]; + table.PatternToTiles["BBAAAAAA"] = [0x06EE, 0x06F2]; + table.PatternToTiles["ABAAAAAA"] = [0x06EE, 0x06F2]; + table.PatternToTiles["BABAAAAA"] = [0x06EE, 0x06F2]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x00E2]; // SW + table.PatternToTiles["ABABBAAA"] = [0x00E2]; + table.PatternToTiles["BAAAAAAA"] = [0x00E3]; // NW + table.PatternToTiles["AAABABBA"] = [0x00E3]; + table.PatternToTiles["AAAABAAA"] = [0x00E1]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x00E1]; + table.PatternToTiles["AABAAAAA"] = [0x00E0]; // NE + table.PatternToTiles["BAAAABAB"] = [0x00E0]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x00E6]; // NW + table.PatternToTiles["AAABBBBA"] = [0x00E6]; + table.PatternToTiles["AABBBBAA"] = [0x00E6]; + table.PatternToTiles["AAABBBAA"] = [0x00E6]; + + table.PatternToTiles["BBBBBAAA"] = [0x00E5]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x00E5]; + table.PatternToTiles["ABBBBAAA"] = [0x00E5]; + table.PatternToTiles["ABBBAAAA"] = [0x00E5]; + + table.PatternToTiles["BBBAAABB"] = [0x00E4]; // SE + table.PatternToTiles["BBAAAABB"] = [0x00E4]; + table.PatternToTiles["BBBAAAAB"] = [0x00E4]; + table.PatternToTiles["BBAAAAAB"] = [0x00E4]; + + table.PatternToTiles["BAAABBBB"] = [0x00E7]; // NE + table.PatternToTiles["AAAABBBB"] = [0x00E7]; + table.PatternToTiles["BAAAABBB"] = [0x00E7]; + table.PatternToTiles["AAAAABBB"] = [0x00E7]; + + _dragonTransitions![(Biome.Dirt, Biome.Rock)] = table; + } + + /// + /// Add Dirt->Water transitions based on DragonMod dirt2water.txt + /// + private void AddDirtToWaterTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Dirt, ToBiome = Biome.Water }; + + // === EDGES === + // W edge: 0x8D + table.PatternToTiles["BAAAAABB"] = [0x008D]; + table.PatternToTiles["BAAAAAAB"] = [0x008D]; + table.PatternToTiles["AAAAAABB"] = [0x008D]; + table.PatternToTiles["AAAAAAAB"] = [0x008D]; + table.PatternToTiles["BAAAAABA"] = [0x008D]; + + // E edge: 0x8D + table.PatternToTiles["AABBBAAA"] = [0x008D]; + table.PatternToTiles["AAABBAAA"] = [0x008D]; + table.PatternToTiles["AABBAAAA"] = [0x008D]; + table.PatternToTiles["AAABAAAA"] = [0x008D]; + table.PatternToTiles["AABABAAA"] = [0x008D]; + + // S edge: 0x91 + table.PatternToTiles["AAAABBBA"] = [0x0091]; + table.PatternToTiles["AAAAABBA"] = [0x0091]; + table.PatternToTiles["AAAABBAA"] = [0x0091]; + table.PatternToTiles["AAAAABAA"] = [0x0091]; + table.PatternToTiles["AAAABABA"] = [0x0091]; + + // N edge: 0x91 + table.PatternToTiles["BBBAAAAA"] = [0x0091]; + table.PatternToTiles["ABBAAAAA"] = [0x0091]; + table.PatternToTiles["BBAAAAAA"] = [0x0091]; + table.PatternToTiles["ABAAAAAA"] = [0x0091]; + table.PatternToTiles["BABAAAAA"] = [0x0091]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x0095]; // SW + table.PatternToTiles["ABABBAAA"] = [0x0095]; + table.PatternToTiles["BAAAAAAA"] = [0x0095]; // NW + table.PatternToTiles["AAABABBA"] = [0x0095]; + table.PatternToTiles["AAAABAAA"] = [0x0095]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x0095]; + table.PatternToTiles["AABAAAAA"] = [0x0091]; // NE + table.PatternToTiles["BAAAABAB"] = [0x008D]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x02ED]; // NW + table.PatternToTiles["AAABBBBA"] = [0x02ED]; + table.PatternToTiles["AABBBBAA"] = [0x02ED]; + table.PatternToTiles["AAABBBAA"] = [0x02ED]; + + table.PatternToTiles["BBBBBAAA"] = [0x008D]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x008D]; + table.PatternToTiles["ABBBBAAA"] = [0x008D]; + table.PatternToTiles["ABBBAAAA"] = [0x008D]; + + table.PatternToTiles["BAAABBBB"] = [0x0091]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0091]; + table.PatternToTiles["BAAAABBB"] = [0x0091]; + table.PatternToTiles["AAAAABBB"] = [0x0091]; + + _dragonTransitions![(Biome.Dirt, Biome.Water)] = table; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Forest.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Forest.cs new file mode 100644 index 00000000..0d7f91a1 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Forest.cs @@ -0,0 +1,250 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Forest biome transitions from DragonMod. +/// +public partial class ImportColoredHeightmap +{ + /// + /// Add Forest->Dirt transitions based on DragonMod forest2dirt.txt + /// + private void AddForestToDirtTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Forest, ToBiome = Biome.Dirt }; + + // === EDGES === + // W edge: 0x166 + table.PatternToTiles["BAAAAABB"] = [0x0166]; + table.PatternToTiles["BAAAAAAB"] = [0x0166]; + table.PatternToTiles["AAAAAABB"] = [0x0166]; + table.PatternToTiles["AAAAAAAB"] = [0x0166]; + table.PatternToTiles["BAAAAABA"] = [0x0166]; + + // E edge: 0x167 + table.PatternToTiles["AABBBAAA"] = [0x0167]; + table.PatternToTiles["AAABBAAA"] = [0x0167]; + table.PatternToTiles["AABBAAAA"] = [0x0167]; + table.PatternToTiles["AAABAAAA"] = [0x0167]; + table.PatternToTiles["AABABAAA"] = [0x0167]; + + // S edge: 0x168 + table.PatternToTiles["AAAABBBA"] = [0x0168]; + table.PatternToTiles["AAAAABBA"] = [0x0168]; + table.PatternToTiles["AAAABBAA"] = [0x0168]; + table.PatternToTiles["AAAAABAA"] = [0x0168]; + table.PatternToTiles["AAAABABA"] = [0x0168]; + + // N edge: 0x165 + table.PatternToTiles["BBBAAAAA"] = [0x0165]; + table.PatternToTiles["ABBAAAAA"] = [0x0165]; + table.PatternToTiles["BBAAAAAA"] = [0x0165]; + table.PatternToTiles["ABAAAAAA"] = [0x0165]; + table.PatternToTiles["BABAAAAA"] = [0x0165]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x0161]; // SW + table.PatternToTiles["ABABBAAA"] = [0x0161]; + table.PatternToTiles["BAAAAAAA"] = [0x0163]; // NW + table.PatternToTiles["AAABABBA"] = [0x0163]; + table.PatternToTiles["AAAABAAA"] = [0x0164]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x0164]; + table.PatternToTiles["AABAAAAA"] = [0x0162]; // NE + table.PatternToTiles["BAAAABAB"] = [0x0162]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x016B]; // NW + table.PatternToTiles["AAABBBBA"] = [0x016B]; + table.PatternToTiles["AABBBBAA"] = [0x016B]; + table.PatternToTiles["AAABBBAA"] = [0x016B]; + + table.PatternToTiles["BBBBBAAA"] = [0x0169]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0169]; + table.PatternToTiles["ABBBBAAA"] = [0x0169]; + table.PatternToTiles["ABBBAAAA"] = [0x0169]; + + table.PatternToTiles["BBBAAABB"] = [0x016C]; // SE + table.PatternToTiles["BBAAAABB"] = [0x016C]; + table.PatternToTiles["BBBAAAAB"] = [0x016C]; + table.PatternToTiles["BBAAAAAB"] = [0x016C]; + + table.PatternToTiles["BAAABBBB"] = [0x016A]; // NE + table.PatternToTiles["AAAABBBB"] = [0x016A]; + table.PatternToTiles["BAAAABBB"] = [0x016A]; + table.PatternToTiles["AAAAABBB"] = [0x016A]; + + // Nacor fallback: 0x638 + table.PatternToTiles["ABBBBBBB"] = [0x0638]; + table.PatternToTiles["BABBBBBB"] = [0x0638]; + table.PatternToTiles["BBABBBBB"] = [0x0638]; + table.PatternToTiles["BBBABBBB"] = [0x0638]; + table.PatternToTiles["BBBBABBB"] = [0x0638]; + table.PatternToTiles["BBBBBABB"] = [0x0638]; + table.PatternToTiles["BBBBBBAB"] = [0x0638]; + table.PatternToTiles["BBBBBBBA"] = [0x0638]; + table.PatternToTiles["BBBBBBBB"] = [0x0638]; + + _dragonTransitions![(Biome.Forest, Biome.Dirt)] = table; + } + + /// + /// Add Forest->Sand transitions based on DragonMod forest2sand.txt + /// + private void AddForestToSandTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Forest, ToBiome = Biome.Sand }; + + // === EDGES === + // W edge: 0x64C + table.PatternToTiles["BAAAAABB"] = [0x064C]; + table.PatternToTiles["BAAAAAAB"] = [0x064C]; + table.PatternToTiles["AAAAAABB"] = [0x064C]; + table.PatternToTiles["AAAAAAAB"] = [0x064C]; + table.PatternToTiles["BAAAAABA"] = [0x064C]; + + // E edge: 0x64D + table.PatternToTiles["AABBBAAA"] = [0x064D]; + table.PatternToTiles["AAABBAAA"] = [0x064D]; + table.PatternToTiles["AABBAAAA"] = [0x064D]; + table.PatternToTiles["AAABAAAA"] = [0x064D]; + table.PatternToTiles["AABABAAA"] = [0x064D]; + + // S edge: 0x64E + table.PatternToTiles["AAAABBBA"] = [0x064E]; + table.PatternToTiles["AAAAABBA"] = [0x064E]; + table.PatternToTiles["AAAABBAA"] = [0x064E]; + table.PatternToTiles["AAAAABAA"] = [0x064E]; + table.PatternToTiles["AAAABABA"] = [0x064E]; + + // N edge: 0x64B + table.PatternToTiles["BBBAAAAA"] = [0x064B]; + table.PatternToTiles["ABBAAAAA"] = [0x064B]; + table.PatternToTiles["BBAAAAAA"] = [0x064B]; + table.PatternToTiles["ABAAAAAA"] = [0x064B]; + table.PatternToTiles["BABAAAAA"] = [0x064B]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x0656]; // SW + table.PatternToTiles["ABABBAAA"] = [0x0656]; + table.PatternToTiles["BAAAAAAA"] = [0x0653]; // NW + table.PatternToTiles["AAABABBA"] = [0x0653]; + table.PatternToTiles["AAAABAAA"] = [0x0654]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x0654]; + table.PatternToTiles["AABAAAAA"] = [0x0655]; // NE + table.PatternToTiles["BAAAABAB"] = [0x0655]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x064F]; // NW + table.PatternToTiles["AAABBBBA"] = [0x064F]; + table.PatternToTiles["AABBBBAA"] = [0x064F]; + table.PatternToTiles["AAABBBAA"] = [0x064F]; + + table.PatternToTiles["BBBBBAAA"] = [0x0652]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0652]; + table.PatternToTiles["ABBBBAAA"] = [0x0652]; + table.PatternToTiles["ABBBAAAA"] = [0x0652]; + + table.PatternToTiles["BBBAAABB"] = [0x0650]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0650]; + table.PatternToTiles["BBBAAAAB"] = [0x0650]; + table.PatternToTiles["BBAAAAAB"] = [0x0650]; + + table.PatternToTiles["BAAABBBB"] = [0x0651]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0651]; + table.PatternToTiles["BAAAABBB"] = [0x0651]; + table.PatternToTiles["AAAAABBB"] = [0x0651]; + + // Nacor fallback: 0x671 + table.PatternToTiles["ABBBBBBB"] = [0x0671]; + table.PatternToTiles["BABBBBBB"] = [0x0671]; + table.PatternToTiles["BBABBBBB"] = [0x0671]; + table.PatternToTiles["BBBABBBB"] = [0x0671]; + table.PatternToTiles["BBBBABBB"] = [0x0671]; + table.PatternToTiles["BBBBBABB"] = [0x0671]; + table.PatternToTiles["BBBBBBAB"] = [0x0671]; + table.PatternToTiles["BBBBBBBA"] = [0x0671]; + table.PatternToTiles["BBBBBBBB"] = [0x0671]; + + _dragonTransitions![(Biome.Forest, Biome.Sand)] = table; + } + + /// + /// Add Forest->Water transitions based on DragonMod forest2water.txt + /// + private void AddForestToWaterTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Forest, ToBiome = Biome.Water }; + + // === EDGES === + // W edge: 0x2EE, 0x2E7 + table.PatternToTiles["BAAAAABB"] = [0x02EE, 0x02E7]; + table.PatternToTiles["BAAAAAAB"] = [0x02EE, 0x02E7]; + table.PatternToTiles["AAAAAABB"] = [0x02EE, 0x02E7]; + table.PatternToTiles["AAAAAAAB"] = [0x02EE, 0x02E7]; + table.PatternToTiles["BAAAAABA"] = [0x02EE, 0x02E7]; + + // E edge: 0x2E6, 0x2F3 + table.PatternToTiles["AABBBAAA"] = [0x02E6, 0x02F3]; + table.PatternToTiles["AAABBAAA"] = [0x02E6, 0x02F3]; + table.PatternToTiles["AABBAAAA"] = [0x02E6, 0x02F3]; + table.PatternToTiles["AAABAAAA"] = [0x02E6, 0x02F3]; + table.PatternToTiles["AABABAAA"] = [0x02E6, 0x02F3]; + + // S edge: 0x2F2, 0x2EB + table.PatternToTiles["AAAABBBA"] = [0x02F2, 0x02EB]; + table.PatternToTiles["AAAAABBA"] = [0x02F2, 0x02EB]; + table.PatternToTiles["AAAABBAA"] = [0x02F2, 0x02EB]; + table.PatternToTiles["AAAAABAA"] = [0x02F2, 0x02EB]; + table.PatternToTiles["AAAABABA"] = [0x02F2, 0x02EB]; + + // N edge: 0x2EA, 0x2F1 + table.PatternToTiles["BBBAAAAA"] = [0x02EA, 0x02F1]; + table.PatternToTiles["ABBAAAAA"] = [0x02EA, 0x02F1]; + table.PatternToTiles["BBAAAAAA"] = [0x02EA, 0x02F1]; + table.PatternToTiles["ABAAAAAA"] = [0x02EA, 0x02F1]; + table.PatternToTiles["BABAAAAA"] = [0x02EA, 0x02F1]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x02FA]; // SW + table.PatternToTiles["ABABBAAA"] = [0x02FA]; + table.PatternToTiles["BAAAAAAA"] = [0x00C6]; // NW + table.PatternToTiles["AAABABBA"] = [0x02F8, 0x02EF]; + table.PatternToTiles["AAAABAAA"] = [0x02F0, 0x02FB]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x02F0, 0x02FB]; + table.PatternToTiles["AABAAAAA"] = [0x02F9]; // NE + table.PatternToTiles["BAAAABAB"] = [0x02F9]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x02ED]; // NW + table.PatternToTiles["AAABBBBA"] = [0x02ED]; + table.PatternToTiles["AABBBBAA"] = [0x02ED]; + table.PatternToTiles["AAABBBAA"] = [0x02ED]; + + table.PatternToTiles["BBBBBAAA"] = [0x008D]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x008D]; + table.PatternToTiles["ABBBBAAA"] = [0x008D]; + table.PatternToTiles["ABBBAAAA"] = [0x008D]; + + table.PatternToTiles["BBBAAABB"] = [0x0095]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0095]; + table.PatternToTiles["BBBAAAAB"] = [0x0095]; + table.PatternToTiles["BBAAAAAB"] = [0x0095]; + + table.PatternToTiles["BAAABBBB"] = [0x0091]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0091]; + table.PatternToTiles["BAAAABBB"] = [0x0091]; + table.PatternToTiles["AAAAABBB"] = [0x0091]; + + // Nacor fallback: 0xAA + table.PatternToTiles["ABBBBBBB"] = [0x00AA]; + table.PatternToTiles["BABBBBBB"] = [0x00AA]; + table.PatternToTiles["BBABBBBB"] = [0x00AA]; + table.PatternToTiles["BBBABBBB"] = [0x00AA]; + table.PatternToTiles["BBBBABBB"] = [0x00AA]; + table.PatternToTiles["BBBBBABB"] = [0x00AA]; + table.PatternToTiles["BBBBBBAB"] = [0x00AA]; + table.PatternToTiles["BBBBBBBA"] = [0x00AA]; + table.PatternToTiles["BBBBBBBB"] = [0x00AA]; + + _dragonTransitions![(Biome.Forest, Biome.Water)] = table; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Grass.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Grass.cs new file mode 100644 index 00000000..349eb79c --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Grass.cs @@ -0,0 +1,718 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Grass biome transitions from DragonMod. +/// +public partial class ImportColoredHeightmap +{ + /// + /// Add Grass->Dirt transitions based on DragonMod grass2dirt.txt + /// + private void AddGrassToDirtTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Dirt }; + + // === EDGES === + // W edge (Dirt to West): 0x87, 0x88 + table.PatternToTiles["BAAAAABB"] = [0x0087, 0x0088]; + table.PatternToTiles["BAAAAAAB"] = [0x0087, 0x0088]; + table.PatternToTiles["AAAAAABB"] = [0x0087, 0x0088]; + table.PatternToTiles["AAAAAAAB"] = [0x0087, 0x0088]; + table.PatternToTiles["BAAAAABA"] = [0x0087, 0x0088]; + + // E edge (Dirt to East): 0x89, 0x8A + table.PatternToTiles["AABBBAAA"] = [0x0089, 0x008A]; + table.PatternToTiles["AAABBAAA"] = [0x0089, 0x008A]; + table.PatternToTiles["AABBAAAA"] = [0x0089, 0x008A]; + table.PatternToTiles["AAABAAAA"] = [0x0089, 0x008A]; + table.PatternToTiles["AABABAAA"] = [0x0089, 0x008A]; + + // S edge (Dirt to South): 0x8B, 0x8C + table.PatternToTiles["AAAABBBA"] = [0x008B, 0x008C]; + table.PatternToTiles["AAAAABBA"] = [0x008B, 0x008C]; + table.PatternToTiles["AAAABBAA"] = [0x008B, 0x008C]; + table.PatternToTiles["AAAAABAA"] = [0x008B, 0x008C]; + table.PatternToTiles["AAAABABA"] = [0x008B, 0x008C]; + + // N edge (Dirt to North): 0x85, 0x86 + table.PatternToTiles["BBBAAAAA"] = [0x0085, 0x0086]; + table.PatternToTiles["ABBAAAAA"] = [0x0085, 0x0086]; + table.PatternToTiles["BBAAAAAA"] = [0x0085, 0x0086]; + table.PatternToTiles["ABAAAAAA"] = [0x0085, 0x0086]; + table.PatternToTiles["BABAAAAA"] = [0x0085, 0x0086]; + + // === INNER CORNERS (diagonal only) === + table.PatternToTiles["AAAAAABA"] = [0x0082]; // SW + table.PatternToTiles["ABABBAAA"] = [0x0082]; // SW alt + table.PatternToTiles["BAAAAAAA"] = [0x007E]; // NW + table.PatternToTiles["AAABABBA"] = [0x007E]; // NW alt + table.PatternToTiles["AAAABAAA"] = [0x007D]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x007D]; // SE alt + table.PatternToTiles["AABAAAAA"] = [0x0083]; // NE + table.PatternToTiles["BAAAABAB"] = [0x0083]; // NE alt + + // === OUTER CORNERS === + // NW corner is TileA: 0x7A + table.PatternToTiles["AABBBBBA"] = [0x007A]; + table.PatternToTiles["AAABBBBA"] = [0x007A]; + table.PatternToTiles["AABBBBAA"] = [0x007A]; + table.PatternToTiles["AAABBBAA"] = [0x007A]; + + // SW corner is TileA: 0x7B + table.PatternToTiles["BBBBBAAA"] = [0x007B]; + table.PatternToTiles["BBBBAAAA"] = [0x007B]; + table.PatternToTiles["ABBBBAAA"] = [0x007B]; + table.PatternToTiles["ABBBAAAA"] = [0x007B]; + + // SE corner is TileA: 0x79 + table.PatternToTiles["BBBAAABB"] = [0x0079]; + table.PatternToTiles["BBAAAABB"] = [0x0079]; + table.PatternToTiles["BBBAAAAB"] = [0x0079]; + table.PatternToTiles["BBAAAAAB"] = [0x0079]; + + // NE corner is TileA: 0x7C + table.PatternToTiles["BAAABBBB"] = [0x007C]; + table.PatternToTiles["AAAABBBB"] = [0x007C]; + table.PatternToTiles["BAAAABBB"] = [0x007C]; + table.PatternToTiles["AAAAABBB"] = [0x007C]; + + // Nacor fallback (mostly Dirt): 0x638 + table.PatternToTiles["ABBBBBBB"] = [0x0638]; + table.PatternToTiles["BABBBBBB"] = [0x0638]; + table.PatternToTiles["BBABBBBB"] = [0x0638]; + table.PatternToTiles["BBBABBBB"] = [0x0638]; + table.PatternToTiles["BBBBABBB"] = [0x0638]; + table.PatternToTiles["BBBBBABB"] = [0x0638]; + table.PatternToTiles["BBBBBBAB"] = [0x0638]; + table.PatternToTiles["BBBBBBBA"] = [0x0638]; + table.PatternToTiles["BBBBBBBB"] = [0x0638]; + + _dragonTransitions![(Biome.Grass, Biome.Dirt)] = table; + } + + /// + /// Add Grass->Sand transitions based on DragonMod grass2sand.txt + /// + private void AddGrassToSandTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Sand }; + + // === EDGES === + // W edge: 0x37 + table.PatternToTiles["BAAAAABB"] = [0x0037]; + table.PatternToTiles["BAAAAAAB"] = [0x0037]; + table.PatternToTiles["AAAAAABB"] = [0x0037]; + table.PatternToTiles["AAAAAAAB"] = [0x0037]; + table.PatternToTiles["BAAAAABA"] = [0x0037]; + + // E edge: 0x38 + table.PatternToTiles["AABBBAAA"] = [0x0038]; + table.PatternToTiles["AAABBAAA"] = [0x0038]; + table.PatternToTiles["AABBAAAA"] = [0x0038]; + table.PatternToTiles["AAABAAAA"] = [0x0038]; + table.PatternToTiles["AABABAAA"] = [0x0038]; + + // S edge: 0x3A + table.PatternToTiles["AAAABBBA"] = [0x003A]; + table.PatternToTiles["AAAAABBA"] = [0x003A]; + table.PatternToTiles["AAAABBAA"] = [0x003A]; + table.PatternToTiles["AAAAABAA"] = [0x003A]; + table.PatternToTiles["AAAABABA"] = [0x003A]; + + // N edge: 0x39 + table.PatternToTiles["BBBAAAAA"] = [0x0039]; + table.PatternToTiles["ABBAAAAA"] = [0x0039]; + table.PatternToTiles["BBAAAAAA"] = [0x0039]; + table.PatternToTiles["ABAAAAAA"] = [0x0039]; + table.PatternToTiles["BABAAAAA"] = [0x0039]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x003C]; // SW + table.PatternToTiles["ABABBAAA"] = [0x003C]; + table.PatternToTiles["BAAAAAAA"] = [0x003E]; // NW + table.PatternToTiles["AAABABBA"] = [0x003E]; + table.PatternToTiles["AAAABAAA"] = [0x003D]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x003D]; + table.PatternToTiles["AABAAAAA"] = [0x003B]; // NE + table.PatternToTiles["BAAAABAB"] = [0x003B]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x0035]; // NW + table.PatternToTiles["AAABBBBA"] = [0x0035]; + table.PatternToTiles["AABBBBAA"] = [0x0035]; + table.PatternToTiles["AAABBBAA"] = [0x0035]; + + table.PatternToTiles["BBBBBAAA"] = [0x0034]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0034]; + table.PatternToTiles["ABBBBAAA"] = [0x0034]; + table.PatternToTiles["ABBBAAAA"] = [0x0034]; + + table.PatternToTiles["BBBAAABB"] = [0x0033]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0033]; + table.PatternToTiles["BBBAAAAB"] = [0x0033]; + table.PatternToTiles["BBAAAAAB"] = [0x0033]; + + table.PatternToTiles["BAAABBBB"] = [0x0036]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0036]; + table.PatternToTiles["BAAAABBB"] = [0x0036]; + table.PatternToTiles["AAAAABBB"] = [0x0036]; + + // Nacor fallback: 0x671 + table.PatternToTiles["ABBBBBBB"] = [0x0671]; + table.PatternToTiles["BABBBBBB"] = [0x0671]; + table.PatternToTiles["BBABBBBB"] = [0x0671]; + table.PatternToTiles["BBBABBBB"] = [0x0671]; + table.PatternToTiles["BBBBABBB"] = [0x0671]; + table.PatternToTiles["BBBBBABB"] = [0x0671]; + table.PatternToTiles["BBBBBBAB"] = [0x0671]; + table.PatternToTiles["BBBBBBBA"] = [0x0671]; + table.PatternToTiles["BBBBBBBB"] = [0x0671]; + + _dragonTransitions![(Biome.Grass, Biome.Sand)] = table; + } + + /// + /// Add Grass->Forest transitions based on DragonMod grass2forest.txt + /// + private void AddGrassToForestTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Forest }; + + // === EDGES === + // W edge: 0xCB, 0xCC + table.PatternToTiles["BAAAAABB"] = [0x00CB, 0x00CC]; + table.PatternToTiles["BAAAAAAB"] = [0x00CB, 0x00CC]; + table.PatternToTiles["AAAAAABB"] = [0x00CB, 0x00CC]; + table.PatternToTiles["AAAAAAAB"] = [0x00CB, 0x00CC]; + table.PatternToTiles["BAAAAABA"] = [0x00CB, 0x00CC]; + + // E edge: 0xCE + table.PatternToTiles["AABBBAAA"] = [0x00CE]; + table.PatternToTiles["AAABBAAA"] = [0x00CE]; + table.PatternToTiles["AABBAAAA"] = [0x00CE]; + table.PatternToTiles["AAABAAAA"] = [0x00CE]; + table.PatternToTiles["AABABAAA"] = [0x00CE]; + + // S edge: 0xD1, 0xD2 + table.PatternToTiles["AAAABBBA"] = [0x00D1, 0x00D2]; + table.PatternToTiles["AAAAABBA"] = [0x00D1, 0x00D2]; + table.PatternToTiles["AAAABBAA"] = [0x00D1, 0x00D2]; + table.PatternToTiles["AAAAABAA"] = [0x00D1, 0x00D2]; + table.PatternToTiles["AAAABABA"] = [0x00D1, 0x00D2]; + + // N edge: 0xC8, 0xC9 + table.PatternToTiles["BBBAAAAA"] = [0x00C8, 0x00C9]; + table.PatternToTiles["ABBAAAAA"] = [0x00C8, 0x00C9]; + table.PatternToTiles["BBAAAAAA"] = [0x00C8, 0x00C9]; + table.PatternToTiles["ABAAAAAA"] = [0x00C8, 0x00C9]; + table.PatternToTiles["BABAAAAA"] = [0x00C8, 0x00C9]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x00CF]; // SW (same as NE in this file) + table.PatternToTiles["ABABBAAA"] = [0x00CF]; + table.PatternToTiles["BAAAAAAA"] = [0x00CD]; // NW + table.PatternToTiles["AAABABBA"] = [0x00CD]; + table.PatternToTiles["AAAABAAA"] = [0x00D0]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x00D0]; + table.PatternToTiles["AABAAAAA"] = [0x00CF]; // NE + table.PatternToTiles["BAAAABAB"] = [0x00CF]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x00D5]; // NW + table.PatternToTiles["AAABBBBA"] = [0x00D5]; + table.PatternToTiles["AABBBBAA"] = [0x00D5]; + table.PatternToTiles["AAABBBAA"] = [0x00D5]; + + table.PatternToTiles["BBBBBAAA"] = [0x00D7]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x00D7]; + table.PatternToTiles["ABBBBAAA"] = [0x00D7]; + table.PatternToTiles["ABBBAAAA"] = [0x00D7]; + + table.PatternToTiles["BBBAAABB"] = [0x00D4]; // SE + table.PatternToTiles["BBAAAABB"] = [0x00D4]; + table.PatternToTiles["BBBAAAAB"] = [0x00D4]; + table.PatternToTiles["BBAAAAAB"] = [0x00D4]; + + table.PatternToTiles["BAAABBBB"] = [0x00D6]; // NE + table.PatternToTiles["AAAABBBB"] = [0x00D6]; + table.PatternToTiles["BAAAABBB"] = [0x00D6]; + table.PatternToTiles["AAAAABBB"] = [0x00D6]; + + // Nacor fallback: 0xC4 + table.PatternToTiles["ABBBBBBB"] = [0x00C4]; + table.PatternToTiles["BABBBBBB"] = [0x00C4]; + table.PatternToTiles["BBABBBBB"] = [0x00C4]; + table.PatternToTiles["BBBABBBB"] = [0x00C4]; + table.PatternToTiles["BBBBABBB"] = [0x00C4]; + table.PatternToTiles["BBBBBABB"] = [0x00C4]; + table.PatternToTiles["BBBBBBAB"] = [0x00C4]; + table.PatternToTiles["BBBBBBBA"] = [0x00C4]; + table.PatternToTiles["BBBBBBBB"] = [0x00C4]; + + _dragonTransitions![(Biome.Grass, Biome.Forest)] = table; + } + + /// + /// Add Grass->Snow transitions based on DragonMod grass2snow.txt + /// + private void AddGrassToSnowTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Snow }; + + // === EDGES === + // W edge: 0x5C1 + table.PatternToTiles["BAAAAABB"] = [0x05C1]; + table.PatternToTiles["BAAAAAAB"] = [0x05C1]; + table.PatternToTiles["AAAAAABB"] = [0x05C1]; + table.PatternToTiles["AAAAAAAB"] = [0x05C1]; + table.PatternToTiles["BAAAAABA"] = [0x05C1]; + + // E edge: 0x5C0 + table.PatternToTiles["AABBBAAA"] = [0x05C0]; + table.PatternToTiles["AAABBAAA"] = [0x05C0]; + table.PatternToTiles["AABBAAAA"] = [0x05C0]; + table.PatternToTiles["AAABAAAA"] = [0x05C0]; + table.PatternToTiles["AABABAAA"] = [0x05C0]; + + // S edge: 0x5BF + table.PatternToTiles["AAAABBBA"] = [0x05BF]; + table.PatternToTiles["AAAAABBA"] = [0x05BF]; + table.PatternToTiles["AAAABBAA"] = [0x05BF]; + table.PatternToTiles["AAAAABAA"] = [0x05BF]; + table.PatternToTiles["AAAABABA"] = [0x05BF]; + + // N edge: 0x5C2 + table.PatternToTiles["BBBAAAAA"] = [0x05C2]; + table.PatternToTiles["ABBAAAAA"] = [0x05C2]; + table.PatternToTiles["BBAAAAAA"] = [0x05C2]; + table.PatternToTiles["ABAAAAAA"] = [0x05C2]; + table.PatternToTiles["BABAAAAA"] = [0x05C2]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x05C6]; // SW + table.PatternToTiles["ABABBAAA"] = [0x05C6]; + table.PatternToTiles["BAAAAAAA"] = [0x05C3]; // NW + table.PatternToTiles["AAABABBA"] = [0x05C3]; + table.PatternToTiles["AAAABAAA"] = [0x05C4]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x05C4]; + table.PatternToTiles["AABAAAAA"] = [0x05C5]; // NE + table.PatternToTiles["BAAAABAB"] = [0x05C5]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x05C7]; // NW + table.PatternToTiles["AAABBBBA"] = [0x05C7]; + table.PatternToTiles["AABBBBAA"] = [0x05C7]; + table.PatternToTiles["AAABBBAA"] = [0x05C7]; + + table.PatternToTiles["BBBBBAAA"] = [0x05CA]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x05CA]; + table.PatternToTiles["ABBBBAAA"] = [0x05CA]; + table.PatternToTiles["ABBBAAAA"] = [0x05CA]; + + table.PatternToTiles["BBBAAABB"] = [0x05C8]; // SE + table.PatternToTiles["BBAAAABB"] = [0x05C8]; + table.PatternToTiles["BBBAAAAB"] = [0x05C8]; + table.PatternToTiles["BBAAAAAB"] = [0x05C8]; + + table.PatternToTiles["BAAABBBB"] = [0x05C9]; // NE + table.PatternToTiles["AAAABBBB"] = [0x05C9]; + table.PatternToTiles["BAAAABBB"] = [0x05C9]; + table.PatternToTiles["AAAAABBB"] = [0x05C9]; + + // Nacor fallback: 0x770 + table.PatternToTiles["ABBBBBBB"] = [0x0770]; + table.PatternToTiles["BABBBBBB"] = [0x0770]; + table.PatternToTiles["BBABBBBB"] = [0x0770]; + table.PatternToTiles["BBBABBBB"] = [0x0770]; + table.PatternToTiles["BBBBABBB"] = [0x0770]; + table.PatternToTiles["BBBBBABB"] = [0x0770]; + table.PatternToTiles["BBBBBBAB"] = [0x0770]; + table.PatternToTiles["BBBBBBBA"] = [0x0770]; + table.PatternToTiles["BBBBBBBB"] = [0x0770]; + + _dragonTransitions![(Biome.Grass, Biome.Snow)] = table; + } + + /// + /// Add Grass->Rock transitions based on DragonMod grass2mountain.txt + /// + private void AddGrassToRockTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Rock }; + + // === EDGES === + // W edge: 0x23A + table.PatternToTiles["BAAAAABB"] = [0x023A]; + table.PatternToTiles["BAAAAAAB"] = [0x023A]; + table.PatternToTiles["AAAAAABB"] = [0x023A]; + table.PatternToTiles["AAAAAAAB"] = [0x023A]; + table.PatternToTiles["BAAAAABA"] = [0x023A]; + + // E edge: 0x23C + table.PatternToTiles["AABBBAAA"] = [0x023C]; + table.PatternToTiles["AAABBAAA"] = [0x023C]; + table.PatternToTiles["AABBAAAA"] = [0x023C]; + table.PatternToTiles["AAABAAAA"] = [0x023C]; + table.PatternToTiles["AABABAAA"] = [0x023C]; + + // S edge: 0x239 + table.PatternToTiles["AAAABBBA"] = [0x0239]; + table.PatternToTiles["AAAAABBA"] = [0x0239]; + table.PatternToTiles["AAAABBAA"] = [0x0239]; + table.PatternToTiles["AAAAABAA"] = [0x0239]; + table.PatternToTiles["AAAABABA"] = [0x0239]; + + // N edge: 0x23B + table.PatternToTiles["BBBAAAAA"] = [0x023B]; + table.PatternToTiles["ABBAAAAA"] = [0x023B]; + table.PatternToTiles["BBAAAAAA"] = [0x023B]; + table.PatternToTiles["ABAAAAAA"] = [0x023B]; + table.PatternToTiles["BABAAAAA"] = [0x023B]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x0232]; // SW + table.PatternToTiles["ABABBAAA"] = [0x0232]; + table.PatternToTiles["BAAAAAAA"] = [0x0233]; // NW + table.PatternToTiles["AAABABBA"] = [0x0233]; + table.PatternToTiles["AAAABAAA"] = [0x0231]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x0231]; + table.PatternToTiles["AABAAAAA"] = [0x0234]; // NE + table.PatternToTiles["BAAAABAB"] = [0x0234]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x0237]; // NW + table.PatternToTiles["AAABBBBA"] = [0x0237]; + table.PatternToTiles["AABBBBAA"] = [0x0237]; + table.PatternToTiles["AAABBBAA"] = [0x0237]; + + table.PatternToTiles["BBBBBAAA"] = [0x0236]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0236]; + table.PatternToTiles["ABBBBAAA"] = [0x0236]; + table.PatternToTiles["ABBBAAAA"] = [0x0236]; + + table.PatternToTiles["BBBAAABB"] = [0x0235]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0235]; + table.PatternToTiles["BBBAAAAB"] = [0x0235]; + table.PatternToTiles["BBAAAAAB"] = [0x0235]; + + table.PatternToTiles["BAAABBBB"] = [0x0238]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0238]; + table.PatternToTiles["BAAAABBB"] = [0x0238]; + table.PatternToTiles["AAAAABBB"] = [0x0238]; + + // Nacor fallback: 0x6DC + table.PatternToTiles["ABBBBBBB"] = [0x06DC]; + table.PatternToTiles["BABBBBBB"] = [0x06DC]; + table.PatternToTiles["BBABBBBB"] = [0x06DC]; + table.PatternToTiles["BBBABBBB"] = [0x06DC]; + table.PatternToTiles["BBBBABBB"] = [0x06DC]; + table.PatternToTiles["BBBBBABB"] = [0x06DC]; + table.PatternToTiles["BBBBBBAB"] = [0x06DC]; + table.PatternToTiles["BBBBBBBA"] = [0x06DC]; + table.PatternToTiles["BBBBBBBB"] = [0x06DC]; + + _dragonTransitions![(Biome.Grass, Biome.Rock)] = table; + } + + /// + /// Add Grass->Cobblestone transitions based on DragonMod grass2cobble.txt + /// + private void AddGrassToCobblestoneTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Cobblestone }; + + // === EDGES === + // W edge: 0x67F + table.PatternToTiles["BAAAAABB"] = [0x067F]; + table.PatternToTiles["BAAAAAAB"] = [0x067F]; + table.PatternToTiles["AAAAAABB"] = [0x067F]; + table.PatternToTiles["AAAAAAAB"] = [0x067F]; + table.PatternToTiles["BAAAAABA"] = [0x067F]; + + // E edge: 0x67E + table.PatternToTiles["AABBBAAA"] = [0x067E]; + table.PatternToTiles["AAABBAAA"] = [0x067E]; + table.PatternToTiles["AABBAAAA"] = [0x067E]; + table.PatternToTiles["AAABAAAA"] = [0x067E]; + table.PatternToTiles["AABABAAA"] = [0x067E]; + + // S edge: 0x67D + table.PatternToTiles["AAAABBBA"] = [0x067D]; + table.PatternToTiles["AAAAABBA"] = [0x067D]; + table.PatternToTiles["AAAABBAA"] = [0x067D]; + table.PatternToTiles["AAAAABAA"] = [0x067D]; + table.PatternToTiles["AAAABABA"] = [0x067D]; + + // N edge: 0x680 + table.PatternToTiles["BBBAAAAA"] = [0x0680]; + table.PatternToTiles["ABBAAAAA"] = [0x0680]; + table.PatternToTiles["BBAAAAAA"] = [0x0680]; + table.PatternToTiles["ABAAAAAA"] = [0x0680]; + table.PatternToTiles["BABAAAAA"] = [0x0680]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x0684]; // SW + table.PatternToTiles["ABABBAAA"] = [0x0684]; + table.PatternToTiles["BAAAAAAA"] = [0x0681]; // NW + table.PatternToTiles["AAABABBA"] = [0x0681]; + table.PatternToTiles["AAAABAAA"] = [0x0682]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x0682]; + table.PatternToTiles["AABAAAAA"] = [0x0683]; // NE + table.PatternToTiles["BAAAABAB"] = [0x0683]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x0685]; // NW + table.PatternToTiles["AAABBBBA"] = [0x0685]; + table.PatternToTiles["AABBBBAA"] = [0x0685]; + table.PatternToTiles["AAABBBAA"] = [0x0685]; + + table.PatternToTiles["BBBBBAAA"] = [0x0688]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0688]; + table.PatternToTiles["ABBBBAAA"] = [0x0688]; + table.PatternToTiles["ABBBAAAA"] = [0x0688]; + + table.PatternToTiles["BBBAAABB"] = [0x0686]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0686]; + table.PatternToTiles["BBBAAAAB"] = [0x0686]; + table.PatternToTiles["BBAAAAAB"] = [0x0686]; + + table.PatternToTiles["BAAABBBB"] = [0x0687]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0687]; + table.PatternToTiles["BAAAABBB"] = [0x0687]; + table.PatternToTiles["AAAAABBB"] = [0x0687]; + + // Nacor fallback: 0x69D + table.PatternToTiles["ABBBBBBB"] = [0x069D]; + table.PatternToTiles["BABBBBBB"] = [0x069D]; + table.PatternToTiles["BBABBBBB"] = [0x069D]; + table.PatternToTiles["BBBABBBB"] = [0x069D]; + table.PatternToTiles["BBBBABBB"] = [0x069D]; + table.PatternToTiles["BBBBBABB"] = [0x069D]; + table.PatternToTiles["BBBBBBAB"] = [0x069D]; + table.PatternToTiles["BBBBBBBA"] = [0x069D]; + table.PatternToTiles["BBBBBBBB"] = [0x069D]; + + _dragonTransitions![(Biome.Grass, Biome.Cobblestone)] = table; + } + + /// + /// Add Grass->Swamp transitions based on DragonMod grass2swamp.txt + /// + private void AddGrassToSwampTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Swamp }; + + // === EDGES === + // W edge: 0x3DDB + table.PatternToTiles["BAAAAABB"] = [0x3DDB]; + table.PatternToTiles["BAAAAAAB"] = [0x3DDB]; + table.PatternToTiles["AAAAAABB"] = [0x3DDB]; + table.PatternToTiles["AAAAAAAB"] = [0x3DDB]; + table.PatternToTiles["BAAAAABA"] = [0x3DDB]; + + // E edge: 0x3DDE + table.PatternToTiles["AABBBAAA"] = [0x3DDE]; + table.PatternToTiles["AAABBAAA"] = [0x3DDE]; + table.PatternToTiles["AABBAAAA"] = [0x3DDE]; + table.PatternToTiles["AAABAAAA"] = [0x3DDE]; + table.PatternToTiles["AABABAAA"] = [0x3DDE]; + + // S edge: 0x3DDF + table.PatternToTiles["AAAABBBA"] = [0x3DDF]; + table.PatternToTiles["AAAAABBA"] = [0x3DDF]; + table.PatternToTiles["AAAABBAA"] = [0x3DDF]; + table.PatternToTiles["AAAAABAA"] = [0x3DDF]; + table.PatternToTiles["AAAABABA"] = [0x3DDF]; + + // N edge: 0x3DE1 + table.PatternToTiles["BBBAAAAA"] = [0x3DE1]; + table.PatternToTiles["ABBAAAAA"] = [0x3DE1]; + table.PatternToTiles["BBAAAAAA"] = [0x3DE1]; + table.PatternToTiles["ABAAAAAA"] = [0x3DE1]; + table.PatternToTiles["BABAAAAA"] = [0x3DE1]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x3DD5]; // SW + table.PatternToTiles["ABABBAAA"] = [0x3DD5]; + table.PatternToTiles["BAAAAAAA"] = [0x3DD8]; // NW + table.PatternToTiles["AAABABBA"] = [0x3DD8]; + table.PatternToTiles["AAAABAAA"] = [0x3DD6]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x3DD6]; + table.PatternToTiles["AABAAAAA"] = [0x3DD7]; // NE + table.PatternToTiles["BAAAABAB"] = [0x3DD7]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x3DE5]; // NW + table.PatternToTiles["AAABBBBA"] = [0x3DE5]; + table.PatternToTiles["AABBBBAA"] = [0x3DE5]; + table.PatternToTiles["AAABBBAA"] = [0x3DE5]; + + table.PatternToTiles["BBBBBAAA"] = [0x3DE4]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x3DE4]; + table.PatternToTiles["ABBBBAAA"] = [0x3DE4]; + table.PatternToTiles["ABBBAAAA"] = [0x3DE4]; + + table.PatternToTiles["BBBAAABB"] = [0x3DE6]; // SE + table.PatternToTiles["BBAAAABB"] = [0x3DE6]; + table.PatternToTiles["BBBAAAAB"] = [0x3DE6]; + table.PatternToTiles["BBAAAAAB"] = [0x3DE6]; + + table.PatternToTiles["BAAABBBB"] = [0x3DE3]; // NE + table.PatternToTiles["AAAABBBB"] = [0x3DE3]; + table.PatternToTiles["BAAAABBB"] = [0x3DE3]; + table.PatternToTiles["AAAAABBB"] = [0x3DE3]; + + _dragonTransitions![(Biome.Grass, Biome.Swamp)] = table; + } + + /// + /// Add Grass->Jungle transitions based on DragonMod grass2jungle.txt + /// + private void AddGrassToJungleTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Jungle }; + + // === EDGES === + // W edge: 0x583 + table.PatternToTiles["BAAAAABB"] = [0x0583]; + table.PatternToTiles["BAAAAAAB"] = [0x0583]; + table.PatternToTiles["AAAAAABB"] = [0x0583]; + table.PatternToTiles["AAAAAAAB"] = [0x0583]; + table.PatternToTiles["BAAAAABA"] = [0x0583]; + + // E edge: 0x582 + table.PatternToTiles["AABBBAAA"] = [0x0582]; + table.PatternToTiles["AAABBAAA"] = [0x0582]; + table.PatternToTiles["AABBAAAA"] = [0x0582]; + table.PatternToTiles["AAABAAAA"] = [0x0582]; + table.PatternToTiles["AABABAAA"] = [0x0582]; + + // S edge: 0x581 + table.PatternToTiles["AAAABBBA"] = [0x0581]; + table.PatternToTiles["AAAAABBA"] = [0x0581]; + table.PatternToTiles["AAAABBAA"] = [0x0581]; + table.PatternToTiles["AAAAABAA"] = [0x0581]; + table.PatternToTiles["AAAABABA"] = [0x0581]; + + // N edge: 0x584 + table.PatternToTiles["BBBAAAAA"] = [0x0584]; + table.PatternToTiles["ABBAAAAA"] = [0x0584]; + table.PatternToTiles["BBAAAAAA"] = [0x0584]; + table.PatternToTiles["ABAAAAAA"] = [0x0584]; + table.PatternToTiles["BABAAAAA"] = [0x0584]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x057A]; // SW + table.PatternToTiles["ABABBAAA"] = [0x057A]; + table.PatternToTiles["BAAAAAAA"] = [0x057D]; // NW + table.PatternToTiles["AAABABBA"] = [0x057D]; + table.PatternToTiles["AAAABAAA"] = [0x057F]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x057F]; + table.PatternToTiles["AABAAAAA"] = [0x0579]; // NE + table.PatternToTiles["BAAAABAB"] = [0x0579]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x0580]; // NW + table.PatternToTiles["AAABBBBA"] = [0x0580]; + table.PatternToTiles["AABBBBAA"] = [0x0580]; + table.PatternToTiles["AAABBBAA"] = [0x0580]; + + table.PatternToTiles["BBBBBAAA"] = [0x0582]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0582]; + table.PatternToTiles["ABBBBAAA"] = [0x0582]; + table.PatternToTiles["ABBBAAAA"] = [0x0582]; + + table.PatternToTiles["BBBAAABB"] = [0x0587]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0587]; + table.PatternToTiles["BBBAAAAB"] = [0x0587]; + table.PatternToTiles["BBAAAAAB"] = [0x0587]; + + table.PatternToTiles["BAAABBBB"] = [0x0583]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0583]; + table.PatternToTiles["BAAAABBB"] = [0x0583]; + table.PatternToTiles["AAAAABBB"] = [0x0583]; + + // Nacor fallback: 0x616 + table.PatternToTiles["ABBBBBBB"] = [0x0616]; + table.PatternToTiles["BABBBBBB"] = [0x0616]; + table.PatternToTiles["BBABBBBB"] = [0x0616]; + table.PatternToTiles["BBBABBBB"] = [0x0616]; + table.PatternToTiles["BBBBABBB"] = [0x0616]; + table.PatternToTiles["BBBBBABB"] = [0x0616]; + table.PatternToTiles["BBBBBBAB"] = [0x0616]; + table.PatternToTiles["BBBBBBBA"] = [0x0616]; + table.PatternToTiles["BBBBBBBB"] = [0x0616]; + + _dragonTransitions![(Biome.Grass, Biome.Jungle)] = table; + } + + /// + /// Add Grass->Water transitions based on DragonMod grass2water-light.txt + /// + private void AddGrassToWaterTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Grass, ToBiome = Biome.Water }; + + // === EDGES === + // W edge: 0x22, 0x26 + table.PatternToTiles["BAAAAABB"] = [0x0022, 0x0026]; + table.PatternToTiles["BAAAAAAB"] = [0x0022, 0x0026]; + table.PatternToTiles["AAAAAABB"] = [0x0022, 0x0026]; + table.PatternToTiles["AAAAAAAB"] = [0x0022, 0x0026]; + table.PatternToTiles["BAAAAABA"] = [0x0022, 0x0026]; + + // E edge: 0x23, 0x27 + table.PatternToTiles["AABBBAAA"] = [0x0023, 0x0027]; + table.PatternToTiles["AAABBAAA"] = [0x0023, 0x0027]; + table.PatternToTiles["AABBAAAA"] = [0x0023, 0x0027]; + table.PatternToTiles["AAABAAAA"] = [0x0023, 0x0027]; + table.PatternToTiles["AABABAAA"] = [0x0023, 0x0027]; + + // S edge: 0x21, 0x25 + table.PatternToTiles["AAAABBBA"] = [0x0021, 0x0025]; + table.PatternToTiles["AAAAABBA"] = [0x0021, 0x0025]; + table.PatternToTiles["AAAABBAA"] = [0x0021, 0x0025]; + table.PatternToTiles["AAAAABAA"] = [0x0021, 0x0025]; + table.PatternToTiles["AAAABABA"] = [0x0021, 0x0025]; + + // N edge: 0x24, 0x28 + table.PatternToTiles["BBBAAAAA"] = [0x0024, 0x0028]; + table.PatternToTiles["ABBAAAAA"] = [0x0024, 0x0028]; + table.PatternToTiles["BBAAAAAA"] = [0x0024, 0x0028]; + table.PatternToTiles["ABAAAAAA"] = [0x0024, 0x0028]; + table.PatternToTiles["BABAAAAA"] = [0x0024, 0x0028]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x001E]; // SW + table.PatternToTiles["ABABBAAA"] = [0x001E]; + table.PatternToTiles["BAAAAAAA"] = [0x0003]; // NW + table.PatternToTiles["AAABABBA"] = [0x0020]; + table.PatternToTiles["AAAABAAA"] = [0x001D]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x001D]; + table.PatternToTiles["AABAAAAA"] = [0x001F]; // NE + table.PatternToTiles["BAAAABAB"] = [0x001F]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x001C]; // NW + table.PatternToTiles["AAABBBBA"] = [0x001C]; + table.PatternToTiles["AABBBBAA"] = [0x001C]; + table.PatternToTiles["AAABBBAA"] = [0x001C]; + + table.PatternToTiles["BBBBBAAA"] = [0x001A]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x001A]; + table.PatternToTiles["ABBBBAAA"] = [0x001A]; + table.PatternToTiles["ABBBAAAA"] = [0x001A]; + + table.PatternToTiles["BBBAAABB"] = [0x001C]; // SE + table.PatternToTiles["BBAAAABB"] = [0x001C]; + table.PatternToTiles["BBBAAAAB"] = [0x001C]; + table.PatternToTiles["BBAAAAAB"] = [0x001C]; + + table.PatternToTiles["BAAABBBB"] = [0x001B]; // NE + table.PatternToTiles["AAAABBBB"] = [0x001B]; + table.PatternToTiles["BAAAABBB"] = [0x001B]; + table.PatternToTiles["AAAAABBB"] = [0x001B]; + + _dragonTransitions![(Biome.Grass, Biome.Water)] = table; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Jungle.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Jungle.cs new file mode 100644 index 00000000..6508341e --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Jungle.cs @@ -0,0 +1,169 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Jungle biome transitions from DragonMod. +/// +public partial class ImportColoredHeightmap +{ + /// + /// Add Jungle->Dirt transitions based on DragonMod jungle2dirt.txt + /// + private void AddJungleToDirtTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Jungle, ToBiome = Biome.Dirt }; + + // === EDGES === + // W edge: 0x599, 0x59D + table.PatternToTiles["BAAAAABB"] = [0x0599, 0x059D]; + table.PatternToTiles["BAAAAAAB"] = [0x0599, 0x059D]; + table.PatternToTiles["AAAAAABB"] = [0x0599, 0x059D]; + table.PatternToTiles["AAAAAAAB"] = [0x0599, 0x059D]; + table.PatternToTiles["BAAAAABA"] = [0x0599, 0x059D]; + + // E edge: 0x598, 0x59C + table.PatternToTiles["AABBBAAA"] = [0x0598, 0x059C]; + table.PatternToTiles["AAABBAAA"] = [0x0598, 0x059C]; + table.PatternToTiles["AABBAAAA"] = [0x0598, 0x059C]; + table.PatternToTiles["AAABAAAA"] = [0x0598, 0x059C]; + table.PatternToTiles["AABABAAA"] = [0x0598, 0x059C]; + + // S edge: 0x597, 0x59B + table.PatternToTiles["AAAABBBA"] = [0x0597, 0x059B]; + table.PatternToTiles["AAAAABBA"] = [0x0597, 0x059C]; + table.PatternToTiles["AAAABBAA"] = [0x0597, 0x059C]; + table.PatternToTiles["AAAAABAA"] = [0x0597, 0x059C]; + table.PatternToTiles["AAAABABA"] = [0x0597, 0x059C]; + + // N edge: 0x59A, 0x59E + table.PatternToTiles["BBBAAAAA"] = [0x059A, 0x059E]; + table.PatternToTiles["ABBAAAAA"] = [0x059A, 0x059E]; + table.PatternToTiles["BBAAAAAA"] = [0x059A, 0x059E]; + table.PatternToTiles["ABAAAAAA"] = [0x059A, 0x059E]; + table.PatternToTiles["BABAAAAA"] = [0x059A, 0x059E]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x05A0]; // SW + table.PatternToTiles["ABABBAAA"] = [0x05A0]; + table.PatternToTiles["BAAAAAAA"] = [0x05A3, 0x05A4]; // NW + table.PatternToTiles["AAABABBA"] = [0x05A3, 0x05A4]; + table.PatternToTiles["AAAABAAA"] = [0x05A5, 0x05A6]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x05A5, 0x05A6]; + table.PatternToTiles["AABAAAAA"] = [0x05A1, 0x059F]; // NE + table.PatternToTiles["BAAAABAB"] = [0x05A1, 0x059F]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x059B]; // NW + table.PatternToTiles["AAABBBBA"] = [0x059B]; + table.PatternToTiles["AABBBBAA"] = [0x059B]; + table.PatternToTiles["AAABBBAA"] = [0x059B]; + + table.PatternToTiles["BBBBBAAA"] = [0x059A]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x059A]; + table.PatternToTiles["ABBBBAAA"] = [0x059A]; + table.PatternToTiles["ABBBAAAA"] = [0x059A]; + + table.PatternToTiles["BBBAAABB"] = [0x059E]; // SE + table.PatternToTiles["BBAAAABB"] = [0x059E]; + table.PatternToTiles["BBBAAAAB"] = [0x059E]; + table.PatternToTiles["BBAAAAAB"] = [0x059E]; + + table.PatternToTiles["BAAABBBB"] = [0x0599]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0599]; + table.PatternToTiles["BAAAABBB"] = [0x0599]; + table.PatternToTiles["AAAAABBB"] = [0x0599]; + + // Nacor fallback: 0x638 + table.PatternToTiles["ABBBBBBB"] = [0x0638]; + table.PatternToTiles["BABBBBBB"] = [0x0638]; + table.PatternToTiles["BBABBBBB"] = [0x0638]; + table.PatternToTiles["BBBABBBB"] = [0x0638]; + table.PatternToTiles["BBBBABBB"] = [0x0638]; + table.PatternToTiles["BBBBBABB"] = [0x0638]; + table.PatternToTiles["BBBBBBAB"] = [0x0638]; + table.PatternToTiles["BBBBBBBA"] = [0x0638]; + table.PatternToTiles["BBBBBBBB"] = [0x0638]; + + _dragonTransitions![(Biome.Jungle, Biome.Dirt)] = table; + } + + /// + /// Add Jungle->Water transitions based on DragonMod jungle2water.txt + /// + private void AddJungleToWaterTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Jungle, ToBiome = Biome.Water }; + + // === EDGES === + // W edge: 0x779, 0x780 + table.PatternToTiles["BAAAAABB"] = [0x0779, 0x0780]; + table.PatternToTiles["BAAAAAAB"] = [0x0779, 0x0780]; + table.PatternToTiles["AAAAAABB"] = [0x0779, 0x0780]; + table.PatternToTiles["AAAAAAAB"] = [0x0779, 0x0780]; + table.PatternToTiles["BAAAAABA"] = [0x0779, 0x0780]; + + // E edge: 0x778, 0x785 + table.PatternToTiles["AABBBAAA"] = [0x0778, 0x0785]; + table.PatternToTiles["AAABBAAA"] = [0x0778, 0x0785]; + table.PatternToTiles["AABBAAAA"] = [0x0778, 0x0785]; + table.PatternToTiles["AAABAAAA"] = [0x0778, 0x0785]; + table.PatternToTiles["AABABAAA"] = [0x0778, 0x0785]; + + // S edge: 0x77D, 0x784 + table.PatternToTiles["AAAABBBA"] = [0x077D, 0x0784]; + table.PatternToTiles["AAAAABBA"] = [0x077D, 0x0784]; + table.PatternToTiles["AAAABBAA"] = [0x077D, 0x0784]; + table.PatternToTiles["AAAAABAA"] = [0x077D, 0x0784]; + table.PatternToTiles["AAAABABA"] = [0x077D, 0x0784]; + + // N edge: 0x77C, 0x783 + table.PatternToTiles["BBBAAAAA"] = [0x077C, 0x0783]; + table.PatternToTiles["ABBAAAAA"] = [0x077C, 0x0783]; + table.PatternToTiles["BBAAAAAA"] = [0x077C, 0x0783]; + table.PatternToTiles["ABAAAAAA"] = [0x077C, 0x0783]; + table.PatternToTiles["BABAAAAA"] = [0x077C, 0x0783]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x078C]; // SW + table.PatternToTiles["ABABBAAA"] = [0x078C]; + table.PatternToTiles["BAAAAAAA"] = [0x078A]; // NW + table.PatternToTiles["AAABABBA"] = [0x078A]; + table.PatternToTiles["AAAABAAA"] = [0x078D]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x078D]; + table.PatternToTiles["AABAAAAA"] = [0x078B]; // NE + table.PatternToTiles["BAAAABAB"] = [0x078B]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x02ED]; // NW + table.PatternToTiles["AAABBBBA"] = [0x02ED]; + table.PatternToTiles["AABBBBAA"] = [0x02ED]; + table.PatternToTiles["AAABBBAA"] = [0x02ED]; + + table.PatternToTiles["BBBBBAAA"] = [0x008D]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x008D]; + table.PatternToTiles["ABBBBAAA"] = [0x008D]; + table.PatternToTiles["ABBBAAAA"] = [0x008D]; + + table.PatternToTiles["BBBAAABB"] = [0x0095]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0095]; + table.PatternToTiles["BBBAAAAB"] = [0x0095]; + table.PatternToTiles["BBAAAAAB"] = [0x0095]; + + table.PatternToTiles["BAAABBBB"] = [0x0091]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0091]; + table.PatternToTiles["BAAAABBB"] = [0x0091]; + table.PatternToTiles["AAAAABBB"] = [0x0091]; + + // Nacor fallback: 0xAA + table.PatternToTiles["ABBBBBBB"] = [0x00AA]; + table.PatternToTiles["BABBBBBB"] = [0x00AA]; + table.PatternToTiles["BBABBBBB"] = [0x00AA]; + table.PatternToTiles["BBBABBBB"] = [0x00AA]; + table.PatternToTiles["BBBBABBB"] = [0x00AA]; + table.PatternToTiles["BBBBBABB"] = [0x00AA]; + table.PatternToTiles["BBBBBBAB"] = [0x00AA]; + table.PatternToTiles["BBBBBBBA"] = [0x00AA]; + table.PatternToTiles["BBBBBBBB"] = [0x00AA]; + + _dragonTransitions![(Biome.Jungle, Biome.Water)] = table; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Sand.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Sand.cs new file mode 100644 index 00000000..1be3d6fe --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Sand.cs @@ -0,0 +1,239 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Sand biome transitions from DragonMod. +/// +public partial class ImportColoredHeightmap +{ + /// + /// Add Sand->Dirt transitions based on DragonMod sand2dirt.txt + /// + private void AddSandToDirtTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Sand, ToBiome = Biome.Dirt }; + + // === EDGES === + // W edge: 0x337 + table.PatternToTiles["BAAAAABB"] = [0x0337]; + table.PatternToTiles["BAAAAAAB"] = [0x0337]; + table.PatternToTiles["AAAAAABB"] = [0x0337]; + table.PatternToTiles["AAAAAAAB"] = [0x0337]; + table.PatternToTiles["BAAAAABA"] = [0x0337]; + + // E edge: 0x336 + table.PatternToTiles["AABBBAAA"] = [0x0336]; + table.PatternToTiles["AAABBAAA"] = [0x0336]; + table.PatternToTiles["AABBAAAA"] = [0x0336]; + table.PatternToTiles["AAABAAAA"] = [0x0336]; + table.PatternToTiles["AABABAAA"] = [0x0336]; + + // S edge: 0x335 + table.PatternToTiles["AAAABBBA"] = [0x0335]; + table.PatternToTiles["AAAAABBA"] = [0x0335]; + table.PatternToTiles["AAAABBAA"] = [0x0335]; + table.PatternToTiles["AAAAABAA"] = [0x0335]; + table.PatternToTiles["AAAABABA"] = [0x0335]; + + // N edge: 0x338 + table.PatternToTiles["BBBAAAAA"] = [0x0338]; + table.PatternToTiles["ABBAAAAA"] = [0x0338]; + table.PatternToTiles["BBAAAAAA"] = [0x0338]; + table.PatternToTiles["ABAAAAAA"] = [0x0338]; + table.PatternToTiles["BABAAAAA"] = [0x0338]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x033C]; // SW + table.PatternToTiles["ABABBAAA"] = [0x033C]; + table.PatternToTiles["BAAAAAAA"] = [0x0339]; // NW + table.PatternToTiles["AAABABBA"] = [0x0339]; + table.PatternToTiles["AAAABAAA"] = [0x033A]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x033A]; + table.PatternToTiles["AABAAAAA"] = [0x033B]; // NE + table.PatternToTiles["BAAAABAB"] = [0x033B]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x033D]; // NW + table.PatternToTiles["AAABBBBA"] = [0x033D]; + table.PatternToTiles["AABBBBAA"] = [0x033D]; + table.PatternToTiles["AAABBBAA"] = [0x033D]; + + table.PatternToTiles["BBBBBAAA"] = [0x0340]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0340]; + table.PatternToTiles["ABBBBAAA"] = [0x0340]; + table.PatternToTiles["ABBBAAAA"] = [0x0340]; + + table.PatternToTiles["BBBAAABB"] = [0x033E]; // SE + table.PatternToTiles["BBAAAABB"] = [0x033E]; + table.PatternToTiles["BBBAAAAB"] = [0x033E]; + table.PatternToTiles["BBAAAAAB"] = [0x033E]; + + table.PatternToTiles["BAAABBBB"] = [0x033F]; // NE + table.PatternToTiles["AAAABBBB"] = [0x033F]; + table.PatternToTiles["BAAAABBB"] = [0x033F]; + table.PatternToTiles["AAAAABBB"] = [0x033F]; + + // Nacor fallback: 0x638 + table.PatternToTiles["ABBBBBBB"] = [0x0638]; + table.PatternToTiles["BABBBBBB"] = [0x0638]; + table.PatternToTiles["BBABBBBB"] = [0x0638]; + table.PatternToTiles["BBBABBBB"] = [0x0638]; + table.PatternToTiles["BBBBABBB"] = [0x0638]; + table.PatternToTiles["BBBBBABB"] = [0x0638]; + table.PatternToTiles["BBBBBBAB"] = [0x0638]; + table.PatternToTiles["BBBBBBBA"] = [0x0638]; + table.PatternToTiles["BBBBBBBB"] = [0x0638]; + + _dragonTransitions![(Biome.Sand, Biome.Dirt)] = table; + } + + /// + /// Add Sand->Rock transitions based on DragonMod sand2mountain.txt + /// + private void AddSandToRockTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Sand, ToBiome = Biome.Rock }; + + // === EDGES === + // W edge: 0x128 + table.PatternToTiles["BAAAAABB"] = [0x0128]; + table.PatternToTiles["BAAAAAAB"] = [0x0128]; + table.PatternToTiles["AAAAAABB"] = [0x0128]; + table.PatternToTiles["AAAAAAAB"] = [0x0128]; + table.PatternToTiles["BAAAAABA"] = [0x0128]; + + // E edge: 0x126 + table.PatternToTiles["AABBBAAA"] = [0x0126]; + table.PatternToTiles["AAABBAAA"] = [0x0126]; + table.PatternToTiles["AABBAAAA"] = [0x0126]; + table.PatternToTiles["AAABAAAA"] = [0x0126]; + table.PatternToTiles["AABABAAA"] = [0x0126]; + + // S edge: 0x127 + table.PatternToTiles["AAAABBBA"] = [0x0127]; + table.PatternToTiles["AAAAABBA"] = [0x0127]; + table.PatternToTiles["AAAABBAA"] = [0x0127]; + table.PatternToTiles["AAAAABAA"] = [0x0127]; + table.PatternToTiles["AAAABABA"] = [0x0127]; + + // N edge: 0x129 + table.PatternToTiles["BBBAAAAA"] = [0x0129]; + table.PatternToTiles["ABBAAAAA"] = [0x0129]; + table.PatternToTiles["BBAAAAAA"] = [0x0129]; + table.PatternToTiles["ABAAAAAA"] = [0x0129]; + table.PatternToTiles["BABAAAAA"] = [0x0129]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x07C0]; // SW + table.PatternToTiles["ABABBAAA"] = [0x07C0]; + table.PatternToTiles["BAAAAAAA"] = [0x07BD]; // NW + table.PatternToTiles["AAABABBA"] = [0x07BD]; + table.PatternToTiles["AAAABAAA"] = [0x07BE]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x07BE]; + table.PatternToTiles["AABAAAAA"] = [0x012D]; // NE + table.PatternToTiles["BAAAABAB"] = [0x07BF]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x0124]; // NW + table.PatternToTiles["AAABBBBA"] = [0x0124]; + table.PatternToTiles["AABBBBAA"] = [0x0124]; + table.PatternToTiles["AAABBBAA"] = [0x0124]; + + table.PatternToTiles["BBBBBAAA"] = [0x0123]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0123]; + table.PatternToTiles["ABBBBAAA"] = [0x0123]; + table.PatternToTiles["ABBBAAAA"] = [0x0123]; + + table.PatternToTiles["BBBAAABB"] = [0x0122]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0122]; + table.PatternToTiles["BBBAAAAB"] = [0x0122]; + table.PatternToTiles["BBAAAAAB"] = [0x0122]; + + table.PatternToTiles["BAAABBBB"] = [0x0125]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0125]; + table.PatternToTiles["BAAAABBB"] = [0x0125]; + table.PatternToTiles["AAAAABBB"] = [0x0125]; + + _dragonTransitions![(Biome.Sand, Biome.Rock)] = table; + } + + /// + /// Add Sand->Water transitions based on DragonMod sand2water.txt + /// + private void AddSandToWaterTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Sand, ToBiome = Biome.Water }; + + // === EDGES === + // W edge: 0x1C1, 0x1C5 + table.PatternToTiles["BAAAAABB"] = [0x01C1, 0x01C5]; + table.PatternToTiles["BAAAAAAB"] = [0x01C1, 0x01C5]; + table.PatternToTiles["AAAAAABB"] = [0x01C1, 0x01C5]; + table.PatternToTiles["AAAAAAAB"] = [0x01C1, 0x01C5]; + table.PatternToTiles["BAAAAABA"] = [0x01C1, 0x01C5]; + + // E edge: 0x1C2, 0x1C6 + table.PatternToTiles["AABBBAAA"] = [0x01C2, 0x01C6]; + table.PatternToTiles["AAABBAAA"] = [0x01C2, 0x01C6]; + table.PatternToTiles["AABBAAAA"] = [0x01C2, 0x01C6]; + table.PatternToTiles["AAABAAAA"] = [0x01C2, 0x01C6]; + table.PatternToTiles["AABABAAA"] = [0x01C2, 0x01C6]; + + // S edge: 0x1C4, 0x1C8 + table.PatternToTiles["AAAABBBA"] = [0x01C4, 0x01C8]; + table.PatternToTiles["AAAAABBA"] = [0x01C4, 0x01C8]; + table.PatternToTiles["AAAABBAA"] = [0x01C4, 0x01C8]; + table.PatternToTiles["AAAAABAA"] = [0x01C4, 0x01C8]; + table.PatternToTiles["AAAABABA"] = [0x01C4, 0x01C8]; + + // N edge: 0x1C3, 0x1C7 + table.PatternToTiles["BBBAAAAA"] = [0x01C3, 0x01C7]; + table.PatternToTiles["ABBAAAAA"] = [0x01C3, 0x01C7]; + table.PatternToTiles["BBAAAAAA"] = [0x01C3, 0x01C7]; + table.PatternToTiles["ABAAAAAA"] = [0x01C3, 0x01C7]; + table.PatternToTiles["BABAAAAA"] = [0x01C3, 0x01C7]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x01BD]; // SW + table.PatternToTiles["ABABBAAA"] = [0x01BD]; + table.PatternToTiles["BAAAAAAA"] = [0x01BF]; // NW + table.PatternToTiles["AAABABBA"] = [0x01BF]; + table.PatternToTiles["AAAABAAA"] = [0x01BC]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x01BC]; + table.PatternToTiles["AABAAAAA"] = [0x01BE]; // NE + table.PatternToTiles["BAAAABAB"] = [0x01BE]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x01BB]; // NW + table.PatternToTiles["AAABBBBA"] = [0x01BB]; + table.PatternToTiles["AABBBBAA"] = [0x01BB]; + table.PatternToTiles["AAABBBAA"] = [0x01BB]; + + table.PatternToTiles["BBBBBAAA"] = [0x01B9]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x01B9]; + table.PatternToTiles["ABBBBAAA"] = [0x01B9]; + table.PatternToTiles["ABBBAAAA"] = [0x01B9]; + + table.PatternToTiles["BBBAAABB"] = [0x01BB]; // SE + table.PatternToTiles["BBAAAABB"] = [0x01BB]; + table.PatternToTiles["BBBAAAAB"] = [0x01BB]; + table.PatternToTiles["BBAAAAAB"] = [0x01BB]; + + table.PatternToTiles["BAAABBBB"] = [0x01BA]; // NE + table.PatternToTiles["AAAABBBB"] = [0x01BA]; + table.PatternToTiles["BAAAABBB"] = [0x01BA]; + table.PatternToTiles["AAAAABBB"] = [0x01BA]; + + // Nacor fallback: 0xAA + table.PatternToTiles["ABBBBBBB"] = [0x00AA]; + table.PatternToTiles["BABBBBBB"] = [0x00AA]; + table.PatternToTiles["BBABBBBB"] = [0x00AA]; + table.PatternToTiles["BBBABBBB"] = [0x00AA]; + table.PatternToTiles["BBBBABBB"] = [0x00AA]; + table.PatternToTiles["BBBBBABB"] = [0x00AA]; + table.PatternToTiles["BBBBBBAB"] = [0x00AA]; + table.PatternToTiles["BBBBBBBA"] = [0x00AA]; + table.PatternToTiles["BBBBBBBB"] = [0x00AA]; + + _dragonTransitions![(Biome.Sand, Biome.Water)] = table; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Snow.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Snow.cs new file mode 100644 index 00000000..19c51b82 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.Snow.cs @@ -0,0 +1,239 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Snow biome transitions from DragonMod. +/// +public partial class ImportColoredHeightmap +{ + /// + /// Add Snow->Dirt transitions based on DragonMod snow2dirt.txt + /// + private void AddSnowToDirtTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Snow, ToBiome = Biome.Dirt }; + + // === EDGES === + // W edge: 0x387 + table.PatternToTiles["BAAAAABB"] = [0x0387]; + table.PatternToTiles["BAAAAAAB"] = [0x0387]; + table.PatternToTiles["AAAAAABB"] = [0x0387]; + table.PatternToTiles["AAAAAAAB"] = [0x0387]; + table.PatternToTiles["BAAAAABA"] = [0x0387]; + + // E edge: 0x386 + table.PatternToTiles["AABBBAAA"] = [0x0386]; + table.PatternToTiles["AAABBAAA"] = [0x0386]; + table.PatternToTiles["AABBAAAA"] = [0x0386]; + table.PatternToTiles["AAABAAAA"] = [0x0386]; + table.PatternToTiles["AABABAAA"] = [0x0386]; + + // S edge: 0x385 + table.PatternToTiles["AAAABBBA"] = [0x0385]; + table.PatternToTiles["AAAAABBA"] = [0x0385]; + table.PatternToTiles["AAAABBAA"] = [0x0385]; + table.PatternToTiles["AAAAABAA"] = [0x0385]; + table.PatternToTiles["AAAABABA"] = [0x0385]; + + // N edge: 0x388 + table.PatternToTiles["BBBAAAAA"] = [0x0388]; + table.PatternToTiles["ABBAAAAA"] = [0x0388]; + table.PatternToTiles["BBAAAAAA"] = [0x0388]; + table.PatternToTiles["ABAAAAAA"] = [0x0388]; + table.PatternToTiles["BABAAAAA"] = [0x0388]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x038C]; // SW + table.PatternToTiles["ABABBAAA"] = [0x038C]; + table.PatternToTiles["BAAAAAAA"] = [0x0389]; // NW + table.PatternToTiles["AAABABBA"] = [0x0389]; + table.PatternToTiles["AAAABAAA"] = [0x038A]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x038A]; + table.PatternToTiles["AABAAAAA"] = [0x038B]; // NE + table.PatternToTiles["BAAAABAB"] = [0x038B]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x038D]; // NW + table.PatternToTiles["AAABBBBA"] = [0x038D]; + table.PatternToTiles["AABBBBAA"] = [0x038D]; + table.PatternToTiles["AAABBBAA"] = [0x038D]; + + table.PatternToTiles["BBBBBAAA"] = [0x0390]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0390]; + table.PatternToTiles["ABBBBAAA"] = [0x0390]; + table.PatternToTiles["ABBBAAAA"] = [0x0390]; + + table.PatternToTiles["BBBAAABB"] = [0x038E]; // SE + table.PatternToTiles["BBAAAABB"] = [0x038E]; + table.PatternToTiles["BBBAAAAB"] = [0x038E]; + table.PatternToTiles["BBAAAAAB"] = [0x038E]; + + table.PatternToTiles["BAAABBBB"] = [0x038F]; // NE + table.PatternToTiles["AAAABBBB"] = [0x038F]; + table.PatternToTiles["BAAAABBB"] = [0x038F]; + table.PatternToTiles["AAAAABBB"] = [0x038F]; + + // Nacor fallback: 0x638 + table.PatternToTiles["ABBBBBBB"] = [0x0638]; + table.PatternToTiles["BABBBBBB"] = [0x0638]; + table.PatternToTiles["BBABBBBB"] = [0x0638]; + table.PatternToTiles["BBBABBBB"] = [0x0638]; + table.PatternToTiles["BBBBABBB"] = [0x0638]; + table.PatternToTiles["BBBBBABB"] = [0x0638]; + table.PatternToTiles["BBBBBBAB"] = [0x0638]; + table.PatternToTiles["BBBBBBBA"] = [0x0638]; + table.PatternToTiles["BBBBBBBB"] = [0x0638]; + + _dragonTransitions![(Biome.Snow, Biome.Dirt)] = table; + } + + /// + /// Add Snow->Rock transitions based on DragonMod snow2mountain.txt + /// + private void AddSnowToRockTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Snow, ToBiome = Biome.Rock }; + + // === EDGES === + // W edge: 0x115 + table.PatternToTiles["BAAAAABB"] = [0x0115]; + table.PatternToTiles["BAAAAAAB"] = [0x0115]; + table.PatternToTiles["AAAAAABB"] = [0x0115]; + table.PatternToTiles["AAAAAAAB"] = [0x0115]; + table.PatternToTiles["BAAAAABA"] = [0x0115]; + + // E edge: 0x117 + table.PatternToTiles["AABBBAAA"] = [0x0117]; + table.PatternToTiles["AAABBAAA"] = [0x0117]; + table.PatternToTiles["AABBAAAA"] = [0x0117]; + table.PatternToTiles["AAABAAAA"] = [0x0117]; + table.PatternToTiles["AABABAAA"] = [0x0117]; + + // S edge: 0x114 + table.PatternToTiles["AAAABBBA"] = [0x0114]; + table.PatternToTiles["AAAAABBA"] = [0x0114]; + table.PatternToTiles["AAAABBAA"] = [0x0114]; + table.PatternToTiles["AAAAABAA"] = [0x0114]; + table.PatternToTiles["AAAABABA"] = [0x0114]; + + // N edge: 0x116 + table.PatternToTiles["BBBAAAAA"] = [0x0116]; + table.PatternToTiles["ABBAAAAA"] = [0x0116]; + table.PatternToTiles["BBAAAAAA"] = [0x0116]; + table.PatternToTiles["ABAAAAAA"] = [0x0116]; + table.PatternToTiles["BABAAAAA"] = [0x0116]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x010D]; // SW + table.PatternToTiles["ABABBAAA"] = [0x010D]; + table.PatternToTiles["BAAAAAAA"] = [0x010E]; // NW + table.PatternToTiles["AAABABBA"] = [0x010E]; + table.PatternToTiles["AAAABAAA"] = [0x010C]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x010C]; + table.PatternToTiles["AABAAAAA"] = [0x010F]; // NE + table.PatternToTiles["BAAAABAB"] = [0x010F]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x0112]; // NW + table.PatternToTiles["AAABBBBA"] = [0x0112]; + table.PatternToTiles["AABBBBAA"] = [0x0112]; + table.PatternToTiles["AAABBBAA"] = [0x0112]; + + table.PatternToTiles["BBBBBAAA"] = [0x0111]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x0111]; + table.PatternToTiles["ABBBBAAA"] = [0x0111]; + table.PatternToTiles["ABBBAAAA"] = [0x0111]; + + table.PatternToTiles["BBBAAABB"] = [0x0110]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0110]; + table.PatternToTiles["BBBAAAAB"] = [0x0110]; + table.PatternToTiles["BBAAAAAB"] = [0x0110]; + + table.PatternToTiles["BAAABBBB"] = [0x0113]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0113]; + table.PatternToTiles["BAAAABBB"] = [0x0113]; + table.PatternToTiles["AAAAABBB"] = [0x0113]; + + _dragonTransitions![(Biome.Snow, Biome.Rock)] = table; + } + + /// + /// Add Snow->Water transitions based on DragonMod snow2water.txt + /// + private void AddSnowToWaterTransitions() + { + var table = new DragonTransitionTable { FromBiome = Biome.Snow, ToBiome = Biome.Water }; + + // === EDGES === + // W edge: 0x30F, 0x313 + table.PatternToTiles["BAAAAABB"] = [0x030F, 0x0313]; + table.PatternToTiles["BAAAAAAB"] = [0x030F, 0x0313]; + table.PatternToTiles["AAAAAABB"] = [0x030F, 0x0313]; + table.PatternToTiles["AAAAAAAB"] = [0x030F, 0x0313]; + table.PatternToTiles["BAAAAABA"] = [0x030F, 0x0313]; + + // E edge: 0x310, 0x314 + table.PatternToTiles["AABBBAAA"] = [0x0310, 0x0314]; + table.PatternToTiles["AAABBAAA"] = [0x0310, 0x0314]; + table.PatternToTiles["AABBAAAA"] = [0x0310, 0x0314]; + table.PatternToTiles["AAABAAAA"] = [0x0310, 0x0314]; + table.PatternToTiles["AABABAAA"] = [0x0310, 0x0314]; + + // S edge: 0x312 + table.PatternToTiles["AAAABBBA"] = [0x0312]; + table.PatternToTiles["AAAAABBA"] = [0x0312]; + table.PatternToTiles["AAAABBAA"] = [0x0312]; + table.PatternToTiles["AAAAABAA"] = [0x0312]; + table.PatternToTiles["AAAABABA"] = [0x0312]; + + // N edge: 0x311, 0x315 + table.PatternToTiles["BBBAAAAA"] = [0x0311, 0x0315]; + table.PatternToTiles["ABBAAAAA"] = [0x0315, 0x0311]; + table.PatternToTiles["BBAAAAAA"] = [0x0311, 0x0315]; + table.PatternToTiles["ABAAAAAA"] = [0x0311, 0x0315]; + table.PatternToTiles["BABAAAAA"] = [0x0311, 0x0315]; + + // === INNER CORNERS === + table.PatternToTiles["AAAAAABA"] = [0x030A, 0x0306]; // SW + table.PatternToTiles["ABABBAAA"] = [0x030A, 0x0306]; + table.PatternToTiles["BAAAAAAA"] = [0x0308, 0x030C]; // NW + table.PatternToTiles["AAABABBA"] = [0x0308, 0x030C]; + table.PatternToTiles["AAAABAAA"] = [0x0307, 0x030B]; // SE + table.PatternToTiles["ABBAAAAB"] = [0x0307, 0x030B]; + table.PatternToTiles["AABAAAAA"] = [0x0309, 0x030D]; // NE + table.PatternToTiles["BAAAABAB"] = [0x0309, 0x030D]; + + // === OUTER CORNERS === + table.PatternToTiles["AABBBBBA"] = [0x02ED]; // NW + table.PatternToTiles["AAABBBBA"] = [0x02ED]; + table.PatternToTiles["AABBBBAA"] = [0x02ED]; + table.PatternToTiles["AAABBBAA"] = [0x02ED]; + + table.PatternToTiles["BBBBBAAA"] = [0x008D]; // SW + table.PatternToTiles["BBBBAAAA"] = [0x008D]; + table.PatternToTiles["ABBBBAAA"] = [0x008D]; + table.PatternToTiles["ABBBAAAA"] = [0x008D]; + + table.PatternToTiles["BBBAAABB"] = [0x0095]; // SE + table.PatternToTiles["BBAAAABB"] = [0x0095]; + table.PatternToTiles["BBBAAAAB"] = [0x0095]; + table.PatternToTiles["BBAAAAAB"] = [0x0095]; + + table.PatternToTiles["BAAABBBB"] = [0x0091]; // NE + table.PatternToTiles["AAAABBBB"] = [0x0091]; + table.PatternToTiles["BAAAABBB"] = [0x0091]; + table.PatternToTiles["AAAAABBB"] = [0x0091]; + + // Nacor fallback: 0xAA + table.PatternToTiles["ABBBBBBB"] = [0x00AA]; + table.PatternToTiles["BABBBBBB"] = [0x00AA]; + table.PatternToTiles["BBABBBBB"] = [0x00AA]; + table.PatternToTiles["BBBABBBB"] = [0x00AA]; + table.PatternToTiles["BBBBABBB"] = [0x00AA]; + table.PatternToTiles["BBBBBABB"] = [0x00AA]; + table.PatternToTiles["BBBBBBAB"] = [0x00AA]; + table.PatternToTiles["BBBBBBBA"] = [0x00AA]; + table.PatternToTiles["BBBBBBBB"] = [0x00AA]; + + _dragonTransitions![(Biome.Snow, Biome.Water)] = table; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.cs new file mode 100644 index 00000000..7901b810 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.DragonTransitions.cs @@ -0,0 +1,153 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Partial class implementing DragonMod-style AAAABBBB transition system. +/// +/// The AAAABBBB system works by examining the 8 neighbors around a center tile +/// in clockwise order starting from NW: [NW, N, NE, E, SE, S, SW, W] +/// +/// Each position is marked as: +/// - A = same biome as center (the tile being processed) +/// - B = target biome (the neighboring biome) +/// +/// IMPORTANT: This is UNIDIRECTIONAL. For each biome pair (e.g., Grass-Dirt), +/// we only define ONE transition direction based on which biome "owns" the border. +/// The Dragon system processes the CENTER tile and looks at its neighbors. +/// +/// From grass2dirt.txt in DragonMod: +/// - TileA = Grass (center tile) +/// - TileB = Dirt (neighbor) +/// - Pattern describes WHERE the Dirt neighbors are +/// +public partial class ImportColoredHeightmap +{ + /// + /// Structure to hold Dragon-style transition definitions for a biome pair. + /// + private class DragonTransitionTable + { + public Biome FromBiome { get; init; } + public Biome ToBiome { get; init; } + public Dictionary PatternToTiles { get; } = new(); + } + + // Dragon-style transition tables + private Dictionary<(Biome from, Biome to), DragonTransitionTable>? _dragonTransitions; + + /// + /// Initialize Dragon-style transition tables. + /// Based on DragonMod transition file format. + /// + private void InitializeDragonTransitions() + { + _dragonTransitions = new Dictionary<(Biome from, Biome to), DragonTransitionTable>(); + + // Pattern positions: [0]=NW, [1]=N, [2]=NE, [3]=E, [4]=SE, [5]=S, [6]=SW, [7]=W + // A = center biome (TileA), B = neighbor biome (TileB) + + // === GRASS transitions (Grass is center, draws transitions to neighbors) === + AddGrassToDirtTransitions(); + AddGrassToSandTransitions(); + AddGrassToForestTransitions(); + AddGrassToSnowTransitions(); + AddGrassToRockTransitions(); + AddGrassToCobblestoneTransitions(); + AddGrassToSwampTransitions(); + AddGrassToJungleTransitions(); + AddGrassToWaterTransitions(); + + // === SNOW transitions (Snow is center) === + AddSnowToDirtTransitions(); + AddSnowToRockTransitions(); + AddSnowToWaterTransitions(); + + // === SAND transitions (Sand is center) === + AddSandToDirtTransitions(); + AddSandToRockTransitions(); + AddSandToWaterTransitions(); + + // === DIRT transitions (Dirt is center) === + AddDirtToCobblestoneTransitions(); + AddDirtToRockTransitions(); + AddDirtToWaterTransitions(); + + // === FOREST transitions (Forest is center) === + AddForestToDirtTransitions(); + AddForestToSandTransitions(); + AddForestToWaterTransitions(); + + // === JUNGLE transitions (Jungle is center) === + AddJungleToDirtTransitions(); + AddJungleToWaterTransitions(); + + Console.WriteLine($"Dragon transitions initialized: {_dragonTransitions.Count} biome pairs"); + } + + /// + /// Calculate transition tile using Dragon AAAABBBB pattern system. + /// + private ushort CalculateDragonTransition(int px, int py, Biome centerBiome, int width, int height) + { + if (_biomeCache == null || _dragonTransitions == null) + return 0; + + Biome GetBiome(int x, int y) + { + if (x < 0 || x >= width || y < 0 || y >= height) + return centerBiome; + return _biomeCache[x, y]; + } + + // Get all 8 neighbors in Dragon order: NW, N, NE, E, SE, S, SW, W + Biome[] neighbors = + [ + GetBiome(px - 1, py - 1), // NW [0] + GetBiome(px, py - 1), // N [1] + GetBiome(px + 1, py - 1), // NE [2] + GetBiome(px + 1, py), // E [3] + GetBiome(px + 1, py + 1), // SE [4] + GetBiome(px, py + 1), // S [5] + GetBiome(px - 1, py + 1), // SW [6] + GetBiome(px - 1, py) // W [7] + ]; + + // Find unique neighbor biomes that are different from center + var differentBiomes = new HashSet(); + foreach (var b in neighbors) + { + if (b != centerBiome && b != Biome.Void) + differentBiomes.Add(b); + } + + if (differentBiomes.Count == 0) + return 0; + + // Try each different biome as the target + foreach (var targetBiome in differentBiomes) + { + if (!_dragonTransitions.TryGetValue((centerBiome, targetBiome), out var table)) + continue; + + // Build the AAAABBBB pattern + var pattern = BuildPattern(neighbors, targetBiome); + + // Look up exact pattern + if (table.PatternToTiles.TryGetValue(pattern, out var tiles) && tiles.Length > 0) + return tiles[_random.Next(tiles.Length)]; + } + + return 0; + } + + /// + /// Build 8-character pattern string from neighbors. + /// + private static string BuildPattern(Biome[] neighbors, Biome targetBiome) + { + return string.Create(8, (neighbors, targetBiome), (span, state) => + { + for (int i = 0; i < 8; i++) + span[i] = state.neighbors[i] == state.targetBiome ? 'B' : 'A'; + }); + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.TileData.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.TileData.cs new file mode 100644 index 00000000..d26845fc --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.TileData.cs @@ -0,0 +1,138 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Partial class containing tile ID definitions for ImportColoredHeightmap. +/// +public partial class ImportColoredHeightmap +{ + // =================================================================================== + // LAND TILE IDS (FLOORS) - Complete list + // =================================================================================== + + // Natural terrain + private static readonly ushort[] GrassTiles = [ + 0x0003, 0x0004, 0x0005, 0x0006, // grass + 0x037B, 0x037C, 0x037D, 0x037E // grass variant + ]; + private static readonly ushort[] SandTiles = [0x0016, 0x0017, 0x0018, 0x0019]; + private static readonly ushort[] DirtTiles = [0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078]; + private static readonly ushort[] WaterTiles = [0x00A8, 0x00A9, 0x00AA, 0x00AB]; // Only for islands + private static readonly ushort[] JungleTiles = [0x00AC, 0x00AD, 0x00AE, 0x00AF]; + private static readonly ushort[] SnowTiles = [0x011A, 0x011B, 0x011C, 0x011D]; + private static readonly ushort[] SwampTiles = [0x3DE9, 0x3DEA, 0x3DEB, 0x3DEC]; + private static readonly ushort[] VoidTiles = [0x01FA, 0x01FB, 0x01FC, 0x01FD, 0x01FE, 0x01FF]; // Gaps between maps + + // Stone/dungeon floors + private static readonly ushort[] SandstoneFloorTiles = [ + 0x0201, 0x0202, 0x0203, 0x0204, // sandstone floor + 0x044E, 0x044F, 0x0450, 0x0451, 0x0452, 0x0453, 0x0454, 0x0455 // sandstone variant + ]; + private static readonly ushort[] MarbleBrownFloorTiles = [ + 0x0205, 0x0206, 0x0207, 0x0208, 0x0209, 0x020A, 0x020B, 0x020C + ]; + private static readonly ushort[] MarbleBlueFloorTiles = [ + 0x0210, 0x0211, 0x0212, 0x0213, 0x0214, 0x0215, 0x0216, 0x0217, 0x0218 + ]; + private static readonly ushort[] CobblestoneFloorTiles = [0x03E9, 0x03EA, 0x03EB, 0x03EC]; + private static readonly ushort[] StoneGrayFloorTiles = [ + 0x0436, 0x0437, 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, 0x0440, 0x0441 + ]; + private static readonly ushort[] BrickFloorTiles = [ + 0x047A, 0x047B, 0x047C, 0x047D, 0x047E, 0x047F, 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, + 0x048F, 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496 + ]; + private static readonly ushort[] MarbleWhiteFloorTiles = [0x0486, 0x0487, 0x0488, 0x0489]; + private static readonly ushort[] MarbleGreenFloorTiles = [0x04E7, 0x04E8]; + private static readonly ushort[] AcidFloorTiles = [ + 0x2E2A, 0x2E2B, 0x2E2C, 0x2E2D, 0x2E2E, 0x2E2F, 0x2E30, 0x2E31, + 0x2E32, 0x2E33, 0x2E34, 0x2E35, 0x2E36, 0x2E37, 0x2E38, 0x2E39, 0x2E3A, 0x2E3B + ]; + + // Legacy/compatibility + private static readonly ushort[] ForestTiles = [0x00C4, 0x00C5, 0x00C6, 0x00C7]; + private static readonly ushort[] RockTiles = [0x022C, 0x022D, 0x022E, 0x022F]; + private static readonly ushort[] LavaTiles = [0x01F4, 0x01F5, 0x01F6, 0x01F7]; + private static readonly ushort[] CaveTiles = [0x0053, 0x0054, 0x0055, 0x0056]; + + // =================================================================================== + // BORDER TILES (GRASS/FOREST COASTLINE EDGES) + // These are land tiles placed where grass/forest meets water + // =================================================================================== + + // Grass border tiles - water direction indicated by tile (LAND TILES ONLY) + // Note: SW/NE use corner static 0x0095 placed in vegetation layer at offset (x-1, y+1) + private static readonly ushort[] GrassBorderWTiles = [0x0096]; // Water to West + private static readonly ushort[] GrassBorderNWTiles = [0x00A0]; // Water to NorthWest + private static readonly ushort[] GrassBorderSETiles = [0x0093]; // Water to SouthEast + private static readonly ushort[] GrassBorderNTiles = [0x0099]; // Water to North + private static readonly ushort[] GrassBorderSTiles = [0x0098]; // Water to South + private static readonly ushort[] GrassBorderETiles = [0x008E]; // Water to East + + // Forest border tiles - water direction indicated by tile (LAND TILES ONLY) + // Note: SW/NE use corner static 0x0095 placed in vegetation layer at offset (x-1, y+1) + private static readonly ushort[] ForestBorderWTiles = [0x02EE]; // Water to West + private static readonly ushort[] ForestBorderNWTiles = [0x00C5]; // Water to NorthWest + private static readonly ushort[] ForestBorderSETiles = [0x02F0]; // Water to SouthEast + private static readonly ushort[] ForestBorderNTiles = [0x02F1]; // Water to North + private static readonly ushort[] ForestBorderSTiles = [0x02F2]; // Water to South + private static readonly ushort[] ForestBorderETiles = [0x02F3]; // Water to East + + // Corner land tile (0x0095) - used for SW, NE, and E_S_BETWEEN corners + // Placed as land tile at offset position (x-1, y+1) - color already at offset in biome map + private static readonly ushort[] CornerTiles = [0x0095]; + + // =================================================================================== + // STRUCTURE STATICS (WALLS) - Complete list + // =================================================================================== + + // Stone walls + private static readonly ushort[] StoneWallStatics = [ + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, + 0x00C5, 0x00C6, 0x00C7, 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x00D0, + 0x01CF, 0x01D0, 0x01D1, 0x01D2, 0x01D3, 0x01D4, 0x01D5, 0x01D6, 0x01D7, 0x01D8, 0x01D9, 0x01DA, 0x01DB, + 0x02CE, 0x02CF, 0x02D0, 0x02D1, 0x02D2, 0x02D3, 0x02D4, 0x02D5, 0x02D6, 0x02D7, 0x02D8, 0x02D9, + 0x0324, 0x0325, 0x0326, 0x0327, 0x0328, 0x0329, 0x032A, 0x032B, 0x032C, 0x032D, 0x032E, 0x032F, 0x0330, 0x0331, 0x0332, + 0x0354, 0x0355, 0x0356, 0x0357, 0x0358, 0x0359, 0x035A, 0x035B, 0x035C, 0x035D, 0x035E, 0x035F, 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, + 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03CC, 0x03CD, 0x03CE, 0x03CF, 0x03D0, 0x03D1, 0x03D2, 0x03D3, 0x03D4, 0x03D5, 0x03D6, 0x03D7, 0x03D8, 0x03D9, 0x03DA, 0x03DB, 0x03DC, 0x03DD, 0x03DE, 0x03DF, 0x03E0, 0x03E1 + ]; + + // Marble walls + private static readonly ushort[] MarbleWallStatics = [ + 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF, 0x0100, 0x0101, 0x0102, 0x0103, 0x0104, 0x0105, 0x0106, 0x0107, + 0x0108, 0x0109, 0x010A, 0x010B, 0x010C, 0x010D, 0x010E, 0x010F, 0x0110, 0x0111, 0x0112, 0x0113, 0x0114, 0x0115, 0x0116, 0x0117, + 0x0118, 0x0119, 0x011A, 0x011B, 0x011C, 0x011D, 0x011E, 0x011F, 0x0120, 0x0121, 0x0122, 0x0123, 0x0124, 0x0125, 0x0126, + 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297, 0x0298, 0x0299, 0x029A, 0x029B, 0x029C, 0x029D, 0x029E, 0x029F, + 0x02A0, 0x02A1, 0x02A2, 0x02A3, 0x02A4, 0x02A5, 0x02A6, 0x02A7, 0x02A8, 0x02A9, 0x02AA, 0x02AB, 0x02AC, 0x02AD, 0x02AE, 0x02AF, + 0x02B0, 0x02B1, 0x02B2, 0x02B3, 0x02B4, 0x02B5, 0x02B6, 0x02B7, 0x02B8, 0x02B9, 0x02BA, 0x02BB + ]; + + // Sandstone walls + private static readonly ushort[] SandstoneWallStatics = [ + 0x0158, 0x0159, 0x015A, 0x015B, 0x015C, 0x015D, 0x015E, 0x015F, 0x0160, 0x0161, 0x0162, 0x0163, 0x0164, 0x0165, 0x0166, 0x0167, + 0x0168, 0x0169, 0x016A, 0x016B, 0x016C, 0x016D, 0x016E, 0x016F, 0x0170, 0x0171, 0x0172, 0x0173, 0x0174, 0x0175, 0x0176, 0x0177, + 0x0178, 0x0179, 0x017A, 0x017B, 0x017C, 0x017D, 0x017E, 0x017F, 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187, + 0x0188, 0x0189, 0x018A, 0x018B, 0x018C, 0x018D, 0x018E, 0x018F, 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197, + 0x0198, 0x0199, 0x019A + ]; + + // Dungeon walls + private static readonly ushort[] DungeonWallStatics = [ + 0x0241, 0x0242, 0x0243, 0x0244, + 0x02F9, 0x02FA, 0x02FB, 0x02FC, 0x02FD, 0x02FE, 0x02FF, 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307, + 0x0308, 0x0309, 0x030A, 0x030B, 0x030C, 0x030E, 0x0310, 0x0311, 0x0314, 0x0315, 0x0316, 0x0317, + 0x0318, 0x0319, 0x031A, 0x031B, 0x031C, 0x031D, 0x031E, 0x031F, 0x0320, 0x0321, 0x0322, + 0x21DD, 0x21DE, 0x21DF, 0x21E0, 0x21E1, 0x21E2, 0x21E3, 0x21E4, 0x21E5, 0x21E6, 0x21E7, 0x21E8 + ]; + + // Legacy aliases + private static readonly ushort[] WallStatics = StoneWallStatics; + private static readonly ushort[] PillarStatics = DungeonWallStatics; + private static readonly ushort[] FenceStatics = [0x0085, 0x0086, 0x0087, 0x0088]; + + // Static object IDs for vegetation + private static readonly ushort[] TreeStatics = [0x0CCA, 0x0CCB, 0x0CCC, 0x0CCD, 0x0CCE, 0x0CD0, 0x0CD3, 0x0CD6, 0x0CD8, 0x0CDA]; + private static readonly ushort[] BushStatics = [0x0C8E, 0x0C8F, 0x0C90, 0x0C91, 0x0C92, 0x0C93, 0x0C94, 0x0C95]; + private static readonly ushort[] GrassStatics = [0x0C45, 0x0C46, 0x0C47, 0x0C48, 0x0C49, 0x0C4A, 0x0C4B]; + private static readonly ushort[] FlowerStatics = [0x0C83, 0x0C84, 0x0C85, 0x0C86, 0x0C87, 0x0C88, 0x0C89]; + private static readonly ushort[] CropStatics = [0x0C55, 0x0C56, 0x0C57, 0x0C58, 0x0C59, 0x0C5A]; +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.TilesBrush.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.TilesBrush.cs new file mode 100644 index 00000000..ff020bc5 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.TilesBrush.cs @@ -0,0 +1,233 @@ +using System.Xml.Linq; + +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Partial class containing TilesBrush XML parsing and transition logic. +/// +public partial class ImportColoredHeightmap +{ + /// + /// Data structure for a TilesBrush definition. + /// + private class TilesBrushData + { + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public List<(ushort TileId, float Chance)> LandTiles { get; set; } = new(); + public Dictionary Edges { get; set; } = new(); + } + + /// + /// Edge transition data for TilesBrush. + /// + private class TilesBrushEdge + { + public string TargetBrushId { get; set; } = ""; + public List UL { get; set; } = new(); // Upper-left corner + public List UR { get; set; } = new(); // Upper-right corner + public List DL { get; set; } = new(); // Down-left corner + public List DR { get; set; } = new(); // Down-right corner + public List UU { get; set; } = new(); // Upper edge (horizontal) + public List LL { get; set; } = new(); // Left edge (vertical) + } + + /// + /// Load and parse TilesBrush.xml file. + /// + private bool LoadTilesBrush(string filePath) + { + try + { + var doc = XDocument.Load(filePath); + var root = doc.Root; + if (root == null || root.Name != "TilesBrush") + { + Console.WriteLine("Invalid TilesBrush.xml: Root element must be 'TilesBrush'"); + return false; + } + + _tilesBrushes = new Dictionary(); + + foreach (var brushElement in root.Elements("Brush")) + { + var brush = new TilesBrushData + { + Id = brushElement.Attribute("Id")?.Value ?? "", + Name = brushElement.Attribute("Name")?.Value ?? "" + }; + + // Parse land tiles + foreach (var landElement in brushElement.Elements("Land").Where(e => e.Attribute("Type") == null)) + { + var idStr = landElement.Attribute("ID")?.Value; + if (idStr != null) + { + var tileId = ParseHexOrDecimal(idStr); + var chanceStr = landElement.Attribute("Chance")?.Value; + float chance = 1.0f; + if (chanceStr != null) + { + chanceStr = chanceStr.Replace(',', '.'); + float.TryParse(chanceStr, System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out chance); + } + brush.LandTiles.Add((tileId, chance)); + } + } + + // Parse edge definitions + foreach (var edgeElement in brushElement.Elements("Edge")) + { + var targetId = edgeElement.Attribute("To")?.Value ?? ""; + var edge = new TilesBrushEdge { TargetBrushId = targetId }; + + foreach (var landElement in edgeElement.Elements("Land")) + { + var typeStr = landElement.Attribute("Type")?.Value; + var idStr = landElement.Attribute("ID")?.Value; + if (typeStr != null && idStr != null) + { + var tileId = ParseHexOrDecimal(idStr); + switch (typeStr) + { + case "UL": edge.UL.Add(tileId); break; + case "UR": edge.UR.Add(tileId); break; + case "DL": edge.DL.Add(tileId); break; + case "DR": edge.DR.Add(tileId); break; + case "UU": edge.UU.Add(tileId); break; + case "LL": edge.LL.Add(tileId); break; + } + } + } + + brush.Edges[targetId] = edge; + } + + _tilesBrushes[brush.Id] = brush; + Console.WriteLine($"Loaded brush: {brush.Id} ({brush.Name}) with {brush.LandTiles.Count} tiles and {brush.Edges.Count} edges"); + } + + Console.WriteLine($"TilesBrush loaded: {_tilesBrushes.Count} brushes"); + return true; + } + catch (Exception e) + { + Console.WriteLine($"Error loading TilesBrush.xml: {e.Message}"); + _tilesBrushes = null; + return false; + } + } + + private static ushort ParseHexOrDecimal(string value) + { + if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return Convert.ToUInt16(value[2..], 16); + } + return ushort.Parse(value); + } + + /// + /// Map biome enum to TilesBrush ID. + /// + private string GetBrushIdForBiome(Biome biome) + { + return biome switch + { + Biome.Grass => "0203", + Biome.Dirt => "0002", + Biome.Sand => "0217", + Biome.Forest => "0204", + Biome.Jungle => "0202", + Biome.Snow => "0210", + Biome.Rock => "0209", // Mountain + Biome.Swamp => "0240", // Swamp Water + Biome.Cobblestone => "0198", + Biome.Brick => "0199", + Biome.Cave => "0215", + _ => "0203" // Default to grass + }; + } + + /// + /// Get transition tile using TilesBrush data. + /// + private ushort? GetTilesBrushTransition(int px, int py, Biome centerBiome) + { + if (_tilesBrushes == null || _biomeCache == null) + return null; + + var brushId = GetBrushIdForBiome(centerBiome); + if (!_tilesBrushes.TryGetValue(brushId, out var brush)) + return null; + + int width = _biomeCache.GetLength(0); + int height = _biomeCache.GetLength(1); + + Biome GetBiomeAt(int x, int y) + { + if (x < 0 || x >= width || y < 0 || y >= height) + return centerBiome; + return _biomeCache[x, y]; + } + + // Get neighboring biomes - standard UO coordinate system (matches LandBrush Direction.Offset) + // North = (0, -1), South = (0, +1), East = (+1, 0), West = (-1, 0) + var n = GetBiomeAt(px, py - 1); // North + var s = GetBiomeAt(px, py + 1); // South + var e = GetBiomeAt(px + 1, py); // East + var w = GetBiomeAt(px - 1, py); // West + + // Check for edges with different biomes + var differentNeighbors = new HashSet(); + if (n != centerBiome) differentNeighbors.Add(n); + if (s != centerBiome) differentNeighbors.Add(s); + if (e != centerBiome) differentNeighbors.Add(e); + if (w != centerBiome) differentNeighbors.Add(w); + + if (differentNeighbors.Count == 0) + return null; + + // Find an edge that matches one of our neighbors + foreach (var neighborBiome in differentNeighbors) + { + var neighborBrushId = GetBrushIdForBiome(neighborBiome); + if (!brush.Edges.TryGetValue(neighborBrushId, out var edge)) + continue; + + bool hasN = n == neighborBiome; + bool hasS = s == neighborBiome; + bool hasE = e == neighborBiome; + bool hasW = w == neighborBiome; + + List? tiles = null; + + // Corner cases (two adjacent cardinal neighbors) + if (hasN && hasW && !hasS && !hasE && edge.UL.Count > 0) + tiles = edge.UL; + else if (hasN && hasE && !hasS && !hasW && edge.UR.Count > 0) + tiles = edge.UR; + else if (hasS && hasW && !hasN && !hasE && edge.DL.Count > 0) + tiles = edge.DL; + else if (hasS && hasE && !hasN && !hasW && edge.DR.Count > 0) + tiles = edge.DR; + // Edge cases (one cardinal neighbor) + else if (hasN && !hasS && !hasE && !hasW && edge.UU.Count > 0) + tiles = edge.UU; + else if (hasS && !hasN && !hasE && !hasW && edge.UU.Count > 0) + tiles = edge.UU; + else if (hasW && !hasN && !hasS && !hasE && edge.LL.Count > 0) + tiles = edge.LL; + else if (hasE && !hasN && !hasS && !hasW && edge.LL.Count > 0) + tiles = edge.LL; + + if (tiles != null && tiles.Count > 0) + { + return tiles[_random.Next(tiles.Count)]; + } + } + + return null; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Transitions.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Transitions.cs new file mode 100644 index 00000000..cca7f266 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Transitions.cs @@ -0,0 +1,637 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Partial class containing biome transition tile definitions and logic. +/// +public partial class ImportColoredHeightmap +{ + // Transition tiles between biomes + // Format for each: [outer corners (4), edges (4), inner corners (4)] + // Outer corners: NW, NE, SW, SE - where center tile has target biome in 2 adjacent cardinal directions + // Edges: N, E, S, W - where center tile has target biome in 1 cardinal direction + // Inner corners: NW, NE, SW, SE - where center tile has target biome only diagonally + + // Sand -> Grass transitions (from MapCreator: tiles 51-62 decimal = 0x0033-0x003E) + private static readonly ushort[] SandToGrassTransitions = [ + 0x0033, 0x0034, 0x0035, 0x0036, // Outer corners: UL, UR, DL, DR + 0x0037, 0x0038, 0x0039, 0x003A, // Edges: N, S, E, W + 0x003B, 0x003C, 0x003D, 0x003E // Inner corners: UL, UR, DL, DR + ]; + + // Grass -> Sand transitions (from MapCreator: tiles 51-62 decimal = 0x0033-0x003E) + private static readonly ushort[] GrassToSandTransitions = [ + 0x0033, 0x0034, 0x0035, 0x0036, // Outer corners: UL, UR, DL, DR + 0x0037, 0x0038, 0x0039, 0x003A, // Edges: N, S, E, W + 0x003B, 0x003C, 0x003D, 0x003E // Inner corners: UL, UR, DL, DR + ]; + + // Grass -> Dirt transitions (TilesBrush.xml: Grass Edge To Dirt) + // Order matches Dragon/AAAABBBB convention: corners NW, NE, SW, SE; edges N, S, W, E. + // UL/NW=0x007D, UR/NE=0x0082, DL/SW=0x0083, DR/SE=0x007E, LL/W=0x0089/0x008A, UU/N=0x008B/0x008C + private static readonly ushort[] GrassToDirtTransitions = [ + 0x007D, 0x0082, 0x0083, 0x007E, // Corners: NW, NE, SW, SE + 0x008B, 0x008C, 0x0089, 0x008A, // Edges: N, S, W, E + 0x007D, 0x0082, 0x0083, 0x007E // Inner corners: NW, NE, SW, SE + ]; + + // Dirt -> Grass transitions (TilesBrush.xml: Dirt Edge To Grass) + // Order matches Dragon/AAAABBBB convention: corners NW, NE, SW, SE; edges N, S, W, E. + // UL/NW=0x0079, UR/NE=0x007B, DL/SW=0x007C, DR/SE=0x007A, LL/W=0x0087/0x0088, UU/N=0x0085/0x0086 + private static readonly ushort[] DirtToGrassTransitions = [ + 0x0079, 0x007B, 0x007C, 0x007A, // Corners: NW, NE, SW, SE + 0x0085, 0x0086, 0x0087, 0x0088, // Edges: N, S, W, E + 0x0079, 0x007B, 0x007C, 0x007A // Inner corners: NW, NE, SW, SE + ]; + + // Snow -> Dirt transitions (from TilesBrush.xml) + // TilesBrush: UL=0x038A, UR=0x038C, DL=0x038B, DR=0x0389, UU=0x0385, LL=0x0386 + private static readonly ushort[] SnowToDirtTransitions = [ + 0x038A, 0x038C, 0x038B, 0x0389, // UL, UR, DL, DR corners + 0x0385, 0x0385, 0x0386, 0x0386, // UU, UU, LL, LL edges + 0x038A, 0x038C, 0x038B, 0x0389 // UL, UR, DL, DR inner corners + ]; + + // Dirt -> Snow transitions (from TilesBrush.xml) + // TilesBrush: UL=0x038A, UR=0x038C, DL=0x038B, DR=0x0389, UU=0x0385, LL=0x0386 + private static readonly ushort[] DirtToSnowTransitions = [ + 0x038A, 0x038C, 0x038B, 0x0389, // UL, UR, DL, DR corners + 0x0385, 0x0385, 0x0386, 0x0386, // UU, UU, LL, LL edges + 0x038A, 0x038C, 0x038B, 0x0389 // UL, UR, DL, DR inner corners + ]; + + // Snow -> Grass transitions (from TilesBrush.xml) + // TilesBrush: UL=0x05C8, UR=0x05CA, DL=0x05C9, DR=0x05C7, UU=0x05C2, LL=0x05C1 + private static readonly ushort[] SnowToGrassTransitions = [ + 0x05C8, 0x05CA, 0x05C9, 0x05C7, // UL, UR, DL, DR corners + 0x05C2, 0x05C2, 0x05C1, 0x05C1, // UU, UU, LL, LL edges + 0x05C8, 0x05CA, 0x05C9, 0x05C7 // UL, UR, DL, DR inner corners + ]; + + // Grass -> Snow transitions (from TilesBrush.xml) + // TilesBrush: UL=0x05C8, UR=0x05CA, DL=0x05C9, DR=0x05C7, UU=0x05C2, LL=0x05C1 + private static readonly ushort[] GrassToSnowTransitions = [ + 0x05C8, 0x05CA, 0x05C9, 0x05C7, // UL, UR, DL, DR corners + 0x05C2, 0x05C2, 0x05C1, 0x05C1, // UU, UU, LL, LL edges + 0x05C8, 0x05CA, 0x05C9, 0x05C7 // UL, UR, DL, DR inner corners + ]; + + + // Mountain/Rock transitions (from TilesBrush.xml) + // Rock → Grass: UL=0x0235, UR=0x0236, DL=0x0238, DR=0x0237, UU=0x023B, LL=0x023A + private static readonly ushort[] RockToGrassTransitions = [ + 0x0235, 0x0236, 0x0238, 0x0237, // UL, UR, DL, DR corners + 0x023B, 0x023B, 0x023A, 0x023A, // UU, UU, LL, LL edges + 0x0235, 0x0236, 0x0238, 0x0237 // UL, UR, DL, DR inner corners + ]; + + // Grass -> Rock: UL=0x0239, UR=0x023B, DL=0x023A, DR=0x0238, UU=0x0241, LL=0x0240 + private static readonly ushort[] GrassToRockTransitions = [ + 0x0239, 0x023B, 0x023A, 0x0238, // UL, UR, DL, DR corners + 0x0241, 0x0241, 0x0240, 0x0240, // UU, UU, LL, LL edges + 0x0239, 0x023B, 0x023A, 0x0238 // UL, UR, DL, DR inner corners + ]; + + // Rock → Dirt: UL=0x00E4, UR=0x00E5, DL=0x00E7, DR=0x00E6, UU=0x00DF, LL=0x00DE + private static readonly ushort[] RockToDirtTransitions = [ + 0x00E4, 0x00E5, 0x00E7, 0x00E6, // UL, UR, DL, DR corners + 0x00DF, 0x00DF, 0x00DE, 0x00DE, // UU, UU, LL, LL edges + 0x00E4, 0x00E5, 0x00E7, 0x00E6 // UL, UR, DL, DR inner corners + ]; + + // Dirt → Rock: UL=0x00E8, UR=0x00E9, DL=0x00EB, DR=0x00EA, UU=0x00F0, LL=0x00EF + private static readonly ushort[] DirtToRockTransitions = [ + 0x00E8, 0x00E9, 0x00EB, 0x00EA, // UL, UR, DL, DR corners + 0x00F0, 0x00F0, 0x00EF, 0x00EF, // UU, UU, LL, LL edges + 0x00E8, 0x00E9, 0x00EB, 0x00EA // UL, UR, DL, DR inner corners + ]; + + // Rock → Snow: UL=0x0110, UR=0x0111, DL=0x0113, DR=0x0112, UU=0x0116, LL=0x0115 + private static readonly ushort[] RockToSnowTransitions = [ + 0x0110, 0x0111, 0x0113, 0x0112, // UL, UR, DL, DR corners + 0x0116, 0x0116, 0x0115, 0x0115, // UU, UU, LL, LL edges + 0x0110, 0x0111, 0x0113, 0x0112 // UL, UR, DL, DR inner corners + ]; + + // Snow → Rock: UL=0x0114, UR=0x0115, DL=0x0117, DR=0x0116, UU=0x011B, LL=0x011A + private static readonly ushort[] SnowToRockTransitions = [ + 0x0114, 0x0115, 0x0117, 0x0116, // UL, UR, DL, DR corners + 0x011B, 0x011B, 0x011A, 0x011A, // UU, UU, LL, LL edges + 0x0114, 0x0115, 0x0117, 0x0116 // UL, UR, DL, DR inner corners + ]; + + // Rock → Sand: UL=0x0122, UR=0x0123, DL=0x0125, DR=0x0124, UU=0x0129, LL=0x0128 + private static readonly ushort[] RockToSandTransitions = [ + 0x0122, 0x0123, 0x0125, 0x0124, // UL, UR, DL, DR corners + 0x0129, 0x0129, 0x0128, 0x0128, // UU, UU, LL, LL edges + 0x0122, 0x0123, 0x0125, 0x0124 // UL, UR, DL, DR inner corners + ]; + + // Sand → Rock: UL=0x0126, UR=0x0127, DL=0x0129, DR=0x0128, UU=0x012D, LL=0x012C + private static readonly ushort[] SandToRockTransitions = [ + 0x0126, 0x0127, 0x0129, 0x0128, // UL, UR, DL, DR corners + 0x012D, 0x012D, 0x012C, 0x012C, // UU, UU, LL, LL edges + 0x0126, 0x0127, 0x0129, 0x0128 // UL, UR, DL, DR inner corners + ]; + + // Rock → Forest: UL=0x00F4, UR=0x00F5, DL=0x00F7, DR=0x00F6, UU=0x00EC, LL=0x00EF + private static readonly ushort[] RockToForestTransitions = [ + 0x00F4, 0x00F5, 0x00F7, 0x00F6, // UL, UR, DL, DR corners + 0x00EC, 0x00EC, 0x00EF, 0x00EF, // UU, UU, LL, LL edges + 0x00F4, 0x00F5, 0x00F7, 0x00F6 // UL, UR, DL, DR inner corners + ]; + + // Forest → Rock: UL=0x00F8, UR=0x00F9, DL=0x00FB, DR=0x00FA, UU=0x0100, LL=0x00FF + private static readonly ushort[] ForestToRockTransitions = [ + 0x00F8, 0x00F9, 0x00FB, 0x00FA, // UL, UR, DL, DR corners + 0x0100, 0x0100, 0x00FF, 0x00FF, // UU, UU, LL, LL edges + 0x00F8, 0x00F9, 0x00FB, 0x00FA // UL, UR, DL, DR inner corners + ]; + + // Grass → Cobblestone (from TilesBrush.xml: Grass Edge To Cobblestone) + // DR=0x0681, DL=0x0683, UL=0x0682, UR=0x0684, LL=0x067E, UU=0x067D + private static readonly ushort[] GrassToCobblestoneTransitions = [ + 0x0682, 0x0684, 0x0683, 0x0681, // UL, UR, DL, DR outer corners + 0x067D, 0x067D, 0x067E, 0x067E, // UU, UU, LL, LL edges + 0x0682, 0x0684, 0x0683, 0x0681 // UL, UR, DL, DR inner corners + ]; + + + + // Cobblestone → Grass (from TilesBrush.xml: Cobblestone Edge To Grass) + // DR=0x0685, DL=0x0687, UL=0x0686, UR=0x0688, LL=0x067F, UU=0x0680 + private static readonly ushort[] CobblestoneToGrassTransitions = [ + 0x0686, 0x0688, 0x0687, 0x0685, // UL, UR, DL, DR outer corners + 0x0680, 0x0680, 0x067F, 0x067F, // UU, UU, LL, LL edges + 0x0686, 0x0688, 0x0687, 0x0685 // UL, UR, DL, DR inner corners + ]; + + // Dirt → Cobblestone (from TilesBrush.xml: Dirt Edge To Large Cobblestones 0198) + // DR=0x0403, DL=0x0404, UL=0x0405, UR=0x0402, LL=0x0400, UU=0x0401 + private static readonly ushort[] DirtToCobblestoneTransitions = [ + 0x0405, 0x0402, 0x0404, 0x0403, // UL, UR, DL, DR corners + 0x0401, 0x0401, 0x0400, 0x0400, // UU, UU, LL, LL edges + 0x0405, 0x0402, 0x0404, 0x0403 // UL, UR, DL, DR inner corners + ]; + + // Cobblestone → Dirt (from TilesBrush.xml: Large Cobblestones 0198 Edge To Dirt) + // DR=0x0406, DL=0x0407, UL=0x0408, UR=0x0409, LL=0x040B, UU=0x040A + private static readonly ushort[] CobblestoneToDirtTransitions = [ + 0x0408, 0x0409, 0x0407, 0x0406, // UL, UR, DL, DR corners + 0x040A, 0x040A, 0x040B, 0x040B, // UU, UU, LL, LL edges + 0x0408, 0x0409, 0x0407, 0x0406 // UL, UR, DL, DR inner corners + ]; + + /// + /// Calculate transition tile for a border tile (called during pre-processing). + /// + private ushort CalculateTransitionTile(int px, int py, Biome centerBiome, int width, int height) + { + if (_biomeCache == null) + return 0; + + Biome GetBiomeAt(int x, int y) + { + if (x < 0 || x >= width || y < 0 || y >= height) + return centerBiome; + return _biomeCache[x, y]; + } + + // Get neighbors - using standard UO coordinate system (matches LandBrush Direction.Offset) + // North = (0, -1), South = (0, +1), East = (+1, 0), West = (-1, 0) + var n = GetBiomeAt(px, py - 1); // North + var s = GetBiomeAt(px, py + 1); // South + var e = GetBiomeAt(px + 1, py); // East + var w = GetBiomeAt(px - 1, py); // West + var ne = GetBiomeAt(px + 1, py - 1); // NorthEast + var se = GetBiomeAt(px + 1, py + 1); // SouthEast + var sw = GetBiomeAt(px - 1, py + 1); // SouthWest + var nw = GetBiomeAt(px - 1, py - 1); // NorthWest + + // Try each transition type + var result = TryGetTransition(centerBiome, Biome.Sand, Biome.Grass, SandToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Sand, GrassToSandTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Dirt, GrassToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Grass, DirtToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Cobblestone, GrassToCobblestoneTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Cobblestone, Biome.Grass, CobblestoneToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Cobblestone, DirtToCobblestoneTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Cobblestone, Biome.Dirt, CobblestoneToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Dirt, SnowToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Snow, DirtToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Grass, SnowToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Snow, GrassToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Grass, RockToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Rock, GrassToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Dirt, RockToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Rock, DirtToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Snow, RockToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Rock, SnowToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Sand, RockToSandTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Sand, Biome.Rock, SandToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Forest, RockToForestTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Forest, Biome.Rock, ForestToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + // No transition: if TilesBrush land tiles are available and enabled, use one of them. + if (_useTilesBrush && _tilesBrushes != null) + { + var brushId = GetBrushIdForBiome(centerBiome); + if (_tilesBrushes.TryGetValue(brushId, out var brush) && brush.LandTiles.Count > 0) + { + var totalChance = brush.LandTiles.Sum(t => t.Chance); + var roll = (float)(_random.NextDouble() * totalChance); + float cumulative = 0; + foreach (var (tileId, chance) in brush.LandTiles) + { + cumulative += chance; + if (roll <= cumulative) + return tileId; + } + return brush.LandTiles[0].TileId; + } + } + + return GetLandTileForBiome(centerBiome); + } + + /// + /// Calculate transition tile only for owned neighbor biomes. + /// This ensures unidirectional transitions - only the tile that "owns" the border gets transition tiles. + /// + private ushort CalculateTransitionTileForOwned(int px, int py, Biome centerBiome, HashSet ownedNeighbors, int width, int height) + { + if (_biomeCache == null) + return 0; + + Biome GetBiomeAt(int x, int y) + { + if (x < 0 || x >= width || y < 0 || y >= height) + return centerBiome; + return _biomeCache[x, y]; + } + + // Get neighbors - using standard UO coordinate system (matches LandBrush Direction.Offset) + // North = (0, -1), South = (0, +1), East = (+1, 0), West = (-1, 0) + var n = GetBiomeAt(px, py - 1); // North + var s = GetBiomeAt(px, py + 1); // South + var e = GetBiomeAt(px + 1, py); // East + var w = GetBiomeAt(px - 1, py); // West + var ne = GetBiomeAt(px + 1, py - 1); // NorthEast + var se = GetBiomeAt(px + 1, py + 1); // SouthEast + var sw = GetBiomeAt(px - 1, py + 1); // SouthWest + var nw = GetBiomeAt(px - 1, py - 1); // NorthWest + + // Mask neighbors - only consider biomes we "own" + n = ownedNeighbors.Contains(n) ? n : centerBiome; + s = ownedNeighbors.Contains(s) ? s : centerBiome; + e = ownedNeighbors.Contains(e) ? e : centerBiome; + w = ownedNeighbors.Contains(w) ? w : centerBiome; + ne = ownedNeighbors.Contains(ne) ? ne : centerBiome; + se = ownedNeighbors.Contains(se) ? se : centerBiome; + sw = ownedNeighbors.Contains(sw) ? sw : centerBiome; + nw = ownedNeighbors.Contains(nw) ? nw : centerBiome; + + // Try each transition type + var result = TryGetTransition(centerBiome, Biome.Sand, Biome.Grass, SandToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Sand, GrassToSandTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Dirt, GrassToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Grass, DirtToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Cobblestone, GrassToCobblestoneTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Cobblestone, Biome.Grass, CobblestoneToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Cobblestone, DirtToCobblestoneTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Cobblestone, Biome.Dirt, CobblestoneToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Dirt, SnowToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Snow, DirtToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Grass, SnowToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Snow, GrassToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Grass, RockToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Rock, GrassToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Dirt, RockToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Rock, DirtToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Snow, RockToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Rock, SnowToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Sand, RockToSandTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Sand, Biome.Rock, SandToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Forest, RockToForestTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Forest, Biome.Rock, ForestToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + return 0; // No transition + } + + /// + /// Gets transition tile based on neighboring biomes. + /// Analyzes the 3x3 neighborhood to determine if this is an edge tile. + /// + private ushort GetTransitionTile(int px, int py, Biome centerBiome) + { + if (_biomeCache == null) + return GetLandTileForBiome(centerBiome); + + // Water, Lava, Void don't use TilesBrush - return directly + if (centerBiome == Biome.Water || centerBiome == Biome.Lava || centerBiome == Biome.Void) + { + return GetLandTileForBiome(centerBiome); + } + + // Try TilesBrush transitions first if enabled and loaded + if (_useTilesBrush && _tilesBrushes != null) + { + var brushResult = GetTilesBrushTransition(px, py, centerBiome); + if (brushResult.HasValue) + return brushResult.Value; + // If no TilesBrush transition matched, fall through to hardcoded transitions below. + } + + int width = _biomeCache.GetLength(0); + int height = _biomeCache.GetLength(1); + + // Get neighboring biomes (N, E, S, W, NE, SE, SW, NW) + Biome GetBiomeAt(int x, int y) + { + if (x < 0 || x >= width || y < 0 || y >= height) + return centerBiome; + return _biomeCache[x, y]; + } + + // Standard UO coordinate system (matches LandBrush Direction.Offset) + var n = GetBiomeAt(px, py - 1); // North + var s = GetBiomeAt(px, py + 1); // South + var e = GetBiomeAt(px + 1, py); // East + var w = GetBiomeAt(px - 1, py); // West + var ne = GetBiomeAt(px + 1, py - 1); // NorthEast + var se = GetBiomeAt(px + 1, py + 1); // SouthEast + var sw = GetBiomeAt(px - 1, py + 1); // SouthWest + var nw = GetBiomeAt(px - 1, py - 1); // NorthWest + + // Try each transition (fallback hardcoded transitions) + var result = TryGetTransition(centerBiome, Biome.Sand, Biome.Grass, SandToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Sand, GrassToSandTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Dirt, GrassToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Grass, DirtToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Dirt, SnowToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Snow, DirtToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Grass, SnowToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Snow, GrassToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + // Rock/Mountain transitions + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Grass, RockToGrassTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Grass, Biome.Rock, GrassToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Dirt, RockToDirtTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Dirt, Biome.Rock, DirtToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Snow, RockToSnowTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Snow, Biome.Rock, SnowToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Sand, RockToSandTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Sand, Biome.Rock, SandToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Rock, Biome.Forest, RockToForestTransitions, null, + n, s, e, w, ne, se, sw, nw); + if (result.HasValue) return result.Value; + + result = TryGetTransition(centerBiome, Biome.Forest, Biome.Rock, ForestToRockTransitions, null, + n, s, e, w, ne, se, sw, nw); ; + if (result.HasValue) return result.Value; + + // No transition needed, return normal tile + return GetLandTileForBiome(centerBiome); + } + + /// + /// Tries to find a transition tile for a biome pair. + /// IMPORTANT: Only biomeA (the "top" biome) gets transition tiles! + /// biomeB (the "bottom" biome) stays as normal tile. + /// + private ushort? TryGetTransition( + Biome centerBiome, Biome biomeA, Biome biomeB, + ushort[] transitions, ushort[]? unused, + Biome n, Biome s, Biome e, Biome w, + Biome ne, Biome se, Biome sw, Biome nw) + { + // ONLY biomeA gets transition tiles when adjacent to biomeB + if (centerBiome != biomeA) + { + return null; + } + + Biome targetBiome = biomeB; + + // Note: In the code, n/s/e/w are named based on inverted isometric coords: + // n = py+1 (actually South in standard coords) + // s = py-1 (actually North in standard coords) + // e = px-1 (actually West in standard coords) + // w = px+1 (actually East in standard coords) + bool hasTargetN = n == targetBiome; + bool hasTargetS = s == targetBiome; + bool hasTargetE = e == targetBiome; + bool hasTargetW = w == targetBiome; + + // Using same logic as GetTilesBrushTransition (which matches TilesBrush.xml) + // Array: [0]=UL, [1]=UR, [2]=DL, [3]=DR, [4-5]=UU, [6-7]=LL, [8-11]=inner corners + + // OUTER corners - two adjacent cardinal neighbors (same as TilesBrush.xml) + if (hasTargetN && hasTargetW && !hasTargetS && !hasTargetE) + return transitions[0]; // N+W → UL + if (hasTargetN && hasTargetE && !hasTargetS && !hasTargetW) + return transitions[1]; // N+E → UR + if (hasTargetS && hasTargetW && !hasTargetN && !hasTargetE) + return transitions[2]; // S+W → DL + if (hasTargetS && hasTargetE && !hasTargetN && !hasTargetW) + return transitions[3]; // S+E → DR + + // Edge transitions - single cardinal neighbor (same as TilesBrush.xml) + if (hasTargetN && !hasTargetS && !hasTargetE && !hasTargetW) + return transitions[4]; // N → UU + if (hasTargetS && !hasTargetN && !hasTargetE && !hasTargetW) + return transitions[5]; // S → UU + if (hasTargetW && !hasTargetN && !hasTargetS && !hasTargetE) + return transitions[6]; // W → LL + if (hasTargetE && !hasTargetN && !hasTargetS && !hasTargetW) + return transitions[7]; // E → LL + + // INNER corners - diagonal neighbor only (no adjacent cardinals) + if (nw == targetBiome && !hasTargetN && !hasTargetW) + return transitions[8]; // NW → UL inner + if (ne == targetBiome && !hasTargetN && !hasTargetE) + return transitions[9]; // NE → UR inner + if (sw == targetBiome && !hasTargetS && !hasTargetW) + return transitions[10]; // SW → DL inner + if (se == targetBiome && !hasTargetS && !hasTargetE) + return transitions[11]; // SE → DR inner + + return null; + } +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Vegetation.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Vegetation.cs new file mode 100644 index 00000000..e5b8d5a3 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.Vegetation.cs @@ -0,0 +1,204 @@ +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Partial class containing vegetation and structure classification logic. +/// +public partial class ImportColoredHeightmap +{ + #region Vegetation Classification + + private enum VegetationType + { + None, Tree, Bush, Grass, Flower, Crop, Coastline + } + + /// + /// Coastline direction for directional static placement. + /// + private enum CoastlineDirection + { + None = 0, + W = 1, // West coast + S = 2, // South coast + N = 3, // North coast + E = 4, // East coast + SW = 5, // Southwest corner + SE = 6, // Southeast corner + NW = 7, // Northwest corner + NE = 8 // Northeast corner + } + + /// + /// Coastline statics by direction from CoastlineTool. + /// + private static readonly Dictionary CoastlineStaticsByDirection = new() + { + { CoastlineDirection.W, [0x179D, 0x179E] }, + { CoastlineDirection.S, [0x179F, 0x17A0] }, + { CoastlineDirection.N, [0x17A1, 0x17A2] }, + { CoastlineDirection.E, [0x17A3, 0x17A4] }, + { CoastlineDirection.SW, [0x17A5, 0x17A9] }, + { CoastlineDirection.SE, [0x17A6, 0x17AC] }, + { CoastlineDirection.NW, [0x17A7, 0x17AA] }, + { CoastlineDirection.NE, [0x17A8, 0x17AB] }, + }; + + /// + /// Coastline color pattern from UOMapMorph: R=200, G=100, B=direction. + /// + private const int CoastlineR = 200; + private const int CoastlineG = 100; + + private static VegetationType ClassifyVegetationColor(byte r, byte g, byte b) + { + // Black = no vegetation + if (r < 20 && g < 20 && b < 20) + return VegetationType.None; + + // Check for coastline color pattern (R=200, G=100) first - takes priority + if (Math.Abs(r - CoastlineR) <= 10 && Math.Abs(g - CoastlineG) <= 10) + return VegetationType.Coastline; + + var vegColors = new (VegetationType type, int r, int g, int b)[] + { + (VegetationType.Tree, 34, 85, 34), + (VegetationType.Bush, 50, 120, 50), + (VegetationType.Grass, 80, 160, 80), + (VegetationType.Flower, 200, 100, 150), + (VegetationType.Crop, 180, 160, 60), + }; + + VegetationType bestMatch = VegetationType.None; + double bestDistance = 50; + + foreach (var (type, vr, vg, vb) in vegColors) + { + var distance = Math.Sqrt( + Math.Pow(r - vr, 2) + + Math.Pow(g - vg, 2) + + Math.Pow(b - vb, 2) + ); + + if (distance < bestDistance) + { + bestDistance = distance; + bestMatch = type; + } + } + + return bestMatch; + } + + /// + /// Get coastline direction from Blue channel of coastline color. + /// Color pattern: R=200, G=100, B=direction indicator. + /// + private static CoastlineDirection GetCoastlineDirection(byte b) + { + return b switch + { + >= 95 and <= 105 => CoastlineDirection.W, // B=100 + >= 115 and <= 125 => CoastlineDirection.S, // B=120 + >= 135 and <= 145 => CoastlineDirection.N, // B=140 + >= 155 and <= 165 => CoastlineDirection.E, // B=160 + >= 175 and <= 185 => CoastlineDirection.SW, // B=180 + >= 186 and <= 195 => CoastlineDirection.SE, // B=190 + >= 205 and <= 215 => CoastlineDirection.NW, // B=210 + >= 216 and <= 225 => CoastlineDirection.NE, // B=220 + _ => CoastlineDirection.W // Default fallback + }; + } + + /// + /// Get coastline static for a specific direction. + /// + private ushort GetCoastlineStaticForDirection(CoastlineDirection direction) + { + if (direction == CoastlineDirection.None) + return 0; + + if (CoastlineStaticsByDirection.TryGetValue(direction, out var statics)) + return statics[_random.Next(statics.Length)]; + + // Fallback to W coast + return CoastlineStaticsByDirection[CoastlineDirection.W][0]; + } + + private ushort GetStaticForVegetation(VegetationType type) + { + var statics = type switch + { + VegetationType.Tree => TreeStatics, + VegetationType.Bush => BushStatics, + VegetationType.Grass => GrassStatics, + VegetationType.Flower => FlowerStatics, + VegetationType.Crop => CropStatics, + // Coastline is handled separately with direction + _ => Array.Empty() + }; + + if (statics.Length == 0) + return 0; + + return statics[_random.Next(statics.Length)]; + } + + /// + /// Check if vegetation type is coastline (requires special handling). + /// + private static bool IsCoastlineVegetation(VegetationType type) + { + return type == VegetationType.Coastline; + } + + #endregion + + #region Structure Classification + + private enum StructureType + { + None, Wall, Pillar, Fence + } + + private static StructureType ClassifyStructureColor(byte r, byte g, byte b) + { + // Black/near-black = no structure + if (r < 20 && g < 20 && b < 20) + return StructureType.None; + + // Grayscale check for walls/pillars + int diff = Math.Max(Math.Max(Math.Abs(r - g), Math.Abs(g - b)), Math.Abs(r - b)); + if (diff < 30) + { + int gray = (r + g + b) / 3; + if (gray >= 150) + return StructureType.Pillar; + else if (gray >= 100) + return StructureType.Wall; + } + + // Brown-ish = fence + if (r > g && r > b && g > b) + return StructureType.Fence; + + return StructureType.None; + } + + private ushort GetStaticForStructure(StructureType type) + { + var statics = type switch + { + StructureType.Wall => WallStatics, + StructureType.Pillar => PillarStatics, + StructureType.Fence => FenceStatics, + _ => Array.Empty() + }; + + if (statics.Length == 0) + return 0; + + return statics[_random.Next(statics.Length)]; + } + + #endregion +} diff --git a/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.cs b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.cs new file mode 100644 index 00000000..a10f4611 --- /dev/null +++ b/CentrED/Tools/LargeScale/Operations/ImportColoredHeightmap.cs @@ -0,0 +1,734 @@ +using CentrED.Client; +using CentrED.Network; +using CentrED.UI; +using Hexa.NET.ImGui; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using static CentrED.Application; +using static CentrED.LangEntry; + +namespace CentrED.Tools.LargeScale.Operations; + +/// +/// Imports a colored heightmap and optionally a vegetation map. +/// Maps biome colors to appropriate land tiles and vegetation colors to static objects. +/// +public partial class ImportColoredHeightmap : LocalLargeScaleTool +{ + public override string Name => LangManager.Get(LSO_IMPORT_COLORED_HEIGHTMAP); + + #region Fields + + private string _biomeFilePath = ""; + private string _heightmapFilePath = ""; + private string _vegetationFilePath = ""; + private string _structureFilePath = ""; + private Image? _biomeFile; + private Image? _heightmapFile; + private Image? _vegetationFile; + private Image? _structureFile; + private bool _useHeightmap = false; + private bool _importVegetation = false; + private bool _importStructures = false; + private bool _clearExistingStatics = false; + private bool _vegetationOnly = false; + private bool _coastlineOnly = false; + + // TilesBrush support + private string _tilesBrushFilePath = ""; + private bool _useTilesBrush = false; + private Dictionary? _tilesBrushes; + + // Dragon AAAABBBB transition system + private bool _useDragonTransitions = true; + + // Coastline fields are in ImportColoredHeightmap.Coastline.cs + + private int xOffset; + private int yOffset; + + // Cache for biome data during processing + private Biome[,]? _biomeCache; + private sbyte[,]? _altitudeCache; + private ushort[,]? _transitionCache; + private bool _applyTransitions = true; + private bool _smoothAltitude = true; + private int _smoothRadius = 2; + + private Random _random = new(); + + // Debug counters + private Dictionary _biomeCounts = new(); + private Dictionary _tileCounts = new(); + + #endregion + + #region UI + + protected override bool DrawToolUI() + { + var changed = false; + + // Biome map file + ImGui.Text(LangManager.Get(IMPORT_BIOME_MAP)); + ImGui.InputText("##biomepath", ref _biomeFilePath, 512); + ImGui.SameLine(); + if (ImGui.Button("...##biome")) + { + if (TinyFileDialogs.TryOpenFile + (LangManager.Get(SELECT_FILE), Environment.CurrentDirectory, ["*.png", "*.bmp"], null, false, out var newPath)) + { + _biomeFilePath = newPath; + return false; + } + } + + ImGui.Separator(); + + // Separate heightmap file (grayscale) + changed |= ImGui.Checkbox("Use Separate Heightmap", ref _useHeightmap); + ImGuiEx.Tooltip("Use a separate grayscale image for altitude (black=low, white=high)"); + + if (_useHeightmap) + { + ImGui.Text("Heightmap (grayscale):"); + ImGui.InputText("##heightpath", ref _heightmapFilePath, 512); + ImGui.SameLine(); + if (ImGui.Button("...##height")) + { + if (TinyFileDialogs.TryOpenFile + (LangManager.Get(SELECT_FILE), Environment.CurrentDirectory, ["*.png", "*.bmp"], null, false, out var newPath)) + { + _heightmapFilePath = newPath; + return false; + } + } + } + + ImGui.Separator(); + + // Structure map checkbox and file + changed |= ImGui.Checkbox("Import Structures", ref _importStructures); + ImGuiEx.Tooltip("Import structure map (walls, pillars) as static objects"); + + if (_importStructures) + { + ImGui.Text("Structure Map"); + ImGui.InputText("##structpath", ref _structureFilePath, 512); + ImGui.SameLine(); + if (ImGui.Button("...##struct")) + { + if (TinyFileDialogs.TryOpenFile + (LangManager.Get(SELECT_FILE), Environment.CurrentDirectory, ["*.png", "*.bmp"], null, false, out var newPath)) + { + _structureFilePath = newPath; + return false; + } + } + } + + ImGui.Separator(); + + // Vegetation-only mode + changed |= ImGui.Checkbox("Vegetation Only Mode", ref _vegetationOnly); + ImGuiEx.Tooltip("Import only vegetation/structures without modifying terrain. Biome map not required."); + + if (_vegetationOnly) + { + _importVegetation = true; + _coastlineOnly = false; + } + + // Coastline-only mode + if (!_vegetationOnly) + { + changed |= ImGui.Checkbox("Coastline Only Mode", ref _coastlineOnly); + ImGuiEx.Tooltip("Apply only coastline statics without modifying terrain. Requires biome map."); + } + + if (_coastlineOnly) + { + _applyCoastline = true; + } + + if (!_vegetationOnly && !_coastlineOnly) + { + changed |= ImGui.Checkbox(LangManager.Get(IMPORT_VEGETATION), ref _importVegetation); + ImGuiEx.Tooltip(LangManager.Get(IMPORT_VEGETATION_TOOLTIP)); + } + + if (!_vegetationOnly && !_coastlineOnly) + { + changed |= ImGui.Checkbox("Apply Biome Transitions", ref _applyTransitions); + ImGuiEx.Tooltip("Apply smooth transition tiles between different biomes"); + + if (_applyTransitions) + { + ImGui.Indent(); + + changed |= ImGui.Checkbox("Use Dragon AAAABBBB System", ref _useDragonTransitions); + ImGuiEx.Tooltip("Use DragonMod-style 8-neighbor pattern matching for transitions (recommended)"); + + changed |= ImGui.Checkbox("Use TilesBrush.xml", ref _useTilesBrush); + ImGuiEx.Tooltip("Use TilesBrush.xml for more accurate biome transitions"); + + if (_useTilesBrush) + { + ImGui.Text("TilesBrush.xml:"); + ImGui.InputText("##tilebrushpath", ref _tilesBrushFilePath, 512); + ImGui.SameLine(); + if (ImGui.Button("...##tilebrush")) + { + if (TinyFileDialogs.TryOpenFile + ("Select TilesBrush.xml", Environment.CurrentDirectory, ["*.xml"], null, false, out var newPath)) + { + _tilesBrushFilePath = newPath; + if (LoadTilesBrush(newPath)) + { + Console.WriteLine($"TilesBrush.xml loaded successfully: {_tilesBrushes?.Count ?? 0} brushes"); + } + return false; + } + } + + if (_tilesBrushes != null) + { + ImGui.TextColored(new System.Numerics.Vector4(0, 1, 0, 1), $"Loaded: {_tilesBrushes.Count} brushes"); + } + else if (!string.IsNullOrEmpty(_tilesBrushFilePath)) + { + ImGui.TextColored(new System.Numerics.Vector4(1, 0, 0, 1), "Not loaded - click ... to load"); + } + } + ImGui.Unindent(); + } + + changed |= ImGui.Checkbox("Smooth Altitude", ref _smoothAltitude); + ImGuiEx.Tooltip("Apply smoothing to altitude to reduce sharp transitions"); + + if (_smoothAltitude) + { + ImGui.SameLine(); + ImGui.PushItemWidth(80); + changed |= ImGui.SliderInt("Radius", ref _smoothRadius, 1, 5); + ImGui.PopItemWidth(); + } + + ImGui.Separator(); + + // Coastline options + changed |= ImGui.Checkbox("Apply Coastline", ref _applyCoastline); + ImGuiEx.Tooltip("Automatically generate coastline transitions between land and water"); + + if (_applyCoastline) + { + ImGui.Indent(); + ImGuiEx.Tooltip("Places wave statics on water tiles adjacent to land"); + ImGui.Unindent(); + } + } + + if (_importVegetation) + { + ImGui.Text(LangManager.Get(IMPORT_VEGETATION_MAP)); + ImGui.InputText("##vegpath", ref _vegetationFilePath, 512); + ImGui.SameLine(); + if (ImGui.Button("...##veg")) + { + if (TinyFileDialogs.TryOpenFile + (LangManager.Get(SELECT_FILE), Environment.CurrentDirectory, ["*.png", "*.bmp"], null, false, out var newPath)) + { + _vegetationFilePath = newPath; + return false; + } + } + + changed |= ImGui.Checkbox(LangManager.Get(CLEAR_EXISTING_STATICS), ref _clearExistingStatics); + ImGuiEx.Tooltip(LangManager.Get(CLEAR_EXISTING_STATICS_TOOLTIP)); + } + + return !changed; + } + + #endregion + + #region Validation + + public override bool CanSubmit(RectU16 area) + { + if (!_vegetationOnly) + { + try + { + using var fileStream = File.OpenRead(_biomeFilePath); + _biomeFile = Image.Load(fileStream); + } + catch (Exception e) + { + _submitStatus = string.Format(LangManager.Get(OPEN_FILE_ERROR_1INFO), e.Message); + return false; + } + + if (_biomeFile.Width != area.Width || _biomeFile.Height != area.Height) + { + _submitStatus = LangManager.Get(FILE_SIZE_MISMATCH_AREA); + _biomeFile.Dispose(); + _biomeFile = null; + return false; + } + } + + if (_useHeightmap && !_vegetationOnly) + { + try + { + using var heightStream = File.OpenRead(_heightmapFilePath); + _heightmapFile = Image.Load(heightStream); + } + catch (Exception e) + { + _submitStatus = $"Heightmap error: {e.Message}"; + _biomeFile?.Dispose(); + _biomeFile = null; + return false; + } + + if (_heightmapFile.Width != area.Width || _heightmapFile.Height != area.Height) + { + _submitStatus = "Heightmap size mismatch"; + _biomeFile?.Dispose(); + _biomeFile = null; + _heightmapFile?.Dispose(); + _heightmapFile = null; + return false; + } + } + + if (_importStructures && !string.IsNullOrEmpty(_structureFilePath)) + { + try + { + using var fileStream = File.OpenRead(_structureFilePath); + _structureFile = Image.Load(fileStream); + } + catch (Exception e) + { + _submitStatus = $"Structure map error: {e.Message}"; + _biomeFile?.Dispose(); + _biomeFile = null; + _heightmapFile?.Dispose(); + _heightmapFile = null; + return false; + } + + if (_structureFile.Width != area.Width || _structureFile.Height != area.Height) + { + _submitStatus = "Structure map size mismatch"; + _biomeFile?.Dispose(); + _biomeFile = null; + _structureFile?.Dispose(); + _structureFile = null; + return false; + } + } + + if (_importVegetation && !string.IsNullOrEmpty(_vegetationFilePath)) + { + try + { + using var fileStream = File.OpenRead(_vegetationFilePath); + _vegetationFile = Image.Load(fileStream); + } + catch (Exception e) + { + _submitStatus = string.Format(LangManager.Get(OPEN_FILE_ERROR_1INFO), e.Message); + _biomeFile?.Dispose(); + _biomeFile = null; + return false; + } + + if (_vegetationFile.Width != area.Width || _vegetationFile.Height != area.Height) + { + _submitStatus = LangManager.Get(FILE_SIZE_MISMATCH_AREA); + _biomeFile?.Dispose(); + _vegetationFile?.Dispose(); + _biomeFile = null; + _vegetationFile = null; + return false; + } + } + + if (_useTilesBrush && _tilesBrushes == null && !string.IsNullOrEmpty(_tilesBrushFilePath)) + { + if (!LoadTilesBrush(_tilesBrushFilePath)) + { + _submitStatus = "Failed to load TilesBrush.xml"; + _biomeFile?.Dispose(); + _biomeFile = null; + _heightmapFile?.Dispose(); + _heightmapFile = null; + _structureFile?.Dispose(); + _structureFile = null; + _vegetationFile?.Dispose(); + _vegetationFile = null; + return false; + } + } + + return true; + } + + #endregion + + #region Processing + + protected override void PreProcessArea(CentrEDClient client, RectU16 area) + { + base.PreProcessArea(client, area); + xOffset = area.X1; + yOffset = area.Y1; + _random = new Random(42); + _biomeCounts.Clear(); + _tileCounts.Clear(); + + // Reload files if they were disposed + if (_biomeFile == null && !string.IsNullOrEmpty(_biomeFilePath)) + { + try + { + using var fileStream = File.OpenRead(_biomeFilePath); + _biomeFile = Image.Load(fileStream); + } + catch (Exception e) + { + Console.WriteLine($"Error reloading biome file: {e.Message}"); + return; + } + } + + if (_useHeightmap && _heightmapFile == null && !string.IsNullOrEmpty(_heightmapFilePath)) + { + try + { + using var heightStream = File.OpenRead(_heightmapFilePath); + _heightmapFile = Image.Load(heightStream); + } + catch (Exception e) + { + Console.WriteLine($"Error reloading heightmap file: {e.Message}"); + } + } + + if (_vegetationOnly) + return; + + if (_biomeFile == null) return; + + int width = _biomeFile.Width; + int height = _biomeFile.Height; + + // Build biome cache (needed for both normal mode and coastline-only mode) + _biomeCache = new Biome[width, height]; + + for (int py = 0; py < height; py++) + { + for (int px = 0; px < width; px++) + { + var color = _biomeFile[px, py]; + var biome = ClassifyBiomeColorOnly(color.R, color.G, color.B); + _biomeCache[px, py] = biome; + } + } + + // Coastline-only mode: skip altitude and transitions + // Coastline is applied directly in ProcessTile using map tile IDs + if (_coastlineOnly) + { + return; + } + + // Build altitude cache for normal mode + var rawAltitude = new sbyte[width, height]; + + for (int py = 0; py < height; py++) + { + for (int px = 0; px < width; px++) + { + var color = _biomeFile[px, py]; + + if (_useHeightmap && _heightmapFile != null) + { + var gray = _heightmapFile[px, py].PackedValue; + rawAltitude[px, py] = (sbyte)(gray - 128); + } + else + { + var (_, altitude) = ClassifyBiomeColor(color.R, color.G, color.B); + rawAltitude[px, py] = altitude; + } + } + } + + // Apply altitude smoothing if enabled + if (_smoothAltitude) + { + _altitudeCache = new sbyte[width, height]; + + for (int py = 0; py < height; py++) + { + for (int px = 0; px < width; px++) + { + var currentBiome = _biomeCache[px, py]; + + if (currentBiome == Biome.Water) + { + _altitudeCache[px, py] = rawAltitude[px, py]; + continue; + } + + // Rock biome ALWAYS uses smooth radius 1, regardless of user setting + int radius = (currentBiome == Biome.Rock) ? 1 : _smoothRadius; + + int sum = 0; + int count = 0; + + for (int dy = -radius; dy <= radius; dy++) + { + for (int dx = -radius; dx <= radius; dx++) + { + int nx = px + dx; + int ny = py + dy; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) + { + if (_biomeCache[nx, ny] != Biome.Water) + { + sum += rawAltitude[nx, ny]; + count++; + } + } + } + } + + _altitudeCache[px, py] = count > 0 + ? (sbyte)Math.Clamp(sum / count, -25, 127) + : rawAltitude[px, py]; + } + } + } + else + { + _altitudeCache = rawAltitude; + } + + // BIDIRECTIONAL TRANSITION SYSTEM - All border tiles get transitions + if (_applyTransitions) + { + // Initialize Dragon transitions if enabled + if (_useDragonTransitions) + { + InitializeDragonTransitions(); + } + + _transitionCache = new ushort[width, height]; + + for (int py = 0; py < height; py++) + { + for (int px = 0; px < width; px++) + { + var centerBiome = _biomeCache[px, py]; + + if (centerBiome == Biome.Water || centerBiome == Biome.Lava || centerBiome == Biome.Void) + { + _transitionCache[px, py] = 0; + continue; + } + + // Check if this tile has any different neighbor (cardinal or diagonal) + bool hasDifferentNeighbor = false; + int[] dx = { 0, 0, 1, -1, -1, 1, -1, 1 }; // N, S, E, W, NW, NE, SW, SE + int[] dy = { -1, 1, 0, 0, -1, -1, 1, 1 }; + + for (int i = 0; i < dx.Length; i++) + { + int nx = px + dx[i]; + int ny = py + dy[i]; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) + { + var neighborBiome = _biomeCache[nx, ny]; + if (neighborBiome != centerBiome && + neighborBiome != Biome.Water && + neighborBiome != Biome.Lava && + neighborBiome != Biome.Void) + { + hasDifferentNeighbor = true; + break; + } + } + } + + if (hasDifferentNeighbor) + { + if (_useDragonTransitions) + { + // Use Dragon AAAABBBB system + _transitionCache[px, py] = CalculateDragonTransition(px, py, centerBiome, width, height); + } + else + { + // Use original transition system + _transitionCache[px, py] = CalculateTransitionTile(px, py, centerBiome, width, height); + } + } + else + { + _transitionCache[px, py] = 0; + } + } + } + } + + // Coastline is now applied directly in ProcessTile using map tile IDs + } + + protected override void ProcessTile(CentrEDClient client, ushort x, ushort y) + { + var pixelX = x - xOffset; + var pixelY = y - yOffset; + + var landTile = client.GetLandTile(x, y); + var currentZ = landTile.Z; + + // Coastline-only mode: coastline is applied in batch via PostProcessArea + if (_coastlineOnly) + { + return; + } + + if (!_vegetationOnly) + { + var biome = _biomeCache != null ? _biomeCache[pixelX, pixelY] : Biome.Grass; + var newZ = _altitudeCache != null ? _altitudeCache[pixelX, pixelY] : (sbyte)0; + + ushort tileId; + if (_applyTransitions && _transitionCache != null) + { + var cachedTile = _transitionCache[pixelX, pixelY]; + if (cachedTile != 0) + { + tileId = cachedTile; + } + else + { + tileId = GetLandTileForBiome(biome); + } + } + else + { + tileId = GetLandTileForBiome(biome); + } + + landTile.ReplaceLand(tileId, newZ); + + _biomeCounts[biome] = _biomeCounts.GetValueOrDefault(biome) + 1; + _tileCounts[tileId] = _tileCounts.GetValueOrDefault(tileId) + 1; + + currentZ = newZ; + // Coastline is applied in batch via PostProcessArea + } + + if ((_importVegetation || _importStructures) && _clearExistingStatics) + { + foreach (var staticTile in client.GetStaticTiles(x, y).ToArray()) + { + client.Remove(staticTile); + } + } + + if (_importStructures && _structureFile != null) + { + var structColor = _structureFile[pixelX, pixelY]; + var structType = ClassifyStructureColor(structColor.R, structColor.G, structColor.B); + + if (structType != StructureType.None) + { + var staticId = GetStaticForStructure(structType); + if (staticId != 0) + { + client.Add(new StaticTile(staticId, x, y, currentZ, 0)); + } + } + } + + if (_importVegetation && _vegetationFile != null) + { + var vegColor = _vegetationFile[pixelX, pixelY]; + var vegType = ClassifyVegetationColor(vegColor.R, vegColor.G, vegColor.B); + + if (vegType != VegetationType.None) + { + ushort staticId; + sbyte staticZ; + + if (IsCoastlineVegetation(vegType)) + { + // Coastline: get direction-specific static and place at z=-4 + var direction = GetCoastlineDirection(vegColor.B); + staticId = GetCoastlineStaticForDirection(direction); + staticZ = -4; + } + else + { + // Regular vegetation + staticId = GetStaticForVegetation(vegType); + staticZ = currentZ; + } + + if (staticId != 0) + { + client.Add(new StaticTile(staticId, x, y, staticZ, 0)); + } + } + } + } + + protected override void PostProcessArea(CentrEDClient client, RectU16 area) + { + base.PostProcessArea(client, area); + + // Apply coastline in batch after all tiles have been imported + ApplyCoastlineToArea(client, area); + + Console.WriteLine("=== ImportColoredHeightmap Debug ==="); + Console.WriteLine($"Coastline: processed {_coastlineProcessed} tiles, added {_coastlineAdded} statics, modified {_coastlineTerrainModified} terrain Z"); + _coastlineProcessed = 0; + _coastlineAdded = 0; + _coastlineTerrainModified = 0; + Console.WriteLine("Biome distribution:"); + foreach (var (biome, count) in _biomeCounts.OrderByDescending(x => x.Value)) + { + Console.WriteLine($" {biome}: {count}"); + } + Console.WriteLine("\nTop 10 tile IDs used:"); + foreach (var (tileId, count) in _tileCounts.OrderByDescending(x => x.Value).Take(10)) + { + Console.WriteLine($" 0x{tileId:X4}: {count}"); + } + Console.WriteLine("===================================="); + + _biomeFile?.Dispose(); + _biomeFile = null; + _heightmapFile?.Dispose(); + _heightmapFile = null; + _vegetationFile?.Dispose(); + _vegetationFile = null; + _structureFile?.Dispose(); + _structureFile = null; + _biomeCache = null; + _altitudeCache = null; + _transitionCache = null; + } + + #endregion +}