feat: 0_height hillshade snapshot + TCRV blueprint section + harness (terrain-water task 05)

0_height: hypsometric storm-ladder tint x Lambert NW hillshade over
the curved heights, captured after topography in both curve modes —
relief is finally visible. Renumbers nothing.

TCRV (50 B, emitted when the curve is on): curve version + input knots
+ output bands, so blueprints are self-describing about the map their
heights went through (H7 spirit). Parsed into
WorldBlueprint.TerrainCurve (null = raw legacy profile); server treats
it as metadata. Round-trip harness compares it when present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-07 15:24:33 -04:00
parent 67868638be
commit e7de292b3d
5 changed files with 142 additions and 1 deletions

View file

@ -32,6 +32,7 @@ namespace IslaApocalypse.Core
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"
// 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

View file

@ -31,6 +31,8 @@ namespace IslaApocalypse.Core
writer.Write(BlueprintFormat.VERSION);
WriteSection(writer, BlueprintFormat.TAG_PARAMS, w => WriteParams(w, bp));
if (bp.TerrainCurve != null)
WriteSection(writer, BlueprintFormat.TAG_TERRAIN_CURVE, w => WriteTerrainCurve(w, bp.TerrainCurve));
WriteSection(writer, BlueprintFormat.TAG_HEIGHTS, w => WriteHeights(w, bp));
WriteSection(writer, BlueprintFormat.TAG_BIOMES, w => WriteBiomes(w, bp));
@ -122,6 +124,16 @@ namespace IslaApocalypse.Core
}
}
private static void WriteTerrainCurve(BinaryWriter writer, TerrainCurveInfo c)
{
writer.Write(c.Version);
writer.Write(c.T1); writer.Write(c.T2); writer.Write(c.T3); writer.Write(c.T4);
writer.Write(c.HMaxCal);
writer.Write(c.Sea); writer.Write(c.OrangeCeil); writer.Write(c.RedCeil);
writer.Write(c.PlateauLo); writer.Write(c.PlateauHi); writer.Write(c.PeakCap);
writer.Write(c.TailSlope);
}
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
{
int n = bp.MapSize;

View file

@ -58,6 +58,20 @@ namespace IslaApocalypse.Core
public Vector2 Centroid; // map pixels
}
/// <summary>
/// The height-redistribution curve that shaped this blueprint's heights (v2 TCRV
/// section, terrain-water task 05). Null when the blueprint was generated with the
/// curve off (or predates it) — heights are then the raw legacy profile. Pure
/// metadata: the server never re-applies the curve; exported heights are already
/// curved.
/// </summary>
public class TerrainCurveInfo
{
public ushort Version;
public float T1, T2, T3, T4, HMaxCal; // input knots
public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands
}
public class WorldBlueprint
{
public int MapSize;
@ -82,6 +96,9 @@ namespace IslaApocalypse.Core
public ushort[,] WaterBodyIds; // 0 = no water, 1 = ocean, 2..N = lakes
public List<WaterBodyInfo> WaterBodies = new List<WaterBodyInfo>();
public ushort[,] WaterSurfaceQ; // quantized levels — BlueprintFormat.DecodeWaterLevel
// The curve that shaped HeightMap (v2 TCRV section); null = raw legacy profile.
public TerrainCurveInfo TerrainCurve;
}
// 2. The Parser Utility
@ -204,6 +221,7 @@ namespace IslaApocalypse.Core
else if (tag == BlueprintFormat.TAG_WATER_BODY_IDS) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: false);
else if (tag == BlueprintFormat.TAG_WATER_SURFACE) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: true);
else if (tag == BlueprintFormat.TAG_WATER_BODY_TABLE) sectionOk = ParseWaterBodyTable(reader, blueprint);
else if (tag == BlueprintFormat.TAG_TERRAIN_CURVE) sectionOk = ParseTerrainCurve(reader, blueprint);
else
{
// The property the redesign exists to buy: future sections (water,
@ -366,6 +384,21 @@ namespace IslaApocalypse.Core
return true;
}
private static bool ParseTerrainCurve(BinaryReader reader, WorldBlueprint blueprint)
{
var c = new TerrainCurveInfo();
c.Version = reader.ReadUInt16();
c.T1 = reader.ReadSingle(); c.T2 = reader.ReadSingle();
c.T3 = reader.ReadSingle(); c.T4 = reader.ReadSingle();
c.HMaxCal = reader.ReadSingle();
c.Sea = reader.ReadSingle(); c.OrangeCeil = reader.ReadSingle();
c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle();
c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle();
c.TailSlope = reader.ReadSingle();
blueprint.TerrainCurve = c;
return true;
}
private static bool ParseRoadTier(BinaryReader reader, List<Vector2[]> into)
{
int pathCount = reader.ReadInt32();