using System; using System.Collections.Generic; namespace IslaApocalypse.Core { /// /// 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. /// public enum RunOrigin : byte { /// Produced by world generation — stratigraphy, or a 3D feature pass. Natural = 0, /// Placed by a player. PlayerPlaced = 1, } /// /// 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 /// by lookup on . 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`. /// /// is not an exception to that rule: it is not a property OF the material, /// it is a property of THIS RUN. /// public readonly struct MaterialRun { /// The material's registry key. → . public readonly MaterialKey Material; /// Thickness in voxels, bottom to top. Strictly positive. public readonly int Thickness; /// Provenance. Decides mesh style; see . 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})"; } /// /// ★★ 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. /// /// /// 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 /// /// /// ═══ 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 (). /// /// 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. /// public sealed class Column { private readonly List _runs; public Column(int expectedRuns = 8) { _runs = new List(expectedRuns); } /// The runs, bottom to top. Read-only: mutation goes through . public IReadOnlyList Runs => _runs; public int RunCount => _runs.Count; /// /// 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. /// public void Append(MaterialRun run) { _runs.Add(run); } /// Append a run by its parts. Convenience over . public void Append(MaterialKey material, int thickness, RunOrigin origin = RunOrigin.Natural) => Append(new MaterialRun(material, thickness, origin)); /// /// 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. /// 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)"; } }