Stands up the CODE_REPO the rewrite is written into (D-049, D-058). Godot 4.7.2 /
.NET 8 / Godot.NET.Sdk 4.7.2. Nothing is generated, meshed, or ported — this is the
shell and the two data contracts.
Four layers, each with its boundary stated in a directory README:
Core/ math + data only, and engine-free — depends on nothing above it
Server/ authoritative logic (empty this phase)
Client/ rendering (empty this phase)
Tools/ the offline generator — may NOT reference Client (Phase 1 fills it)
Contracts (compiling stubs, no algorithms):
- Column model (D-053): a 2D grid of columns, each a stack of (material, thickness)
runs, any material at any depth. Air is the TOP RUN — there is no air-vs-solid
height branch, and no surface-height accessor exists to reintroduce one. Water is
not a band; it stays an overlay.
- Material schemas (D-054): terrain and building as two append-only registries,
bridged by a recipe seam that is deliberately EMPTY. Identity is a registry key,
never a serialized ordinal. Mesh style is per-ORIGIN and is not a material field.
Rails, so Phase 1 inherits them rather than rediscovering them:
- GenerationScale: MapSize is a parameter, everything derives from it, nothing uses
a raw pixel number. Tightening over the reference — decorrelation offsets are
declared in map widths, not pixels (chat1/00 §4.2).
- ToolingPaths: config/blueprint/output paths all env-overridable, resolved in one
place, so a batch cannot touch the developer's live files.
- FileSafety: the permanent no-deletion rules as throws rather than sentences.
user:// isolation: project name and use_custom_user_dir both pin the runtime dir to
~/.local/share/islaApocalypse-v2/, away from the old prototype's preserved seeds and
batches. Tools/Scenes/UserDirProbe.tscn confirms it rather than assuming it.
170 lines
7.3 KiB
C#
170 lines
7.3 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace IslaApocalypse.Core
|
||
{
|
||
/// <summary>
|
||
/// Where a run came FROM. Not a material property — a fact about provenance.
|
||
///
|
||
/// ⭐ THIS, AND ONLY THIS, DECIDES MESH STYLE: natural runs mesh smooth (SN/DC), player-placed
|
||
/// runs mesh as cubes, and the seam-blend handles the join. The same material can be both —
|
||
/// which is exactly why mesh style is NOT a field on the material row.
|
||
/// → D-054 ("material does not override mesh style"), `Design - Rendering - Mesher.md`, D-052.
|
||
///
|
||
/// No mesher exists this phase. The flag exists so the mesher has something true to read.
|
||
/// </summary>
|
||
public enum RunOrigin : byte
|
||
{
|
||
/// <summary>Produced by world generation — stratigraphy, or a 3D feature pass.</summary>
|
||
Natural = 0,
|
||
|
||
/// <summary>Placed by a player.</summary>
|
||
PlayerPlaced = 1,
|
||
}
|
||
|
||
/// <summary>
|
||
/// One run-length band in a column: a material, a thickness, and where it came from.
|
||
/// → `Design - Data - Column Model.md`, D-053.
|
||
///
|
||
/// ⚠ A RUN STORES A MATERIAL IDENTITY AND NOTHING ELSE ABOUT THE MATERIAL.
|
||
/// Hardness, yield, support strength — every property — comes from
|
||
/// <see cref="MaterialRegistry"/> by lookup on <see cref="Material"/>. Nothing is stored
|
||
/// per-run. That is properties-from-type, and it is what makes materials, support and
|
||
/// mod-ability clean. → `Design - Data - Material Schema.md`.
|
||
///
|
||
/// <see cref="Origin"/> is not an exception to that rule: it is not a property OF the material,
|
||
/// it is a property of THIS RUN.
|
||
/// </summary>
|
||
public readonly struct MaterialRun
|
||
{
|
||
/// <summary>The material's registry key. → <see cref="MaterialKey"/>.</summary>
|
||
public readonly MaterialKey Material;
|
||
|
||
/// <summary>Thickness in voxels, bottom to top. Strictly positive.</summary>
|
||
public readonly int Thickness;
|
||
|
||
/// <summary>Provenance. Decides mesh style; see <see cref="RunOrigin"/>.</summary>
|
||
public readonly RunOrigin Origin;
|
||
|
||
public MaterialRun(MaterialKey material, int thickness, RunOrigin origin = RunOrigin.Natural)
|
||
{
|
||
if (material.IsNone)
|
||
throw new ArgumentException("A run must name a material. Air is a material (core.air), not an absence.", nameof(material));
|
||
if (thickness <= 0)
|
||
throw new ArgumentOutOfRangeException(nameof(thickness), thickness, "A run's thickness must be strictly positive. A zero-thickness run is not a thing; omit it.");
|
||
|
||
Material = material;
|
||
Thickness = thickness;
|
||
Origin = origin;
|
||
}
|
||
|
||
public override string ToString() => $"{Material} x{Thickness} ({Origin})";
|
||
}
|
||
|
||
/// <summary>
|
||
/// ★★ THE LOAD-BEARING CONTRACT — one horizontal cell of the world, as a vertical stack of
|
||
/// run-length bands, bottom to top. → `Design - Data - Column Model.md`, D-053.
|
||
///
|
||
/// <code>
|
||
/// bedrock 0–50
|
||
/// limestone 50–60
|
||
/// air 60–70 ← a cave: an air run mid-column
|
||
/// limestone 70–84
|
||
/// iron 84–86 ← an ore vein: a material run inside limestone
|
||
/// limestone 86–90
|
||
/// clay 90–98
|
||
/// topsoil 98–100
|
||
/// air 100+ ← everything above the surface is simply the top air run
|
||
/// </code>
|
||
///
|
||
/// ═══ TWO GENERALIZATIONS THIS SHAPE BUYS ═══
|
||
///
|
||
/// 1. ANY MATERIAL AT ANY DEPTH — not "one material per depth zone". A run can be any material
|
||
/// at any position, which is what lets 3D feature passes write into columns: a cave is an
|
||
/// AIR run mid-stack, an ore vein is an ore run inside rock. No cave or ore code exists this
|
||
/// phase; the SHAPE that makes them trivial does.
|
||
///
|
||
/// 2. SURFACE MATERIAL IS CLIMATE-DRIVEN, not a fixed top layer. The topmost solid run's
|
||
/// material is f(elevation, temperature, precipitation) from the 2D maps, so snow caps and
|
||
/// bare peaks fall out for free. ⭐ Snow is a surface MATERIAL, not a biome — biomes are a
|
||
/// separate classification layer. No stratigraphy exists this phase.
|
||
///
|
||
/// ═══ ⚠⚠ THE BEHAVIOURAL RULE — THE ONE THIS TYPE EXISTS TO ENFORCE ═══
|
||
///
|
||
/// NEVER BRANCH ON "AIR VS SOLID" FROM HEIGHT AS A SPECIAL CASE.
|
||
///
|
||
/// The prototype's mistake began exactly there — panicking about "is this air or solid, based
|
||
/// on the heightmap". In the band model there is NO height-triggered branching: everything
|
||
/// above the surface is simply the top AIR run. Air is not a special height rule; it is just
|
||
/// the topmost band, a registry row like any other (<see cref="MaterialRegistry.Air"/>).
|
||
///
|
||
/// This type deliberately exposes NO surface-height, ground-level, or is-solid-at accessor.
|
||
/// Every one of those is the old mistake wearing a helper's clothes. If you find yourself
|
||
/// wanting one, you are about to reintroduce the bug this model was built to make impossible.
|
||
///
|
||
/// ═══ ⚠ WHAT IS NOT HERE, AND MUST NOT BE ═══
|
||
///
|
||
/// • WATER IS NOT A BAND. Water is a separate OVERLAY over the columns — the levels-not-cells
|
||
/// model (sea, lakes, rivers). It is never stored as a run. Nothing about water belongs in
|
||
/// this type. → `Design - Water - Runtime Water.md`, `Design - Water - Sea Level Model.md`.
|
||
///
|
||
/// • NO PER-RUN PROPERTIES. Runs store identity; the registry answers everything else.
|
||
///
|
||
/// • NO ALGORITHMS. Phase 0 is the shape only. Run splitting on dig, merging, stability
|
||
/// storage, chunking for streaming, and the mesher are all later phases. The minimal API
|
||
/// below — construct, append, iterate — is the whole surface on purpose.
|
||
///
|
||
/// ⚠ A LIVE INVARIANT constrains how the shaping/hydrology/water passes use this model (D-046):
|
||
/// classify-height and render-height must stay consistent. Its home is
|
||
/// `Design - Pipeline - Generation Order.md` § LIVE INVARIANT — read it there before writing
|
||
/// anything that reads or writes columns from both height views.
|
||
/// </summary>
|
||
public sealed class Column
|
||
{
|
||
private readonly List<MaterialRun> _runs;
|
||
|
||
public Column(int expectedRuns = 8)
|
||
{
|
||
_runs = new List<MaterialRun>(expectedRuns);
|
||
}
|
||
|
||
/// <summary>The runs, bottom to top. Read-only: mutation goes through <see cref="Append"/>.</summary>
|
||
public IReadOnlyList<MaterialRun> Runs => _runs;
|
||
|
||
public int RunCount => _runs.Count;
|
||
|
||
/// <summary>
|
||
/// Append a run ON TOP of the stack. Runs are built bottom-up, in order.
|
||
///
|
||
/// No merging of like-material neighbours, no splitting, no insertion mid-stack. Those are
|
||
/// later-phase operations with their own correctness questions; this phase builds the shape.
|
||
/// </summary>
|
||
public void Append(MaterialRun run)
|
||
{
|
||
_runs.Add(run);
|
||
}
|
||
|
||
/// <summary>Append a run by its parts. Convenience over <see cref="Append(MaterialRun)"/>.</summary>
|
||
public void Append(MaterialKey material, int thickness, RunOrigin origin = RunOrigin.Natural)
|
||
=> Append(new MaterialRun(material, thickness, origin));
|
||
|
||
/// <summary>
|
||
/// Total stacked thickness of every run, including air.
|
||
///
|
||
/// ⚠ THIS IS NOT A SURFACE HEIGHT AND MUST NOT BE USED AS ONE. It is the top of the column
|
||
/// including its air run. There is deliberately no "height of the solid part" here — see
|
||
/// the behavioural rule in this type's summary.
|
||
/// </summary>
|
||
public int TotalThickness
|
||
{
|
||
get
|
||
{
|
||
int t = 0;
|
||
for (int i = 0; i < _runs.Count; i++) t += _runs[i].Thickness;
|
||
return t;
|
||
}
|
||
}
|
||
|
||
public override string ToString() => $"Column({_runs.Count} runs, {TotalThickness} thick)";
|
||
}
|
||
}
|