using Godot; using Godot.Collections; // Required for Godot's built-in JSON parser namespace IslaApocalypse.Core // Change this if your namespace is different { public static class ConfigManager { // Default Fallbacks // public static int WorldSeed = 1063685222; public static int WorldSeed = (int)GD.Randi(); // if 0 also randomizes public static int MapSize = 8192; public static float CraterRadius = 400f; public static float DensityMultiplier = 1.0f; // <-- Replaces TownCount public static int ChunkRadius = 24; // Default chunk size, can be overridden by config // Iteration toggle: skip the ~25-min A* road pass entirely. The blueprint is // still exported (road sections present but empty) — an iteration artifact, // not a shippable world. Default false. public static bool SkipRoads = false; // Sea-level model (D-033, terrain-water task 04): "flat" = one scalar sea // level everywhere (SeaLevelValue); "field" = the legacy latitude Lerp // (0.26 north .. 0.15 south). Generator-only — the runtime never computes // sea level. Default: flat 0.15. public static string SeaLevelModel = "flat"; public static float SeaLevelValue = 0.15f; // Height-redistribution curve (tasks 05–10, graduation M-7): "v5" is the // task-09 gate's winner — the BALANCED terraced ascent with corner easing and // modulated shelves; "off" is the raw legacy profile. "v1"–"v4" and the // taste-batch tri-state ("v5-compact" lost, "v5-balanced" became plain "v5") // are retired. Default: v5. public static string TerrainCurve = "v5"; // Terrain detail passes (task 10): "v1" = shelf micro-relief + shelf-edge // variation as one judged unit (requires the curve; no-op when it is off); // "off" disables both. ShelfReliefAmp is the micro-relief amplitude in metres // of OUTPUT height. ShelfEdgeVariation is the shelf-edge warp amplitude in // metres of INPUT height — how far the shelf/riser boundary contour is // displaced, not an elevation change; it is clamped at load time to the // largest shift the curve's bands can absorb. Defaults: v1, 3 m, 12 m. public static string TerrainDetail = "v1"; public static float ShelfReliefAmp = 3.0f; public static float ShelfEdgeVariation = 12.0f; // Hydraulic erosion (task 17, Phase C0): droplet-based carve-and-deposit on // the RENDER height map only — the classify path (biomes/water) never sees // it. "off" until the developer's gate approves it; the batch that turns it // on does so explicitly. The three GOVERNORS hard-bound the pass: // DropletCount (cost/detail), DropletLifetime (max steps per droplet), // CarveCap (max erosion depth per cell, metres — the runaway-trench guard // and what keeps erosion a detailing pass). ErosionSeaMargin is the flood // guard: no cell is ever carved below sea + margin, and below-sea cells are // never touched at all, so the rendered coastline cannot move. The remaining // dials are the standard droplet-model strength constants; slopes/amounts // are in METRES (1 raw height unit = 251 m). // Task-18 defaults tune for a DRAINAGE HIERARCHY: long-lived, committed // droplets (lifetime 384 at inertia 0.35, evaporation 0.004) travel far // enough down a flank that their paths overlap and deepen shared low lines // into trunk channels, instead of dying as independent 48-px scratches; // the carve cap is raised to 15 m so trunks can separate from the fine // rills instead of both piling up against the same ceiling. A modest // erode rate keeps the total material moved in detailing range. // ErosionDepositCap is governor 4 (task 18): brush-spread deposition alone // does not bound a spike once droplets carry long-path loads. public static string Erosion = "off"; public static int ErosionDropletCount = 250000; public static int ErosionDropletLifetime = 384; public static float ErosionCarveCap = 15.0f; // m per cell public static float ErosionDepositCap = 6.0f; // m per cell; <= 0 = unbounded public static float ErosionSeaMargin = 0.5f; // m above sea, carve floor public static int ErosionBrushRadius = 2; // px public static float ErosionInertia = 0.35f; public static float ErosionCapacity = 4.0f; public static float ErosionMinSlope = 0.02f; // m per px, capacity floor public static float ErosionErodeRate = 0.12f; public static float ErosionDepositRate = 0.15f; public static float ErosionEvaporation = 0.004f; public static float ErosionGravity = 4.0f; // How erosion treats the crater surrounds (task 19). The task-17 hard // 1.2 × CraterRadius cutoff left a visible un-eroded disc: measured, the carve // writes only inside 0.80 × and its displacement is exactly 0 beyond that, so // 620 811 LAND cells of ordinary terrain were being held smooth for no // geometric reason. Now only the deep strike core is protected. // "full" — full-strength erosion right up to the core boundary. The // crater formed after the terrain and has weathered since. // "feather" — erosion ramps 0→full across CraterErosionCore → // CraterErosionFeather (the detail pass's shape), so the crater // reads as younger, less-weathered, and there is no seam at all. // Radii are FACTORS of CraterRadius. The flooded bay and its sea connection do // NOT depend on these: below-sea cells are read-only in both directions. public static string CraterErosionMode = "feather"; public static float CraterErosionCore = CRATER_EROSION_CORE_DEFAULT; public static float CraterErosionFeather = CRATER_EROSION_FEATHER_DEFAULT; // 0.80 = the carve's OWN extent (MapGenerator's physicalCraterRadius). Keeping the // core at least this wide is what makes erosion and the carve touch DISJOINT cells, // which is what keeps the post-carve flood guard exactly zero: the carve runs after // erosion and scales height toward the sea target, so it AMPLIFIES any erosion delta // inside its radius and can push a hair-above-sea cell across the waterline. Measured // at a 0.50 core: 79 cells newly below the rendered sea, 113 310 below-sea cells // disturbed. A smaller core reclaims nothing extra either — the over-protected annulus // is 0.80x-1.2x, entirely outside the carve. public const float CRATER_EROSION_CORE_DEFAULT = 0.80f; public const float CRATER_EROSION_FEATHER_DEFAULT = 1.05f; // Rivers (task 22, C0b part 2a): carve the frozen task-21b river plan's // beds into the RENDER map — ocean trunks, routed giants (lowland reach to // the sea), lake-enders. NO WATER yet (part 2b). "off" until the routing- // style gate; the A/B batch turns it on explicitly. RiverRoutingStyle picks // the lowland routing for routed giants: "short" heads direct (lightly // terrain-aware), "lowground" follows the lowest ground and wanders like a // real river — the task-22 gate decides which ships; "lowground" is the // provisional default pending that verdict. Width/depth scales are taste // dials on the flow-proportional bed profile. LOWGROUND is the LOCKED // default — the task-22/23 gate verdict ("short" stays available for the // record). RiverSeaMargin is the bed's // absolute floor above sea — the erosion flood-guard discipline: no river // bed may create inland below-sea cells, so the rendered coastline cannot // move even with rivers carved. // RiverStepDropM/RiverWaterDepthM (task 23): the stepped-water dials — each // river is a chain of flat water-body reaches; a new reach starts every // StepDrop metres of bed descent and sits WaterDepth metres above its bed. // Smaller drop = more, finer steps = smoother water (the smoothing dial; // tilted continuous-slope water is the deferred model B). public static string Rivers = "off"; public static string RiverRoutingStyle = "lowground"; public static float RiverWidthScale = 1.75f; // widened at the task-23 gate's ask public static float RiverStepDropM = 2.0f; public static float RiverWaterDepthM = 1.2f; public static float RiverDepthScale = 1.0f; public static float RiverSeaMargin = 0.2f; // m above sea, bed floor // Island falloff shaping (task 11). // // CoastProfile: "wide" adds the submarine shelf — the height curve is identity // at and below sea, so it never reached the seabed, which still dropped ~6.7x // steeper than the land it meets. "steep" is the pre-task-11 seabed, kept for // A/B. The shelf cannot move the waterline, so biomes and water are identical // either way. Default: wide. // // IslandAxisX/Y: the falloff axis ratios. These MOVE THE COASTLINE and // therefore move biomes, so the DEFAULT is the shape the gate approved. // Task 11 shipped 1.30/0.78 as the default and the developer's gate REJECTED // that elongation; the defaults are back to 1.15/0.90 so that omitting the // keys can no longer silently produce the rejected island (task 12 §4). // NOTE, measured in task 11: the island is already Trench-clamped in x at // ~90% of the map width, so AxisX is a weak lever — aspect responds almost // entirely to AxisY, which trades against land area. // // OffshoreIslandDensity: fraction of the ocean noise field above the islet // threshold. 0 disables the layer. Islets never touch the Trench and are held // off the mainland by a depth moat. public static string CoastProfile = "wide"; public static float IslandAxisX = LEGACY_AXIS_X; // 1.15 — the gate's verdict public static float IslandAxisY = LEGACY_AXIS_Y; // 0.90 public static float OffshoreIslandDensity = 0.02f; // The gate-approved island shape, named so the defaults above and the bad-value // restore below both point at one place. (`const`, so using it in a field // initialiser declared earlier resolves at compile time.) public const float LEGACY_AXIS_X = 1.15f; public const float LEGACY_AXIS_Y = 0.90f; public static void LoadConfig() { string path = "res://ServerConfig.json"; // Iteration override (task 22): ISLA_SERVER_CONFIG names an alternate // config FILE to load instead of the repo's ServerConfig.json. Batch and // A/B runs point this at a scratch config, so the developer's live // ServerConfig.json is never written by tooling again — the whole // backup/restore dance (and its task-19 near-miss) goes away. Loud, so // a forgotten env var cannot silently masquerade as the repo config. string envPath = OS.GetEnvironment("ISLA_SERVER_CONFIG"); if (!string.IsNullOrEmpty(envPath)) { if (FileAccess.FileExists(envPath)) { path = envPath; GD.Print($"[ConfigManager] ⚠ ISLA_SERVER_CONFIG override: loading '{envPath}' (NOT the repo ServerConfig.json)."); } else GD.PrintErr($"[ConfigManager] ISLA_SERVER_CONFIG set but '{envPath}' does not exist — falling back to the repo config."); } if (!FileAccess.FileExists(path)) { GD.PrintErr("[ConfigManager] ServerConfig.json not found! Defaulting to 8K."); return; } // Read the file using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); string content = file.GetAsText(); // Parse the JSON var json = new Json(); var error = json.Parse(content); if (error != Error.Ok) { GD.PrintErr($"[ConfigManager] JSON Parse Error: {json.GetErrorMessage()}"); return; } var data = (Dictionary)json.Data; // Extract the Seed if (data.ContainsKey("WorldSeed")) { WorldSeed = (int)data["WorldSeed"]; } // Extract the MapProfile and run your Switch/Case logic! string profile = "8K"; if (data.ContainsKey("MapProfile")) { profile = (string)data["MapProfile"]; } // Extract the TownDensity and run your Switch/Case logic! string townDensity = "Normal"; if (data.ContainsKey("TownDensity")) { townDensity = (string)data["TownDensity"]; } // Extract the ChunkRadius if (data.ContainsKey("ChunkRadius")) { ChunkRadius = (int)data["ChunkRadius"]; } // Extract the SkipRoads iteration toggle if (data.ContainsKey("SkipRoads")) { SkipRoads = (bool)data["SkipRoads"]; } // Extract the sea-level model if (data.ContainsKey("SeaLevelModel")) { string model = (string)data["SeaLevelModel"]; if (model == "flat" || model == "field") SeaLevelModel = model; else GD.PrintErr($"[ConfigManager] Unknown SeaLevelModel '{model}'. Keeping '{SeaLevelModel}'."); } if (data.ContainsKey("SeaLevelValue")) { SeaLevelValue = (float)data["SeaLevelValue"]; } // Extract the terrain-curve gate if (data.ContainsKey("TerrainCurve")) { string curve = (string)data["TerrainCurve"]; if (curve == "off" || curve == "v5") TerrainCurve = curve; else if (curve == "v5-balanced") GD.PrintErr($"[ConfigManager] TerrainCurve 'v5-balanced' won the task-09 gate and is now plain \"v5\". Keeping '{TerrainCurve}'."); else if (curve == "v5-compact") GD.PrintErr($"[ConfigManager] TerrainCurve 'v5-compact' was retired by the task-09 verdict (BALANCED won). Keeping '{TerrainCurve}' — use \"v5\" or \"off\"."); else if (curve == "v1" || curve == "v2" || curve == "v3" || curve == "v4") GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v5, tasks 09/10). Keeping '{TerrainCurve}' — use \"v5\" or \"off\"."); else GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'."); } // Extract the terrain-detail gate + relief amplitude if (data.ContainsKey("TerrainDetail")) { string detail = (string)data["TerrainDetail"]; if (detail == "off" || detail == "v1") TerrainDetail = detail; else GD.PrintErr($"[ConfigManager] Unknown TerrainDetail '{detail}'. Keeping '{TerrainDetail}'."); } if (data.ContainsKey("ShelfReliefAmp")) { ShelfReliefAmp = (float)data["ShelfReliefAmp"]; } if (data.ContainsKey("ShelfEdgeVariation")) { ShelfEdgeVariation = (float)data["ShelfEdgeVariation"]; } // Extract the erosion gate + dials (task 17) if (data.ContainsKey("Erosion")) { string erosion = (string)data["Erosion"]; if (erosion == "off" || erosion == "v1") Erosion = erosion; else GD.PrintErr($"[ConfigManager] Unknown Erosion '{erosion}'. Keeping '{Erosion}'."); } if (data.ContainsKey("ErosionDropletCount")) ErosionDropletCount = (int)data["ErosionDropletCount"]; if (data.ContainsKey("ErosionDropletLifetime")) ErosionDropletLifetime = (int)data["ErosionDropletLifetime"]; if (data.ContainsKey("ErosionCarveCap")) ErosionCarveCap = (float)data["ErosionCarveCap"]; if (data.ContainsKey("ErosionDepositCap")) ErosionDepositCap = (float)data["ErosionDepositCap"]; if (data.ContainsKey("ErosionSeaMargin")) ErosionSeaMargin = (float)data["ErosionSeaMargin"]; if (data.ContainsKey("ErosionBrushRadius")) ErosionBrushRadius = (int)data["ErosionBrushRadius"]; if (data.ContainsKey("ErosionInertia")) ErosionInertia = (float)data["ErosionInertia"]; if (data.ContainsKey("ErosionCapacity")) ErosionCapacity = (float)data["ErosionCapacity"]; if (data.ContainsKey("ErosionMinSlope")) ErosionMinSlope = (float)data["ErosionMinSlope"]; if (data.ContainsKey("ErosionErodeRate")) ErosionErodeRate = (float)data["ErosionErodeRate"]; if (data.ContainsKey("ErosionDepositRate")) ErosionDepositRate = (float)data["ErosionDepositRate"]; if (data.ContainsKey("ErosionEvaporation")) ErosionEvaporation = (float)data["ErosionEvaporation"]; if (data.ContainsKey("ErosionGravity")) ErosionGravity = (float)data["ErosionGravity"]; // Governor bounds are enforced HERE, loudly, so a bad dial is a refused // dial rather than a silently absurd generation. The clamps are wide — // they exist to catch typos (an extra zero), not to tune. int rawCount = ErosionDropletCount; int rawLife = ErosionDropletLifetime; float rawCap = ErosionCarveCap; ErosionDropletCount = Mathf.Clamp(ErosionDropletCount, 0, 50_000_000); ErosionDropletLifetime = Mathf.Clamp(ErosionDropletLifetime, 1, 4096); ErosionCarveCap = Mathf.Clamp(ErosionCarveCap, 0f, 60f); if (rawCount != ErosionDropletCount || rawLife != ErosionDropletLifetime || rawCap != ErosionCarveCap) GD.PrintErr($"[ConfigManager] Erosion governor out of bounds — clamped: count {rawCount}->{ErosionDropletCount}, lifetime {rawLife}->{ErosionDropletLifetime}, cap {rawCap}->{ErosionCarveCap} m."); // Rivers gate + dials (task 22) if (data.ContainsKey("Rivers")) { string rv = (string)data["Rivers"]; if (rv == "off" || rv == "v1") Rivers = rv; else GD.PrintErr($"[ConfigManager] Unknown Rivers '{rv}'. Keeping '{Rivers}'."); } if (data.ContainsKey("RiverRoutingStyle")) { string st = (string)data["RiverRoutingStyle"]; if (st == "short" || st == "lowground") RiverRoutingStyle = st; else GD.PrintErr($"[ConfigManager] Unknown RiverRoutingStyle '{st}'. Keeping '{RiverRoutingStyle}'."); } if (data.ContainsKey("RiverWidthScale")) RiverWidthScale = (float)data["RiverWidthScale"]; if (data.ContainsKey("RiverDepthScale")) RiverDepthScale = (float)data["RiverDepthScale"]; if (data.ContainsKey("RiverSeaMargin")) RiverSeaMargin = (float)data["RiverSeaMargin"]; if (data.ContainsKey("RiverStepDropM")) RiverStepDropM = (float)data["RiverStepDropM"]; if (data.ContainsKey("RiverWaterDepthM")) RiverWaterDepthM = (float)data["RiverWaterDepthM"]; RiverStepDropM = Mathf.Clamp(RiverStepDropM, 0.25f, 10f); RiverWaterDepthM = Mathf.Clamp(RiverWaterDepthM, 0.2f, 5f); RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f); RiverDepthScale = Mathf.Clamp(RiverDepthScale, 0.1f, 5f); RiverSeaMargin = Mathf.Clamp(RiverSeaMargin, 0f, 5f); // Crater erosion treatment (task 19) if (data.ContainsKey("CraterErosionMode")) { string cm = (string)data["CraterErosionMode"]; if (cm == "full" || cm == "feather") CraterErosionMode = cm; else GD.PrintErr($"[ConfigManager] Unknown CraterErosionMode '{cm}'. Keeping '{CraterErosionMode}'."); } if (data.ContainsKey("CraterErosionCore")) CraterErosionCore = (float)data["CraterErosionCore"]; if (data.ContainsKey("CraterErosionFeather")) CraterErosionFeather = (float)data["CraterErosionFeather"]; CraterErosionCore = Mathf.Clamp(CraterErosionCore, 0f, 3f); CraterErosionFeather = Mathf.Clamp(CraterErosionFeather, 0f, 4f); // A feather band that does not extend past the core is not a band; say so // rather than silently behaving like "full". if (CraterErosionMode == "feather" && CraterErosionFeather <= CraterErosionCore) { GD.PrintErr($"[ConfigManager] CraterErosionFeather {CraterErosionFeather:F2} must exceed " + $"CraterErosionCore {CraterErosionCore:F2} — the ramp would have zero width. " + $"Restoring {CRATER_EROSION_FEATHER_DEFAULT:F2}."); CraterErosionFeather = CRATER_EROSION_FEATHER_DEFAULT; } // Negative is meaningless; 0 is the documented "unbounded" escape hatch. ErosionDepositCap = Mathf.Clamp(ErosionDepositCap, 0f, 60f); ErosionSeaMargin = Mathf.Clamp(ErosionSeaMargin, 0f, 5f); ErosionBrushRadius = Mathf.Clamp(ErosionBrushRadius, 0, 8); ErosionInertia = Mathf.Clamp(ErosionInertia, 0f, 0.99f); ErosionCapacity = Mathf.Max(ErosionCapacity, 0f); ErosionMinSlope = Mathf.Max(ErosionMinSlope, 0f); ErosionErodeRate = Mathf.Clamp(ErosionErodeRate, 0f, 1f); ErosionDepositRate = Mathf.Clamp(ErosionDepositRate, 0f, 1f); ErosionEvaporation = Mathf.Clamp(ErosionEvaporation, 0f, 0.5f); ErosionGravity = Mathf.Max(ErosionGravity, 0f); // Extract the island-falloff dials (task 11) if (data.ContainsKey("CoastProfile")) { string coast = (string)data["CoastProfile"]; if (coast == "steep" || coast == "wide") CoastProfile = coast; else GD.PrintErr($"[ConfigManager] Unknown CoastProfile '{coast}'. Keeping '{CoastProfile}'."); } if (data.ContainsKey("IslandAxisX")) IslandAxisX = (float)data["IslandAxisX"]; if (data.ContainsKey("IslandAxisY")) IslandAxisY = (float)data["IslandAxisY"]; if (data.ContainsKey("OffshoreIslandDensity")) OffshoreIslandDensity = (float)data["OffshoreIslandDensity"]; // The axis ratios divide map extents; a zero or negative one is a divide-by- // zero that would silently produce an all-ocean map. Refuse it loudly. if (IslandAxisX <= 0.01f || IslandAxisY <= 0.01f) { GD.PrintErr($"[ConfigManager] IslandAxisX/Y must be > 0.01 (got {IslandAxisX}/{IslandAxisY}). Restoring legacy {LEGACY_AXIS_X}/{LEGACY_AXIS_Y}."); IslandAxisX = LEGACY_AXIS_X; IslandAxisY = LEGACY_AXIS_Y; } OffshoreIslandDensity = Mathf.Clamp(OffshoreIslandDensity, 0f, 0.5f); switch (profile) { case "4K": MapSize = 4096; CraterRadius = 400f; // Scaled down crater break; case "6K": MapSize = 6144; CraterRadius = 600f; break; case "8K": MapSize = 8192; CraterRadius = 800f; // Your original crater size spread over an 8K map break; case "10K": MapSize = 10240; CraterRadius = 1000f; break; default: GD.PrintErr($"[ConfigManager] Unknown MapProfile '{profile}'. Defaulting to 8K."); MapSize = 8192; CraterRadius = 800f; break; } switch (townDensity) { case "Sparse": DensityMultiplier = 0.5f; break; case "Normal": DensityMultiplier = 1.0f; break; case "Dense": DensityMultiplier = 2.0f; break; default: GD.PrintErr($"[ConfigManager] Unknown TownDensity '{townDensity}'. Defaulting to Normal."); DensityMultiplier = 1.0f; break; } GD.Print($"[ConfigManager] Successfully loaded {profile} Profile. MapSize set to {MapSize}."); } } }