feat: curve v5 — corner fixes + COMPACT/BALANCED taste presets (terrain-water task 09)

The three corner fixes (both presets): riser endpoint slope floors
0.2 -> 0.1 (0.1u + 0.9*smoothstep — climbs decelerate into shelves and
accelerate out); summit-spike base floor 0.1 -> 0.05
(0.05u + 0.95*u^4 — no hard skirt under the peaks); SHELF_SPAN_MIN
2 m -> 6 m (pronounced shelves keep a gentle tilt).

Knots are preset-parameterized (CurveKnots, explicit parameter — pure,
D-035), calibrated from the same pooled batch-04 CDF:
COMPACT  P65/77/84/88/94/97 -> 65/12/7/4/6/3/3 land split
BALANCED P60/73/83/88/96/99 -> 60/13/10/5/8/3/1
Gate: "off"|"v5-compact"|"v5-balanced" (batch-only tri-state;
interim default v5-balanced pending the developer's verdict — the
winner becomes plain "v5" in a follow-up commit); v1-v4 retired
loudly. Assertion runs per preset (8 corners x per-seed spikeMax; the
lowered floors stay strictly positive). TCRV v5 extension (+9 B):
preset id + K5/K6 — the effective curve is unambiguous from the
record; old records parse (version-dispatched); harness compares.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stewart Howe 2026-08-08 04:11:53 -04:00
parent 68f7bca8e0
commit 02577872f2
6 changed files with 137 additions and 101 deletions

View file

@ -142,6 +142,13 @@ namespace IslaApocalypse.Core
writer.Write(c.BenchSeedOffset); writer.Write(c.PlateauSeedOffset);
writer.Write(c.StrengthSeedOffset);
}
// v5+ extension: knot preset id + K5/K6.
if (c.Version >= 5)
{
writer.Write(c.PresetId);
writer.Write(c.K5); writer.Write(c.K6);
}
}
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)

View file

@ -26,12 +26,13 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
public static string SeaLevelModel = "flat";
public static float SeaLevelValue = 0.15f;
// Height-redistribution curve (tasks 0508, graduation M-7): "v4" applies the
// terraced-ascent curve with spatially modulated shelves and the per-seed peak
// spike (HeightCurve.cs); "off" is the raw legacy profile. "v1""v3" were
// retired by successor recalibrations (old blueprints are regenerable). Biome
// classification is curve-invariant by construction either way. Default: v4.
public static string TerrainCurve = "v4";
// Height-redistribution curve (tasks 0509, graduation M-7): the task-09 taste
// batch runs a tri-state — "v5-compact" (maximum lowland) vs "v5-balanced"
// (task-07/08 lineage), both with the corner fixes; "off" is the raw legacy
// profile. The loser retires after the developer's gate and the winner becomes
// plain "v5". "v1""v4" retired. INTERIM default pending the verdict:
// v5-balanced.
public static string TerrainCurve = "v5-balanced";
public static void LoadConfig()
{
@ -106,10 +107,10 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
if (data.ContainsKey("TerrainCurve"))
{
string curve = (string)data["TerrainCurve"];
if (curve == "off" || curve == "v4")
if (curve == "off" || curve == "v5-compact" || curve == "v5-balanced")
TerrainCurve = curve;
else if (curve == "v1" || curve == "v2" || curve == "v3")
GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v4, task 08). Keeping '{TerrainCurve}' — use \"v4\" or \"off\".");
else if (curve == "v1" || curve == "v2" || curve == "v3" || curve == "v4")
GD.PrintErr($"[ConfigManager] TerrainCurve '{curve}' was retired by a later recalibration (v5, task 09). Keeping '{TerrainCurve}' — use \"v5-compact\", \"v5-balanced\" or \"off\".");
else
GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
}

View file

@ -80,6 +80,11 @@ namespace IslaApocalypse.Core
public float BenchAmp, PlateauAmp, ShelfSpanMin, ShelfSpanMax;
public float ElevFreqIslands, StrengthFreqIslands;
public int BenchSeedOffset, PlateauSeedOffset, StrengthSeedOffset;
// v5 extension — knot preset (1 = compact, 2 = balanced) and the two knots the
// four base slots cannot carry; with these the effective curve is unambiguous.
public byte PresetId;
public float K5, K6;
}
public class WorldBlueprint
@ -416,6 +421,11 @@ namespace IslaApocalypse.Core
c.BenchSeedOffset = reader.ReadInt32(); c.PlateauSeedOffset = reader.ReadInt32();
c.StrengthSeedOffset = reader.ReadInt32();
}
if (c.Version >= 5)
{
c.PresetId = reader.ReadByte();
c.K5 = reader.ReadSingle(); c.K6 = reader.ReadSingle();
}
blueprint.TerrainCurve = c;
return true;
}

View file

@ -1,146 +1,151 @@
using Godot;
/// <summary>
/// The height-redistribution curve, v4 — SPATIALLY MODULATED SHELVES (terrain-water
/// task 08). Pure, static, monotonic piecewise map; every per-column input is an
/// explicit PARAMETER (D-035): the seed spike max and the four modulated shelf
/// values. The generator samples the modulation fields; this class never touches
/// noise.
/// One preset's input knots for the v5 curve. Two presets exist for the task-09
/// taste batch — COMPACT (maximum lowland, cordillera-from-plains) and BALANCED
/// (the task-07/08 lineage, gradual highland approach). The loser retires after
/// the developer's gate; the winner becomes plain "v5".
/// </summary>
public sealed class CurveKnots
{
public readonly byte PresetId;
public readonly string Name;
public readonly float K1, K2, K3, K4, K5, K6;
public CurveKnots(byte id, string name, float k1, float k2, float k3, float k4, float k5, float k6)
{
PresetId = id; Name = name;
K1 = k1; K2 = k2; K3 = k3; K4 = k4; K5 = k5; K6 = k6;
}
}
/// <summary>
/// The height-redistribution curve, v5 (terrain-water task 09) — v4's spatially
/// modulated shelves plus the THREE CORNER FIXES, with PRESET-PARAMETERIZED knots.
/// Pure, static, monotonic; every per-column input and the knot set are explicit
/// PARAMETERS (D-035).
///
/// v4 (developer's task-07 gate finding: the terraces work, but uniform anchors put
/// a flat ring at exactly 100 m and exactly 220 m on every mountain — the bathtub
/// rings): the bench and plateau OUTPUT anchors become smooth spatial fields —
/// benchLo = 100 m ± 12 m and plateauLo = 220 m ± 20 m via two decorrelated
/// very-low-frequency noise fields — and a third field modulates SHELF STRENGTH,
/// blending each shelf between "pronounced flat" (output span ~2 m) and "barely a
/// hint" (~25 m of gentle slope), so not every flank at shelf height develops the
/// full terrace. Shelves stay locally flat; the two magic altitudes stop existing.
/// The corner fixes (both presets — the naturalness work):
/// 1. Riser endpoint slope floor 0.2 → 0.1 (blend 0.1u + 0.9·smoothstep): climbs
/// decelerate into shelves and accelerate out of them — no machined edges.
/// 2. Summit-spike base floor 0.1 → 0.05 (blend 0.05u + 0.95·u⁴): the spike
/// leaves the plateau gently — no hard skirt under the peaks.
/// 3. SHELF_SPAN_MIN 2 m → 6 m: pronounced shelves keep a gentle tilt — flat to
/// build on, never snooker-table flat.
///
/// Structure otherwise v3's, unchanged: identity ≤ sea, frozen toe/rise (storm
/// ladder), foothill riser → bench → mid riser → plateau → per-seed-normalized
/// stiff spike (u⁴, 420 m cap) → tail. Input knots are v3's calibration.
/// Presets (input land-fraction targets; knots calibrated 2026-08-08 from the same
/// pooled batch-04 flat-sea land CDF as tasks 05/07, 340,618,126 samples; achieved
/// fractions exact by construction):
/// COMPACT — 65/12/7/4/6/3/3 (orange/red/foothill-riser/bench/mid-riser/plateau/spike)
/// BALANCED — 60/13/10/5/8/3/1
///
/// ORDERING SAFETY BY CONSTRUCTION (asserted): with the amplitudes below, at every
/// column: red ceiling 0.27 < benchLo… (bench min 0.5006), bench top max 0.6962 <
/// plateauLo min 0.9468, plateau top max 1.2062 < peak cap 1.8287. The numeric
/// assertion additionally sweeps all 8 modulation-extreme corners per generation.
/// Unchanged from v4: storm-ladder anchors, bench 100±12 m, plateau 220±20 m,
/// strength modulation (span max 25 m), 420 m cap, per-seed spike normalization,
/// modulation fields/seed offsets, the classify-map invariant.
/// </summary>
public static class HeightCurve
{
public const ushort VERSION = 4;
public const ushort VERSION = 5;
// Input knots — v3 calibration (pooled batch-04 land CDF, P59/72/82/87/95/98).
public const float K1 = 0.509179f;
public const float K2 = 0.604081f;
public const float K3 = 0.698485f;
public const float K4 = 0.767213f;
public const float K5 = 0.930304f;
public const float K6 = 1.050720f;
public static readonly CurveKnots COMPACT = new CurveKnots(1, "compact",
0.550982f, 0.646528f, 0.723145f, 0.784045f, 0.903025f, 1.003079f); // P65/77/84/88/94/97
public static readonly CurveKnots BALANCED = new CurveKnots(2, "balanced",
0.515899f, 0.612157f, 0.710472f, 0.784045f, 0.962922f, 1.119118f); // P60/73/83/88/96/99
// Fixed output anchors — storm ladder + ceiling (frozen).
public const float SEA = 0.15f;
public const float ORANGE_CEIL = 0.206f;
public const float RED_CEIL = 0.27f;
public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869
public const float PEAK_CAP = SEA + 420f / 251f;
public const float TAIL_SLOPE = 0.25f;
public const float SPIKE_MIN_SPAN = 0.01f;
// Modulated shelf anchors: base ± amplitude (raw units; 251 m per unit).
public const float BENCH_BASE = SEA + 100f / 251f; // ≈ 0.54841 (100 m)
public const float BENCH_AMP = 12f / 251f; // ± 12 m
public const float PLATEAU_BASE = SEA + 220f / 251f; // ≈ 1.02649 (220 m)
public const float PLATEAU_AMP = 20f / 251f; // ± 20 m
// Modulated shelf anchors (unchanged from v4).
public const float BENCH_BASE = SEA + 100f / 251f;
public const float BENCH_AMP = 12f / 251f;
public const float PLATEAU_BASE = SEA + 220f / 251f;
public const float PLATEAU_AMP = 20f / 251f;
// Shelf strength: output span of each shelf segment, blended by the strength
// field. Strong (t=1) → SPAN_MIN (~2 m, pronounced flat). Weak (t=0) →
// SPAN_MAX (~25 m, barely a hint of a shelf).
public const float SHELF_SPAN_MIN = 0.008f;
// Corner fix 3: pronounced shelves keep ~6 m of tilt across the shelf band.
public const float SHELF_SPAN_MIN = 6f / 251f; // ≈ 0.0239 (v4: 0.008 ≈ 2 m)
public const float SHELF_SPAN_MAX = 0.10f;
// Modulation-field derivation (generator-side, recorded in TCRV): field seed =
// RESOLVED WorldSeed + offset; frequency = (periods per island width) / MapSize.
// Modulation-field derivation (unchanged from v4).
public const int BENCH_SEED_OFFSET = 7101;
public const int PLATEAU_SEED_OFFSET = 7207;
public const int STRENGTH_SEED_OFFSET = 7303;
public const float ELEV_FREQ_ISLANDS = 3.0f; // ~3 undulations across the island
public const float STRENGTH_FREQ_ISLANDS = 5.0f; // finer patchiness for shelf strength
public const float ELEV_FREQ_ISLANDS = 3.0f;
public const float STRENGTH_FREQ_ISLANDS = 5.0f;
public static float EffectiveSpikeMax(float hMaxSeed)
public static float EffectiveSpikeMax(float hMaxSeed, CurveKnots k)
{
return Mathf.Max(hMaxSeed, K6 + SPIKE_MIN_SPAN);
return Mathf.Max(hMaxSeed, k.K6 + SPIKE_MIN_SPAN);
}
/// <summary>Shelf output span for a strength sample t ∈ [0,1].</summary>
public static float ShelfSpan(float strength01)
{
return Mathf.Lerp(SHELF_SPAN_MAX, SHELF_SPAN_MIN, Mathf.Clamp(strength01, 0f, 1f));
}
/// <param name="h">Raw pre-curve height.</param>
/// <param name="hMaxSeed">Seed's raw pre-curve maximum (per-seed spike normalizer).</param>
/// <param name="benchLo">This column's bench anchor (BENCH_BASE ± BENCH_AMP).</param>
/// <param name="benchSpan">This column's bench output span (ShelfSpan of the strength field).</param>
/// <param name="plateauLo">This column's plateau anchor (PLATEAU_BASE ± PLATEAU_AMP).</param>
/// <param name="plateauSpan">This column's plateau output span.</param>
public static float Apply(float h, float hMaxSeed,
float benchLo, float benchSpan, float plateauLo, float plateauSpan)
float benchLo, float benchSpan, float plateauLo, float plateauSpan, CurveKnots k)
{
if (h <= SEA) return h;
float u, s;
if (h < K1)
if (h < k.K1)
{
u = (h - SEA) / (K1 - SEA);
u = (h - SEA) / (k.K1 - SEA);
s = 0.3f * u + 0.7f * (u * (2f - u)); // frozen ease-out toe
return SEA + s * (ORANGE_CEIL - SEA);
}
if (h < K2)
if (h < k.K2)
{
u = (h - K1) / (K2 - K1);
u = (h - k.K1) / (k.K2 - k.K1);
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // frozen linear rise
}
if (h < K3)
if (h < k.K3)
{
u = (h - K2) / (K3 - K2);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // foothill riser
u = (h - k.K2) / (k.K3 - k.K2);
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // foothill riser — corner fix 1
return RED_CEIL + s * (benchLo - RED_CEIL);
}
if (h < K4)
if (h < k.K4)
{
u = (h - K3) / (K4 - K3);
return benchLo + u * benchSpan; // bench — modulated
u = (h - k.K3) / (k.K4 - k.K3);
return benchLo + u * benchSpan; // bench (min span 6 m — fix 3)
}
float benchTop = benchLo + benchSpan;
if (h < K5)
if (h < k.K5)
{
u = (h - K4) / (K5 - K4);
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // mid riser
u = (h - k.K4) / (k.K5 - k.K4);
s = 0.1f * u + 0.9f * (u * u * (3f - 2f * u)); // mid riser — corner fix 1
return benchTop + s * (plateauLo - benchTop);
}
if (h < K6)
if (h < k.K6)
{
u = (h - K5) / (K6 - K5);
return plateauLo + u * plateauSpan; // plateau — modulated
u = (h - k.K5) / (k.K6 - k.K5);
return plateauLo + u * plateauSpan; // plateau
}
float plateauTop = plateauLo + plateauSpan;
float spikeMax = EffectiveSpikeMax(hMaxSeed);
float spikeMax = EffectiveSpikeMax(hMaxSeed, k);
if (h < spikeMax)
{
u = (h - K6) / (spikeMax - K6);
s = 0.1f * u + 0.9f * (u * u * u * u); // summit spike (v2/v3 shape)
u = (h - k.K6) / (spikeMax - k.K6);
s = 0.05f * u + 0.95f * (u * u * u * u); // summit spike — corner fix 2
return plateauTop + s * (PEAK_CAP - plateauTop);
}
return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
}
/// <summary>
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve:
/// sweeps the full domain at every one of the 8 modulation-extreme corners
/// (bench anchor ±, plateau anchor ±, strength min/max) with the per-seed
/// spikeMax — the adversarial corner set for the ordering constraints. Loud
/// throw, refuses to generate.
/// Per-generation numeric strict-monotonicity check of the EFFECTIVE curve for
/// the selected preset: all 8 modulation-extreme corners × per-seed spikeMax.
/// The corner fixes lower the slope floors (risers 0.1, spike base 0.05) — the
/// sweep proves they stay strictly positive everywhere. Loud throw on failure.
/// </summary>
public static void AssertMonotonic(float hMaxSeed)
public static void AssertMonotonic(float hMaxSeed, CurveKnots k)
{
float[] benchLos = { BENCH_BASE - BENCH_AMP, BENCH_BASE + BENCH_AMP };
float[] plateauLos = { PLATEAU_BASE - PLATEAU_AMP, PLATEAU_BASE + PLATEAU_AMP };
@ -153,27 +158,27 @@ public static class HeightCurve
foreach (float sp in spans)
{
float prevH = -7f;
float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp);
float prev = Apply(prevH, hMaxSeed, bl, sp, pl, sp, k);
void Check(double hd)
{
float h = (float)hd;
if (h <= prevH) return; // dedupe float32 samples (task-05 fix)
float v = Apply(h, hMaxSeed, bl, sp, pl, sp);
float v = Apply(h, hMaxSeed, bl, sp, pl, sp, k);
if (v <= prev)
throw new System.InvalidOperationException(
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate.");
$"[HeightCurve] MONOTONICITY VIOLATION at h={h} (preset {k.Name}, hMaxSeed={hMaxSeed}, benchLo={bl}, plateauLo={pl}, span={sp}): {v} <= {prev}. Refusing to generate.");
prev = v;
prevH = h;
}
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed, k) + 0.5);
for (double hh = -7.0 + 0.01; hh < 0.10; hh += 0.01) Check(hh);
for (double hh = 0.10; hh <= top; hh += 0.0001) Check(hh);
for (double hh = top + 0.05; hh <= top + 6.0; hh += 0.05) Check(hh);
}
}
}
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, 8 modulation corners, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION} preset '{k.Name}', 8 modulation corners, effective spikeMax {EffectiveSpikeMax(hMaxSeed, k):F6}).");
}
}

View file

@ -46,6 +46,10 @@ public partial class MapGenerator : TextureRect
private FastNoiseLite _plateauNoise;
private FastNoiseLite _strengthNoise;
// v5: the selected knot preset (task-09 taste batch: COMPACT or BALANCED);
// null = curve off.
private CurveKnots _curveKnots;
// The seed's raw pre-curve height maximum (post noise/falloff/Trench/spine,
// pre-carve) — the v2 curve's per-seed spike normalizer. Computed in
// GenerateTopography pass 1; recorded in TCRV (effective, guard applied).
@ -84,7 +88,10 @@ public partial class MapGenerator : TextureRect
this.CustomMinimumSize = new Vector2(MapSize, MapSize);
_heightMap = new float[MapSize, MapSize];
_curveOn = ConfigManager.TerrainCurve == "v4";
_curveKnots = ConfigManager.TerrainCurve == "v5-compact" ? HeightCurve.COMPACT
: ConfigManager.TerrainCurve == "v5-balanced" ? HeightCurve.BALANCED
: null;
_curveOn = _curveKnots != null;
// (The monotonicity assertion now runs inside GenerateTopography, against the
// effective per-seed curve, once hMaxSeed is known.)
_heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap;
@ -276,11 +283,14 @@ public partial class MapGenerator : TextureRect
// the bench slots carry the two BASE anchors; the v4 extension record carries
// the modulation parameters (amplitudes, spans, frequencies, seed offsets) —
// the blueprint stays self-describing.
// TCRV v5: knot slots carry the PRESET's K1..K4; the extension carries the
// modulation params plus preset id and K5/K6 — the effective curve is
// unambiguous from the record alone.
TerrainCurve = _curveOn ? new TerrainCurveInfo
{
Version = HeightCurve.VERSION,
T1 = HeightCurve.K1, T2 = HeightCurve.K2, T3 = HeightCurve.K3, T4 = HeightCurve.K4,
SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed), // per-seed
T1 = _curveKnots.K1, T2 = _curveKnots.K2, T3 = _curveKnots.K3, T4 = _curveKnots.K4,
SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed, _curveKnots), // per-seed
Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL,
PlateauLo = HeightCurve.BENCH_BASE, PlateauHi = HeightCurve.PLATEAU_BASE,
PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE,
@ -288,7 +298,8 @@ public partial class MapGenerator : TextureRect
ShelfSpanMin = HeightCurve.SHELF_SPAN_MIN, ShelfSpanMax = HeightCurve.SHELF_SPAN_MAX,
ElevFreqIslands = HeightCurve.ELEV_FREQ_ISLANDS, StrengthFreqIslands = HeightCurve.STRENGTH_FREQ_ISLANDS,
BenchSeedOffset = HeightCurve.BENCH_SEED_OFFSET, PlateauSeedOffset = HeightCurve.PLATEAU_SEED_OFFSET,
StrengthSeedOffset = HeightCurve.STRENGTH_SEED_OFFSET
StrengthSeedOffset = HeightCurve.STRENGTH_SEED_OFFSET,
PresetId = _curveKnots.PresetId, K5 = _curveKnots.K5, K6 = _curveKnots.K6
} : null,
FormatVersion = 2,
Params = new BlueprintParams
@ -445,7 +456,7 @@ public partial class MapGenerator : TextureRect
// The v2 curve is SEED-DEPENDENT: its spike maps [t4, hMaxSeed] onto the peak
// band, so the monotonicity assertion must run against the EFFECTIVE per-seed
// curve — after hMaxSeed is known, before any pixel is curved.
if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed);
if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed, _curveKnots);
// --- PASS 2: curve (task 05/06) + crater carve ---
// Curve applied AFTER noise + falloff + Trench, BEFORE the crater carve, so
@ -469,7 +480,7 @@ public partial class MapGenerator : TextureRect
float benchLo = HeightCurve.BENCH_BASE + _benchNoise.GetNoise2D(x, y) * HeightCurve.BENCH_AMP;
float plateauLo = HeightCurve.PLATEAU_BASE + _plateauNoise.GetNoise2D(x, y) * HeightCurve.PLATEAU_AMP;
float shelfSpan = HeightCurve.ShelfSpan((_strengthNoise.GetNoise2D(x, y) + 1f) * 0.5f);
curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan);
curvedH = HeightCurve.Apply(raw, _hMaxSeed, benchLo, shelfSpan, plateauLo, shelfSpan, _curveKnots);
}
else
{

View file

@ -202,10 +202,12 @@ public partial class RoundTripHarness : Node
bool same = ca.Version == cb.Version;
float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope,
ca.BenchAmp, ca.PlateauAmp, ca.ShelfSpanMin, ca.ShelfSpanMax, ca.ElevFreqIslands, ca.StrengthFreqIslands,
ca.BenchSeedOffset, ca.PlateauSeedOffset, ca.StrengthSeedOffset };
ca.BenchSeedOffset, ca.PlateauSeedOffset, ca.StrengthSeedOffset,
ca.PresetId, ca.K5, ca.K6 };
float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope,
cb.BenchAmp, cb.PlateauAmp, cb.ShelfSpanMin, cb.ShelfSpanMax, cb.ElevFreqIslands, cb.StrengthFreqIslands,
cb.BenchSeedOffset, cb.PlateauSeedOffset, cb.StrengthSeedOffset };
cb.BenchSeedOffset, cb.PlateauSeedOffset, cb.StrengthSeedOffset,
cb.PresetId, cb.K5, cb.K6 };
for (int i = 0; i < fa.Length; i++)
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }