islaApocalypse/Core/Scripts/BlueprintFormat.cs
beezm cc6a24d2b9 fix: brush-spread deposition + deposit-cap governor; retune erosion for a drainage hierarchy (terrain-water task 18)
Deposition now spreads over the same cone brush as carving, and the per-cell
erosion ledger becomes a signed NET displacement ledger, so both caps are
measured from the height the pass found. Brush-spreading alone cut the spike
15.49 -> 6.12 m at task-17 dials, but NOT at hierarchy dials (long paths carry
far more sediment; a loaded droplet meeting a rise dumps min(rise, load) at
once -> 25.5 m). So deposition also gets governor 4, ErosionDepositCap
(default 6 m, <= 0 = unbounded), asserted on exit like the carve cap.

Dial defaults retuned for a drainage HIERARCHY: 250k droplets x 384 steps at
inertia 0.35 / evaporation 0.004, erode 0.12, carve cap 15 m. Diagnosis was
that lifetime 48 let a droplet travel at most 48 px on a ~3000 px island
radius, so paths could not overlap into trunks: the deepest task-17 features
were 43x36 px patches. Now 177 channel systems of 200+ cells at the 3 m
threshold, top one 204x279 px with 14 tributary tips; cells past 5 m up 8.6x
(4,948 -> 42,385) while the fine rills are retained. Verified descending, not
contour-locked: 0/155 components have drop/extent < 0.12 (median 1.13).

EROS body version -> 2 (deposit cap inserted after the carve cap); v1 payloads
are skipped whole by the existing rule. HydraulicErosion.VERSION now reads
BlueprintFormat.EROS_VERSION instead of restating it — the local copy had
already drifted and stamped a v2 body as v1, which readers decode with every
float shifted by one field.

Oracle green on both seeds (1280587109, 1512575962): 1_biomes/0_water
md5-identical erosion-ON vs OFF, BIOM/WBID bitwise equal. Flood guard 0 new
water px, 0 below-sea cells touched, crater zone untouched, island top
unchanged, carve field isotropic to 1.4% across the folded 45deg period.
Erosion pass 34 s at these dials.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 04:16:33 -04:00

92 lines
4.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace IslaApocalypse.Core
{
/// <summary>
/// The single registry of constants for the v2 blueprint container — magic, version,
/// section tags, validation bounds, and the sentinel values used when a v2 file is
/// re-encoded from a v1 source that cannot supply real generation parameters.
///
/// The byte-accurate layout lives in Core/Scripts/BLUEPRINT_FORMAT.md. Writer:
/// BlueprintWriter.WriteV2. Reader: MapDataParser (v2 branch). Tags are FourCC codes
/// stored as little-endian u32, so the raw bytes on disk read as ASCII ("PRMS", "HGTS",
/// ...) in a hex dump.
/// </summary>
public static class BlueprintFormat
{
// "ISLA" as raw bytes 'I','S','L','A' == little-endian u32 0x414C5349.
// Deliberately NOT a length-prefixed .NET string: the v1 header starts with the
// 7-bit length prefix 0x07, so the first byte alone (0x49 vs 0x07) identifies the
// format and the reader can dispatch without ambiguity.
public const uint MAGIC = 0x414C5349;
public const uint VERSION = 2;
// Section tags (FourCC, little-endian). Reader rule: known tag -> parse,
// unknown tag -> skip by length and continue. Never reuse a retired tag value.
public const uint TAG_PARAMS = 0x534D5250; // "PRMS"
public const uint TAG_HEIGHTS = 0x53544748; // "HGTS"
public const uint TAG_BIOMES = 0x4D4F4942; // "BIOM"
public const uint TAG_TOWNS = 0x4E574F54; // "TOWN"
public const uint TAG_ROADS_HIGHWAY = 0x57484452; // "RDHW"
public const uint TAG_ROADS_BRANCH = 0x52424452; // "RDBR"
public const uint TAG_ROADS_RUGGED = 0x47524452; // "RDRG"
public const uint TAG_ROADS_TRAIL = 0x4C544452; // "RDTL"
public const uint TAG_WATER_BODY_IDS = 0x44494257; // "WBID"
public const uint TAG_WATER_BODY_TABLE = 0x42544257; // "WBTB"
public const uint TAG_WATER_SURFACE = 0x46525357; // "WSRF"
public const uint TAG_TERRAIN_CURVE = 0x56524354; // "TCRV"
public const uint TAG_TERRAIN_DETAIL = 0x4C544454; // "TDTL"
public const uint TAG_EROSION = 0x534F5245; // "EROS"
// TDTL body version. 1 = shelf micro-relief + D8 drainage incision (the
// task-10 draft; the incision was reverted, and the only v1 payloads in
// existence are in that batch's tree). 2 = the current body. A reader that
// meets an unrecognised body version skips it rather than misreading a
// differently shaped payload into plausible-looking nonsense.
public const ushort TDTL_VERSION = 2;
// EROS body version. 1 (task 17) = governors count/lifetime/carve cap, sea
// clamp, brush, strength constants, RNG seed offset, crater exclusion factor;
// the only v1 payloads in existence are in that task's batch tree. 2 (task 18,
// current) inserts the DEPOSIT CAP governor after the carve cap — deposition is
// now brush-spread and per-cell bounded. Same reader rule as TDTL: an unknown
// body version is skipped whole rather than misread into plausible nonsense.
public const ushort EROS_VERSION = 2;
// WSRF quantization: u16, 0 reserved as the no-water sentinel. A real level L
// (raw blueprint height units) encodes as 1 + round(L × 32768), so a genuine
// level can never encode to 0. Decodes back via (q 1) / 32768. Covers
// [0 .. ~1.99997] raw at 1/32768 raw resolution ≈ 7.7 mm of world height
// (1 raw unit = 251 m) — far below the 1 m voxel.
public const float WSRF_SCALE = 1f / 32768f;
public static ushort EncodeWaterLevel(float level)
{
return (ushort)Godot.Mathf.Clamp(1 + Godot.Mathf.RoundToInt(level * 32768f), 1, ushort.MaxValue);
}
public static float DecodeWaterLevel(ushort quantized)
{
return (quantized - 1) * WSRF_SCALE;
}
// MapSize sanity bounds, checked before any allocation on read.
public const int MIN_MAP_SIZE = 256;
public const int MAX_MAP_SIZE = 32768;
// Sentinels written into the params section when the source blueprint came from a
// v1 file (a re-encode cannot know the original generation inputs). WorldSeed 0 is
// also the config "randomize" sentinel, which no real resolved seed ever is.
public const int SENTINEL_WORLD_SEED = 0;
public const float SENTINEL_CRATER_RADIUS = -1f;
public const float SENTINEL_DENSITY_MULTIPLIER = -1f;
public const float SENTINEL_IMPACT_CENTER = -1f; // both components
/// <summary>Renders a tag as its 4 ASCII characters, for log messages.</summary>
public static string TagToString(uint tag)
{
return new string(new[] {
(char)(tag & 0xFF), (char)((tag >> 8) & 0xFF),
(char)((tag >> 16) & 0xFF), (char)((tag >> 24) & 0xFF)
});
}
}
}