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
+}