Task 17's hard 1.2 x CraterRadius cutoff left a visible un-eroded disc with a hard edge. Measured (radius dump, seed 1280587109): the carve writes only inside 0.80 x (640 px) and its displacement is EXACTLY 0 beyond that, so the 640-960 px annulus was 620,811 LAND cells of ordinary terrain held smooth for no geometric reason. That annulus is now eroded — 98% of those cells are touched. The protected core is the carve's own extent (CraterErosionCore, 0.80 x). CraterErosionMode selects the transition: "full" applies full strength at the core boundary (older-crater look), "feather" ramps 0->full out to CraterErosionFeather (1.05 x, mirroring the detail pass) for a younger-crater look with no seam. Implemented as a WEIGHT that scales carve and deposit amounts, not a skip, which is what makes feather a one-liner. Default "feather" pending the gate. Core = the carve radius is load-bearing, not tidy: the carve runs AFTER erosion and scales height toward the sea target, so it amplifies any erosion delta inside its radius. Measured at a 0.50 core: 79 cells newly below the rendered sea and 113,310 below-sea cells disturbed — the in-pass flood guard cannot see this because it measures before the carve. At 0.80 the two passes touch disjoint cells and the guarantee is exact again (0/0/0). A narrower core also reclaims nothing extra, since the over-protected annulus lies entirely outside the carve. MapGenerator now owns CRATER_CARVE_FACTOR as the single source of that 0.80 and warns loudly if the configured core is narrower. Verified both modes, seed 1280587109: 1_biomes/0_water md5-identical to erosion-OFF, BIOM/WBID bitwise equal, 0 newly below/above sea, 0 below-sea cells modified, 0 cells modified inside the core, island top 457.65 m unchanged, isotropy 1.005/1.007. Bay-to-ocean connection demonstrated explicitly by flood fill through rendered water to the map's north border (2,122,935 cells), not merely argued from the sea clamp. EROS body version -> 3: craterExclFactor replaced by craterCoreFactor + craterFeatherFactor + craterMode. Round-trip harness PASS with a real v3 payload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94 lines
4.6 KiB
C#
94 lines
4.6 KiB
C#
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.
|
||
// 3 (task 19, current) replaces the single crater exclusion factor with the
|
||
// protected-core factor, a feather-band factor, and the crater mode byte.
|
||
public const ushort EROS_VERSION = 3;
|
||
|
||
// 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)
|
||
});
|
||
}
|
||
}
|
||
}
|