Phase 0: clean project skeleton for the v2 rewrite
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.
This commit is contained in:
commit
9107b0822b
36 changed files with 1453 additions and 0 deletions
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.svg text
|
||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# ---- Godot 4.x ------------------------------------------------------------
|
||||
# The import/script cache. Regenerated on import; never committed.
|
||||
.godot/
|
||||
# Exported builds.
|
||||
/build/
|
||||
/export/
|
||||
*.translation
|
||||
|
||||
# ---- .NET / C# ------------------------------------------------------------
|
||||
bin/
|
||||
obj/
|
||||
.mono/
|
||||
*.user
|
||||
*.userprefs
|
||||
*.suo
|
||||
*.swp
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
msbuild.binlog
|
||||
|
||||
# ---- OS / editor ----------------------------------------------------------
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
# ---- ⚠ NEVER COMMIT GENERATED WORLD DATA ---------------------------------
|
||||
# Blueprints, batches and snapshots are OUTPUT — large, regenerable, and they live under the
|
||||
# project's user:// directory, not here. If any of these ever appear inside the repo, something
|
||||
# resolved a path wrong; fix the path, do not commit the file.
|
||||
# → Core/Scripts/ToolingPaths.cs, Tools/README.md
|
||||
*.blueprint
|
||||
batches/*/
|
||||
!batches/README.md
|
||||
|
||||
# Godot's C# project editor rewrites the .csproj on import and leaves a backup beside it.
|
||||
# An artifact of the editor, not a source file. (Not deleted — file-safety rule: no deletions.)
|
||||
*.csproj.old
|
||||
22
Client/README.md
Normal file
22
Client/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Client — rendering and presentation
|
||||
|
||||
**Holds:** rendering, meshing, UI, input, everything the player sees.
|
||||
|
||||
**Boundary:** may use `Core/`. Must not depend on `Server/` internals.
|
||||
|
||||
> ### ⚠ Nothing in `Tools/` may reference this folder. The wall runs one way.
|
||||
> → `Tools/README.md`, `Design - Tooling - Offline Generation.md`.
|
||||
|
||||
## Empty this phase — deliberately
|
||||
|
||||
Phase 0 has no renderer. The mesher (SN/DC smooth terrain + greedy-cube builds with seam-blend,
|
||||
→ D-052, `Design - Rendering - Mesher.md`) is a much later phase, and building it before the data is
|
||||
right is explicitly the trap the rewrite exists to undo (D-056: *"the data is perfected before the
|
||||
mesher"*).
|
||||
|
||||
## One thing already decided, so it is not re-litigated later
|
||||
|
||||
**Mesh style is per-ORIGIN, not per-material.** Natural runs mesh smooth, player-placed runs mesh as
|
||||
cubes, and the same material can be both. The flag the mesher reads is `RunOrigin` on the run — there
|
||||
is no mesh-style field on any material row, and adding one would be a design regression.
|
||||
→ `Core/Scripts/Column.cs`, D-054.
|
||||
1
Client/Scripts/.gitkeep
Normal file
1
Client/Scripts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Placeholder so the empty layer folder is tracked. Delete when the first script lands.
|
||||
55
Core/README.md
Normal file
55
Core/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Core — math and data only
|
||||
|
||||
**Holds:** the column model, the material schemas, the scaling discipline, tooling path
|
||||
resolution and the file-safety rails. Constants and contracts.
|
||||
|
||||
**Boundary: Core depends on nothing above it.** Not on `Server/`, not on `Client/`, not on
|
||||
`Tools/`. The dependency arrow points one way into this folder and never out of it.
|
||||
|
||||
> ### ⚠ Additional boundary, tighter than the layer rule: **Core is engine-free.**
|
||||
>
|
||||
> No `using Godot;` anywhere in this folder. Core is plain C# — no `Node`, no `GD.Print`, no
|
||||
> `ProjectSettings`, no engine types in any signature.
|
||||
>
|
||||
> **Two reasons, both load-bearing.** (1) D-049 commits to *"C# for fast iteration, with C++-ready
|
||||
> seams… port path-by-path when settled and hot"* — the hot paths that get ported first are exactly
|
||||
> the ones in here, and a type that names `Godot.Vector2` cannot cross that seam. (2) It keeps the
|
||||
> column model testable and reviewable without standing up an engine.
|
||||
>
|
||||
> Where Core genuinely needs something the engine knows — the resolved `user://` directory — the
|
||||
> engine layer **hands it in** (`ToolingPaths.Configure`), rather than Core reaching for it.
|
||||
> If you find yourself wanting an engine type here, the type belongs one layer up.
|
||||
|
||||
## What is in here
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `Scripts/Column.cs` | ★★ **The load-bearing contract** (D-053). A column as run-length bands; `MaterialRun`; `RunOrigin`. Read its header before touching anything that stores world data. |
|
||||
| `Scripts/ColumnGrid.cs` | The 2D grid of columns. Shape only — chunking and streaming are later phases. |
|
||||
| `Scripts/MaterialKey.cs` | Material **identity** — a registry key, never an ordinal. |
|
||||
| `Scripts/TerrainMaterial.cs` | A row in the terrain schema (D-054). |
|
||||
| `Scripts/BuildingMaterial.cs` | A row in the building schema (D-054). |
|
||||
| `Scripts/MaterialRegistry.cs` | Both registries, append-only, seed rows only. |
|
||||
| `Scripts/RecipeRegistry.cs` | The transformation seam — **deliberately empty**. |
|
||||
| `Scripts/GenerationScale.cs` | ⭐ The scaling discipline. `MapSize`, `ScaleFactor`, normalized offsets. |
|
||||
| `Scripts/ToolingPaths.cs` | Every tooling path, env-overridable, resolved in one place. |
|
||||
| `Scripts/FileSafety.cs` | The permanent file-safety rules, as throws rather than sentences. |
|
||||
|
||||
## The three rules this layer exists to make unbreakable
|
||||
|
||||
1. **Air is the top run.** There is no "air vs solid" height branch, anywhere, ever.
|
||||
→ `Scripts/Column.cs`.
|
||||
2. **Properties come from the registry, never from storage.** A run stores an identity.
|
||||
→ `Scripts/MaterialRegistry.cs`.
|
||||
3. **Nothing uses a raw pixel number.** Every distance is a fraction of `MapSize`.
|
||||
→ `Scripts/GenerationScale.cs`.
|
||||
|
||||
## Not here, and not by accident
|
||||
|
||||
- **No water.** Water is an overlay over the columns (levels-not-cells), never a band.
|
||||
- **No biomes.** Biomes are a later *classification* of finished shape, not an input to it (D-049).
|
||||
- **No algorithms.** Phase 0 is shape. Stratigraphy, feature passes, meshing and run-splitting on
|
||||
dig are later phases.
|
||||
|
||||
→ `Design - Data - Column Model.md`, `Design - Data - Material Schema.md`,
|
||||
`Design - Tooling - Scaling Discipline.md`
|
||||
64
Core/Scripts/BuildingMaterial.cs
Normal file
64
Core/Scripts/BuildingMaterial.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A row in the BUILDING material schema — what players construct with.
|
||||
/// → `Design - Data - Material Schema.md` §2, D-054.
|
||||
///
|
||||
/// ⚠ DELIBERATELY A SEPARATE SCHEMA FROM <see cref="TerrainMaterial"/>, NOT A TAG ON ONE TABLE.
|
||||
/// Natural stone and a placed stone block are not the same thing tagged differently: one is a
|
||||
/// resource you harvest, the other is something you craft and build with. One tagged table was
|
||||
/// considered and REJECTED — it blurs the resource→product relationship that IS the survival
|
||||
/// loop, and invites the two concerns to bleed into each other the way biomes bled into terrain.
|
||||
/// The bridge between the two schemas is <see cref="RecipeRegistry"/>, not a shared row type.
|
||||
///
|
||||
/// ⚠ NO MESH-STYLE FIELD HERE EITHER — same rule, same reason. See <see cref="TerrainMaterial"/>.
|
||||
///
|
||||
/// APPEND-ONLY. Adding a material is one new row.
|
||||
/// </summary>
|
||||
public sealed class BuildingMaterial
|
||||
{
|
||||
/// <summary>Identity. A registry key, not an ordinal.</summary>
|
||||
public MaterialKey Key { get; }
|
||||
|
||||
/// <summary>Human-facing name. Presentation only.</summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How far this material carries structural support from an anchor, before decay.
|
||||
/// → `Design - Systems - Structural Integrity.md` (D-055: per-material reach).
|
||||
/// </summary>
|
||||
public float SupportReach { get; }
|
||||
|
||||
/// <summary>How fast support falls off across that reach. 0 = no decay, 1 = immediate.</summary>
|
||||
public float SupportDecay { get; }
|
||||
|
||||
/// <summary>Weight the material imposes on what carries it.</summary>
|
||||
public float Weight { get; }
|
||||
|
||||
/// <summary>Damage the material absorbs before failing.</summary>
|
||||
public float Durability { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime-only dense index. ⚠⚠ NEVER SERIALIZED — see <see cref="TerrainMaterial.RuntimeIndex"/>.
|
||||
/// </summary>
|
||||
public int RuntimeIndex { get; internal set; } = -1;
|
||||
|
||||
public BuildingMaterial(
|
||||
MaterialKey key,
|
||||
string displayName,
|
||||
float supportReach,
|
||||
float supportDecay,
|
||||
float weight,
|
||||
float durability)
|
||||
{
|
||||
Key = key;
|
||||
DisplayName = displayName;
|
||||
SupportReach = supportReach;
|
||||
SupportDecay = supportDecay;
|
||||
Weight = weight;
|
||||
Durability = durability;
|
||||
}
|
||||
|
||||
public override string ToString() => $"BuildingMaterial({Key})";
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/BuildingMaterial.cs.uid
Normal file
1
Core/Scripts/BuildingMaterial.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dxqo466b6ns0l
|
||||
170
Core/Scripts/Column.cs
Normal file
170
Core/Scripts/Column.cs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
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)";
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/Column.cs.uid
Normal file
1
Core/Scripts/Column.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bui0ydptx6ndk
|
||||
54
Core/Scripts/ColumnGrid.cs
Normal file
54
Core/Scripts/ColumnGrid.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
|
||||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// The world as a 2D GRID OF COLUMNS — one per horizontal cell.
|
||||
/// → `Design - Data - Column Model.md`, D-053.
|
||||
///
|
||||
/// The grid is square and its side is a GENERATION PARAMETER, never a constant. It is carried
|
||||
/// as a <see cref="GenerationScale"/> so that every derived distance in the project is forced
|
||||
/// through the one scaling rule. → `Design - Tooling - Scaling Discipline.md`.
|
||||
///
|
||||
/// ⚠ SHAPE ONLY, PHASE 0. Chunking for streaming, region files, serialization and the mesher
|
||||
/// interface are all later phases. A flat array is the honest Phase 0 representation: it says
|
||||
/// "grid of columns" and commits to nothing about how it will be paged.
|
||||
///
|
||||
/// ⚠ NOTHING ABOUT WATER LIVES HERE. Water is an overlay over the grid, not a layer in it.
|
||||
/// </summary>
|
||||
public sealed class ColumnGrid
|
||||
{
|
||||
private readonly Column[] _columns;
|
||||
|
||||
/// <summary>The scale this world was generated at. Side length, scale factor, derived distances.</summary>
|
||||
public GenerationScale Scale { get; }
|
||||
|
||||
/// <summary>Grid side in columns. 1 column = 1 metre (D-002, 1:1 scale).</summary>
|
||||
public int Side => Scale.MapSize;
|
||||
|
||||
public ColumnGrid(GenerationScale scale)
|
||||
{
|
||||
Scale = scale;
|
||||
_columns = new Column[(long)scale.MapSize * scale.MapSize <= int.MaxValue
|
||||
? scale.MapSize * scale.MapSize
|
||||
: throw new ArgumentOutOfRangeException(nameof(scale), scale.MapSize,
|
||||
"MapSize squared exceeds a single flat array. Chunked storage is a later phase — see the class comment.")];
|
||||
}
|
||||
|
||||
/// <summary>The column at (x, y). Null until one is placed — Phase 0 allocates no columns.</summary>
|
||||
public Column this[int x, int y]
|
||||
{
|
||||
get => _columns[Index(x, y)];
|
||||
set => _columns[Index(x, y)] = value;
|
||||
}
|
||||
|
||||
private int Index(int x, int y)
|
||||
{
|
||||
if ((uint)x >= (uint)Side) throw new ArgumentOutOfRangeException(nameof(x), x, $"x outside [0,{Side}).");
|
||||
if ((uint)y >= (uint)Side) throw new ArgumentOutOfRangeException(nameof(y), y, $"y outside [0,{Side}).");
|
||||
return y * Side + x;
|
||||
}
|
||||
|
||||
public override string ToString() => $"ColumnGrid({Side}x{Side})";
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/ColumnGrid.cs.uid
Normal file
1
Core/Scripts/ColumnGrid.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dgcj88njh3wvl
|
||||
85
Core/Scripts/FileSafety.cs
Normal file
85
Core/Scripts/FileSafety.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// ⚠⚠ THE PERMANENT FILE-SAFETY RULES, ENFORCED IN CODE.
|
||||
/// → `Design - Tooling - Iteration and Batching.md` § FILE-SAFETY rules — permanent.
|
||||
///
|
||||
/// ═══ THE RULES ═══
|
||||
///
|
||||
/// 1. NO DELETION in the runtime root or anywhere under batches/.
|
||||
/// 2. INTERMEDIATES PERSIST in a scratch/ subfolder that is NEVER cleaned.
|
||||
/// 3. THE DEVELOPER'S PLACED FILES ARE NEVER TOUCHED.
|
||||
/// 4. ANY DELETION IS NAMED EXPLICITLY IN THE RUN REPORT.
|
||||
///
|
||||
/// ═══ WHY THIS IS CODE AND NOT A README LINE ═══
|
||||
///
|
||||
/// These were born from a real incident: an executor admitted it had been deleting the
|
||||
/// developer's staged test blueprint. It was owned and fixed IN CODE. A rule that depends on an
|
||||
/// executor remembering it will eventually meet an executor who does not — so the rule is a
|
||||
/// throw, not a sentence.
|
||||
///
|
||||
/// ═══ HOW TO USE IT ═══
|
||||
///
|
||||
/// There is no Delete() convenience here on purpose. Any code that removes a file calls
|
||||
/// <see cref="AssertDeletable"/> first, with a reason, and the reason goes in the run report.
|
||||
/// Deleting without asking is the thing being prevented; making it one line easier to ask is
|
||||
/// the whole mechanism.
|
||||
///
|
||||
/// Nothing deletes anything this phase.
|
||||
/// </summary>
|
||||
public static class FileSafety
|
||||
{
|
||||
/// <summary>
|
||||
/// Refuse the deletion unless it is provably outside every protected root, and record why.
|
||||
/// Throws <see cref="UnauthorizedAccessException"/> when the path is protected.
|
||||
/// </summary>
|
||||
/// <param name="path">The file or directory a caller intends to remove.</param>
|
||||
/// <param name="reason">
|
||||
/// Why. Required, non-blank — it is what the run report has to print. "cleanup" is not a
|
||||
/// reason; "regenerable legacy blueprint, 22 GB, nothing reads it" is.
|
||||
/// </param>
|
||||
public static void AssertDeletable(string path, string reason)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("A path is required.", nameof(path));
|
||||
if (string.IsNullOrWhiteSpace(reason))
|
||||
throw new ArgumentException(
|
||||
"A deletion needs a stated reason — it must be named explicitly in the run report. " +
|
||||
"If you cannot name one, that is the answer.", nameof(reason));
|
||||
|
||||
string full = Path.GetFullPath(path);
|
||||
|
||||
foreach (string protectedRoot in ProtectedRoots())
|
||||
{
|
||||
if (IsWithin(full, protectedRoot))
|
||||
throw new UnauthorizedAccessException(
|
||||
$"REFUSED: '{full}' is inside a protected root ('{protectedRoot}'). " +
|
||||
"No deletion is permitted in the runtime root or under batches/ — intermediates persist. " +
|
||||
$"(Stated reason was: {reason})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The roots nothing may delete from. The batches root and the resolved output/user roots —
|
||||
/// i.e. everything a generation run and a human's browsing of it depend on.
|
||||
/// </summary>
|
||||
public static string[] ProtectedRoots() => new[]
|
||||
{
|
||||
Path.GetFullPath(ToolingPaths.BatchesRoot),
|
||||
Path.GetFullPath(ToolingPaths.OutputDir),
|
||||
Path.GetFullPath(ToolingPaths.BlueprintPath),
|
||||
Path.GetFullPath(ToolingPaths.UserDataDir),
|
||||
};
|
||||
|
||||
private static bool IsWithin(string candidate, string root)
|
||||
{
|
||||
string c = candidate.TrimEnd(Path.DirectorySeparatorChar);
|
||||
string r = root.TrimEnd(Path.DirectorySeparatorChar);
|
||||
return string.Equals(c, r, StringComparison.Ordinal)
|
||||
|| c.StartsWith(r + Path.DirectorySeparatorChar, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/FileSafety.cs.uid
Normal file
1
Core/Scripts/FileSafety.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://jdpjwyumfo76
|
||||
119
Core/Scripts/GenerationScale.cs
Normal file
119
Core/Scripts/GenerationScale.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System;
|
||||
|
||||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// ⭐ THE SCALING DISCIPLINE, IN CODE. → `Design - Tooling - Scaling Discipline.md`, D-002.
|
||||
///
|
||||
/// ═══ THE RULE ═══
|
||||
///
|
||||
/// NOTHING IN THE GENERATOR USES A RAW PIXEL NUMBER.
|
||||
/// Every distance, radius, threshold and noise frequency is a FRACTION OF MapSize,
|
||||
/// or derived from ScaleFactor.
|
||||
///
|
||||
/// ═══ WHY IT IS A RULE AND NOT A PREFERENCE ═══
|
||||
///
|
||||
/// These failures are SILENT. A hardcoded pixel count does not throw — the generator still
|
||||
/// runs and still produces a map, and the map is just subtly wrong in ways that are hard to
|
||||
/// attribute. The prototype learned this the expensive way: moving to 1:1 scale made the world
|
||||
/// four times wider in pixels while the noise settings stayed put, and the terrain became
|
||||
/// "TV static" — the same features packed into a quarter of the space. The same transition
|
||||
/// broke a long tail of checks like "look 10 pixels away to see if there's water", which
|
||||
/// quietly started measuring a quarter of the distance they used to.
|
||||
///
|
||||
/// In the developer's own words: "These kind of numbers I'm getting tired of changing because
|
||||
/// of map size. We need to make sure these scale!"
|
||||
///
|
||||
/// ═══ ⚠ ONE BASELINE, DELIBERATELY ═══
|
||||
///
|
||||
/// The prototype ran TWO reference scales side by side: /1024 for noise and map drawing,
|
||||
/// /4096 for town counts and road repulsion. Neither was wrong, but a reader — or a resize —
|
||||
/// had to know which applied where, and the design doc's standing instruction is to "pick
|
||||
/// deliberately and say which". This repo picks: ONE baseline, 1024, for everything. If a
|
||||
/// second is ever genuinely needed it gets its own named type and its own reason, not a
|
||||
/// bare divisor at a call site.
|
||||
///
|
||||
/// ═══ ⚠⚠ THE TIGHTENING OVER THE REFERENCE — NORMALIZED ADDITIVE OFFSETS ═══
|
||||
///
|
||||
/// The reference decorrelated noise layers with offsets in RAW PIXELS — GetNoise2D(x + 1000, …),
|
||||
/// GetNoise2D(x * 1.5f + 5000, …). Because frequency already carries a 1/MapSize normalization,
|
||||
/// a pixel-space offset does NOT hold still across map sizes: 1000 px is 1.0 normalized units
|
||||
/// at 4K but 0.5 at 8K, so a resize silently samples a DIFFERENT SLICE of the noise field. The
|
||||
/// feature scale is preserved; the REALIZATION is not. The same seed at two map sizes gives two
|
||||
/// different worlds, for no reason anyone wrote down. (Found by the chat1/00 reference
|
||||
/// inventory, §4.2 — where it also corrected the vault's standing diagnosis that the detail
|
||||
/// noises were unscaled. They are scaled; it is the OFFSETS that are not.)
|
||||
///
|
||||
/// So in this repo: DECORRELATION OFFSETS ARE DECLARED IN MAP WIDTHS, via
|
||||
/// <see cref="OffsetInMapWidths"/>. Never write a bare pixel constant into a noise coordinate.
|
||||
///
|
||||
/// No noise is built this phase. The convention is established before the generator exists so
|
||||
/// that nothing is ever added unscaled — which is the only time this rule is cheap to hold.
|
||||
/// </summary>
|
||||
public readonly struct GenerationScale
|
||||
{
|
||||
/// <summary>
|
||||
/// The reference map width the scaling baseline is anchored to. The world's noise looked
|
||||
/// right at 1024 columns; every larger map stretches its features back out to match.
|
||||
/// </summary>
|
||||
public const int BaselineMapSize = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// The map's side in columns. 1 column = 1 metre (D-002, 1:1 scale).
|
||||
///
|
||||
/// ⚠ A GENERATION PARAMETER. Never a constant, never baked in. The rewrite's default target
|
||||
/// is 10k x 10k, configurable 8k–12k (D-056) — but that is a DEFAULT chosen by config, and
|
||||
/// no code below this line may assume it.
|
||||
/// </summary>
|
||||
public readonly int MapSize;
|
||||
|
||||
public GenerationScale(int mapSize)
|
||||
{
|
||||
if (mapSize < BaselineMapSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(mapSize), mapSize,
|
||||
$"MapSize must be at least the {BaselineMapSize} baseline.");
|
||||
MapSize = mapSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapSize / 1024. Divide a baseline-tuned noise frequency by this to hold feature size
|
||||
/// constant in METRES as the map grows. A mountain range is the same physical size at any
|
||||
/// map profile — which is what makes the map-size setting safe to change at all.
|
||||
/// </summary>
|
||||
public float ScaleFactor => MapSize / (float)BaselineMapSize;
|
||||
|
||||
/// <summary>
|
||||
/// A distance, from a fraction of the map. Use this instead of writing a pixel count.
|
||||
/// <c>Fraction(0.75f)</c> is "three quarters of the way across", at every map size.
|
||||
/// </summary>
|
||||
public float Fraction(float fractionOfMap) => fractionOfMap * MapSize;
|
||||
|
||||
/// <summary>
|
||||
/// The inverse: what fraction of the map a distance in columns represents. For turning a
|
||||
/// measured pixel distance back into a scale-free constant before you commit it to code.
|
||||
/// </summary>
|
||||
public float AsFraction(float distanceInColumns) => distanceInColumns / MapSize;
|
||||
|
||||
/// <summary>
|
||||
/// A noise frequency, from one tuned at the 1024 baseline.
|
||||
/// <c>NoiseFrequency(0.004f)</c> reproduces the reference's base terrain frequency at any
|
||||
/// map size. → the reference's <c>0.004f / scaleFactor</c>, chat1/00 §2.1.
|
||||
/// </summary>
|
||||
public float NoiseFrequency(float baselineFrequency) => baselineFrequency / ScaleFactor;
|
||||
|
||||
/// <summary>
|
||||
/// ⭐ A decorrelation offset for a noise coordinate, declared in MAP WIDTHS.
|
||||
///
|
||||
/// <c>OffsetInMapWidths(0.25f)</c> shifts the sample by a quarter of the map at EVERY map
|
||||
/// size — so two noise layers stay exactly as decorrelated at 12K as they were at 8K, and
|
||||
/// the same seed produces the same world shape at any size.
|
||||
///
|
||||
/// ⚠ This is the one place a decorrelation offset may come from. A bare pixel constant in a
|
||||
/// noise coordinate is the bug described in this type's summary; there is no correct value
|
||||
/// for one.
|
||||
/// </summary>
|
||||
public float OffsetInMapWidths(float mapWidths) => mapWidths * MapSize;
|
||||
|
||||
public override string ToString() => $"GenerationScale(MapSize={MapSize}, ScaleFactor={ScaleFactor:F3})";
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/GenerationScale.cs.uid
Normal file
1
Core/Scripts/GenerationScale.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bnpvgf20ybmhd
|
||||
47
Core/Scripts/MaterialKey.cs
Normal file
47
Core/Scripts/MaterialKey.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
|
||||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A material's IDENTITY. → `Design - Data - Material Schema.md`, D-054.
|
||||
///
|
||||
/// ⚠⚠ IDENTITY IS A REGISTRY KEY, NEVER A POSITIONAL ORDINAL.
|
||||
///
|
||||
/// This is deliberately a stable string key and not an enum. The prototype's blueprint format
|
||||
/// keyed on enum ordinals, which made the enum's DECLARATION ORDER a wire format: inserting a
|
||||
/// value in the middle silently reinterpreted every world already generated
|
||||
/// (→ `Design - Tooling - Offline Generation.md`: "anything that changes its format or its enum
|
||||
/// ordinals breaks every world already generated"). The registry is also the mod surface
|
||||
/// (→ D-011, D-043) — a mod adds rows, and rows cannot be given ordinals by a third party.
|
||||
///
|
||||
/// So: adding a material is one new row, appended anywhere, in any order, by anyone, forever.
|
||||
///
|
||||
/// No serialization is built this phase. This type exists so that when it is, the identity it
|
||||
/// writes is already the key rather than a position.
|
||||
/// </summary>
|
||||
public readonly struct MaterialKey : IEquatable<MaterialKey>
|
||||
{
|
||||
/// <summary>The stable identity. Lowercase, dot-namespaced: "core.limestone", "mod.basalt".</summary>
|
||||
public readonly string Value;
|
||||
|
||||
public MaterialKey(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new ArgumentException("A material key may not be null or blank.", nameof(value));
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>The unset key. Distinguishable from any real row; never registered.</summary>
|
||||
public static readonly MaterialKey None = default;
|
||||
|
||||
public bool IsNone => Value == null;
|
||||
|
||||
public bool Equals(MaterialKey other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||
public override bool Equals(object obj) => obj is MaterialKey k && Equals(k);
|
||||
public override int GetHashCode() => Value == null ? 0 : StringComparer.Ordinal.GetHashCode(Value);
|
||||
public override string ToString() => Value ?? "<none>";
|
||||
|
||||
public static bool operator ==(MaterialKey a, MaterialKey b) => a.Equals(b);
|
||||
public static bool operator !=(MaterialKey a, MaterialKey b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/MaterialKey.cs.uid
Normal file
1
Core/Scripts/MaterialKey.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dw6ee00eqjwp1
|
||||
117
Core/Scripts/MaterialRegistry.cs
Normal file
117
Core/Scripts/MaterialRegistry.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// The two material schemas, as APPEND-ONLY registries. → `Design - Data - Material Schema.md`, D-054.
|
||||
///
|
||||
/// ⭐ NO MATERIAL LOGIC IS HARDCODED ANYWHERE — everything reads from here.
|
||||
/// This is the specific discipline that makes deferring the full material list SAFE, unlike
|
||||
/// biomes, which were hardcoded and coupled. Understand the shape now; fill the rows forever.
|
||||
///
|
||||
/// ⚠ THE SEED ROWS BELOW ARE NOT THE MATERIAL LIST. They are the few that are certain
|
||||
/// (limestone is in — karst substrate). Do NOT enumerate the full list now; design materials
|
||||
/// incrementally as building proceeds. Adding one is one new row, appended, nothing else.
|
||||
///
|
||||
/// ⚠ REGISTRATION ORDER IS NOT MEANINGFUL. It sets <c>RuntimeIndex</c>, which is never
|
||||
/// serialized. Reordering these lines must not change a single persisted byte, now or ever.
|
||||
///
|
||||
/// Phase 0: rows are seeded in code. Loading them from a data file is a later phase — the shape
|
||||
/// here (identity + properties, no logic) is what makes that a loader change and nothing more.
|
||||
/// </summary>
|
||||
public static class MaterialRegistry
|
||||
{
|
||||
// ---- terrain --------------------------------------------------------
|
||||
private static readonly Dictionary<MaterialKey, TerrainMaterial> _terrain = new();
|
||||
private static readonly List<TerrainMaterial> _terrainByIndex = new();
|
||||
|
||||
// ---- building -------------------------------------------------------
|
||||
private static readonly Dictionary<MaterialKey, BuildingMaterial> _building = new();
|
||||
private static readonly List<BuildingMaterial> _buildingByIndex = new();
|
||||
|
||||
/// <summary>
|
||||
/// AIR. The one key the column model leans on structurally: everything above the surface is
|
||||
/// the top AIR run, and a cave is an air run mid-column. It is a material row like any
|
||||
/// other — there is no "is air" branch anywhere. → `Design - Data - Column Model.md`.
|
||||
/// </summary>
|
||||
public static readonly MaterialKey Air = new("core.air");
|
||||
|
||||
static MaterialRegistry()
|
||||
{
|
||||
// ===== TERRAIN — seed rows, NOT final =====
|
||||
// key display hardness yield per-voxel support
|
||||
RegisterTerrain("core.air", "Air", 0.0f, MaterialKey.None, 0, 0.0f);
|
||||
RegisterTerrain("core.sand", "Sand", 0.2f, "core.sand", 1, 0.1f);
|
||||
RegisterTerrain("core.dirt", "Dirt", 0.3f, "core.dirt", 1, 0.2f);
|
||||
RegisterTerrain("core.clay", "Clay", 0.4f, "core.clay", 1, 0.3f);
|
||||
RegisterTerrain("core.limestone", "Limestone", 0.7f, "core.limestone", 1, 0.7f); // karst substrate
|
||||
RegisterTerrain("core.stone", "Stone", 0.9f, "core.stone", 1, 0.9f);
|
||||
RegisterTerrain("core.bedrock", "Bedrock", 1.0f, MaterialKey.None, 0, 1.0f); // unmineable floor
|
||||
|
||||
// ===== BUILDING — seed rows, NOT final =====
|
||||
// key display reach decay weight durability
|
||||
RegisterBuilding("build.thatch", "Thatch/Palm", 1.5f, 0.60f, 0.2f, 0.2f);
|
||||
RegisterBuilding("build.wood", "Wood Plank", 4.0f, 0.30f, 0.5f, 0.5f);
|
||||
RegisterBuilding("build.stone", "Worked Stone", 6.0f, 0.20f, 1.0f, 0.8f);
|
||||
RegisterBuilding("build.concrete", "Concrete", 9.0f, 0.12f, 1.1f, 0.9f);
|
||||
RegisterBuilding("build.steel", "Steel", 14.0f, 0.06f, 0.9f, 1.0f);
|
||||
}
|
||||
|
||||
// ---- registration (append-only; no removal, by design) --------------
|
||||
|
||||
private static void RegisterTerrain(string key, string name, float hardness, MaterialKey yieldKey, int yieldPer, float support)
|
||||
=> RegisterTerrain(new TerrainMaterial(new MaterialKey(key), name, hardness, yieldKey, yieldPer, support));
|
||||
|
||||
private static void RegisterTerrain(string key, string name, float hardness, string yieldKey, int yieldPer, float support)
|
||||
=> RegisterTerrain(key, name, hardness, new MaterialKey(yieldKey), yieldPer, support);
|
||||
|
||||
private static void RegisterBuilding(string key, string name, float reach, float decay, float weight, float durability)
|
||||
=> RegisterBuilding(new BuildingMaterial(new MaterialKey(key), name, reach, decay, weight, durability));
|
||||
|
||||
/// <summary>Append a terrain material. Public so mods and data loaders add rows the same way seeds do.</summary>
|
||||
public static void RegisterTerrain(TerrainMaterial material)
|
||||
{
|
||||
if (material == null) throw new ArgumentNullException(nameof(material));
|
||||
if (_terrain.ContainsKey(material.Key))
|
||||
throw new InvalidOperationException($"Terrain material '{material.Key}' is already registered. The registry is append-only; keys are unique.");
|
||||
material.RuntimeIndex = _terrainByIndex.Count;
|
||||
_terrainByIndex.Add(material);
|
||||
_terrain.Add(material.Key, material);
|
||||
}
|
||||
|
||||
/// <summary>Append a building material.</summary>
|
||||
public static void RegisterBuilding(BuildingMaterial material)
|
||||
{
|
||||
if (material == null) throw new ArgumentNullException(nameof(material));
|
||||
if (_building.ContainsKey(material.Key))
|
||||
throw new InvalidOperationException($"Building material '{material.Key}' is already registered. The registry is append-only; keys are unique.");
|
||||
material.RuntimeIndex = _buildingByIndex.Count;
|
||||
_buildingByIndex.Add(material);
|
||||
_building.Add(material.Key, material);
|
||||
}
|
||||
|
||||
// ---- lookup ---------------------------------------------------------
|
||||
|
||||
public static TerrainMaterial Terrain(MaterialKey key)
|
||||
=> _terrain.TryGetValue(key, out var m)
|
||||
? m
|
||||
: throw new KeyNotFoundException($"No terrain material registered for key '{key}'.");
|
||||
|
||||
public static BuildingMaterial Building(MaterialKey key)
|
||||
=> _building.TryGetValue(key, out var m)
|
||||
? m
|
||||
: throw new KeyNotFoundException($"No building material registered for key '{key}'.");
|
||||
|
||||
public static bool TryTerrain(MaterialKey key, out TerrainMaterial material) => _terrain.TryGetValue(key, out material);
|
||||
public static bool TryBuilding(MaterialKey key, out BuildingMaterial material) => _building.TryGetValue(key, out material);
|
||||
|
||||
public static bool IsRegisteredTerrain(MaterialKey key) => _terrain.ContainsKey(key);
|
||||
|
||||
public static IReadOnlyList<TerrainMaterial> AllTerrain => _terrainByIndex;
|
||||
public static IReadOnlyList<BuildingMaterial> AllBuilding => _buildingByIndex;
|
||||
|
||||
public static int TerrainCount => _terrainByIndex.Count;
|
||||
public static int BuildingCount => _buildingByIndex.Count;
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/MaterialRegistry.cs.uid
Normal file
1
Core/Scripts/MaterialRegistry.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dtkpl88w2n3q5
|
||||
79
Core/Scripts/RecipeRegistry.cs
Normal file
79
Core/Scripts/RecipeRegistry.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// ⭐ THE TRANSFORMATION / RECIPE SEAM — the bridge between the two material schemas.
|
||||
/// → `Design - Data - Material Schema.md` §3, D-054.
|
||||
///
|
||||
/// Terrain materials are RESOURCES that refine and combine into BUILDING materials —
|
||||
/// e.g. sand + clay + rock → concrete. That relationship is the survival loop, and keeping the
|
||||
/// two schemas distinct with a transformation between them is what stops it collapsing into
|
||||
/// "the same material tagged differently".
|
||||
///
|
||||
/// ⚠⚠ THIS REGISTRY IS DELIBERATELY EMPTY, AND THAT IS THE POINT.
|
||||
///
|
||||
/// The recipes themselves are DEFERRED. Designing the resource tree now would be a
|
||||
/// biomes-premature-style over-commitment — the exact mistake the rewrite exists to undo.
|
||||
/// But the SEAM EXISTS NOW: the architecture has the hole, so filling it later is new ROWS and
|
||||
/// never a rewrite.
|
||||
///
|
||||
/// Do not add recipes here without a design decision that says the tree is ready.
|
||||
/// </summary>
|
||||
public static class RecipeRegistry
|
||||
{
|
||||
private static readonly List<Recipe> _recipes = new();
|
||||
|
||||
// ===== NO SEED ROWS. See the class comment — the emptiness is the design. =====
|
||||
|
||||
/// <summary>Append a recipe. Append-only, same as the material registries.</summary>
|
||||
public static void Register(Recipe recipe)
|
||||
{
|
||||
if (recipe == null) throw new ArgumentNullException(nameof(recipe));
|
||||
_recipes.Add(recipe);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<Recipe> All => _recipes;
|
||||
public static int Count => _recipes.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One transformation: some quantity of terrain-derived inputs produces a building material.
|
||||
///
|
||||
/// ⚠ SHAPE ONLY. The exact recipe FORMAT is an open note in the design doc — inputs may grow
|
||||
/// tools, stations, time or byproducts. This is the minimum that makes the seam real enough to
|
||||
/// compile against, not a committed format.
|
||||
/// </summary>
|
||||
public sealed class Recipe
|
||||
{
|
||||
/// <summary>Input material keys and their amounts. Terrain-derived resources.</summary>
|
||||
public IReadOnlyList<RecipeInput> Inputs { get; }
|
||||
|
||||
/// <summary>The building material produced.</summary>
|
||||
public MaterialKey Output { get; }
|
||||
|
||||
/// <summary>How much of <see cref="Output"/> one application yields.</summary>
|
||||
public int OutputAmount { get; }
|
||||
|
||||
public Recipe(IReadOnlyList<RecipeInput> inputs, MaterialKey output, int outputAmount)
|
||||
{
|
||||
Inputs = inputs ?? throw new ArgumentNullException(nameof(inputs));
|
||||
Output = output;
|
||||
OutputAmount = outputAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One input line of a <see cref="Recipe"/>.</summary>
|
||||
public readonly struct RecipeInput
|
||||
{
|
||||
public readonly MaterialKey Material;
|
||||
public readonly int Amount;
|
||||
|
||||
public RecipeInput(MaterialKey material, int amount)
|
||||
{
|
||||
Material = material;
|
||||
Amount = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/RecipeRegistry.cs.uid
Normal file
1
Core/Scripts/RecipeRegistry.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ghic8rgrpnre
|
||||
76
Core/Scripts/TerrainMaterial.cs
Normal file
76
Core/Scripts/TerrainMaterial.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A row in the TERRAIN material schema — what the natural world is made of.
|
||||
/// → `Design - Data - Material Schema.md` §1, D-054.
|
||||
///
|
||||
/// PROPERTIES-FROM-TYPE: a column run stores only a <see cref="MaterialKey"/>. Every property
|
||||
/// lives here and is reached by registry lookup. Nothing is ever stored per-run.
|
||||
/// → `Design - Data - Column Model.md`.
|
||||
///
|
||||
/// ⚠ THERE IS NO MESH-STYLE FIELD ON THIS ROW, AND THERE MUST NEVER BE ONE.
|
||||
/// Mesh style is per-ORIGIN, not per-material: natural terrain meshes smooth, player-placed
|
||||
/// blocks mesh as cubes, and the same material can be both. The origin flag lives on the run
|
||||
/// (<see cref="RunOrigin"/>), which is where the mesher reads it.
|
||||
/// → D-054 open notes ("material does not override mesh style"), `Design - Rendering - Mesher.md`.
|
||||
///
|
||||
/// APPEND-ONLY. Adding a material is one new row in <see cref="MaterialRegistry"/> — never a
|
||||
/// code change anywhere else. No material logic is hardcoded anywhere.
|
||||
/// </summary>
|
||||
public sealed class TerrainMaterial
|
||||
{
|
||||
/// <summary>Identity. A registry key, not an ordinal. → <see cref="MaterialKey"/>.</summary>
|
||||
public MaterialKey Key { get; }
|
||||
|
||||
/// <summary>Human-facing name. Presentation only — never an identity, never a lookup key.</summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>Mining difficulty. Units deliberately unfixed this phase; relative for now.</summary>
|
||||
public float Hardness { get; }
|
||||
|
||||
/// <summary>
|
||||
/// What a mined voxel gives. SEAM: the resource/item schema is deferred, so this currently
|
||||
/// names a material key. When resources become their own schema this becomes a resource key
|
||||
/// — one field's type, not a redesign.
|
||||
/// </summary>
|
||||
public MaterialKey YieldKey { get; }
|
||||
|
||||
/// <summary>How much <see cref="YieldKey"/> one mined voxel gives.</summary>
|
||||
public int YieldPerVoxel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// How much structural support this material carries as NATURAL terrain.
|
||||
/// Consumed by `Design - Systems - Structural Integrity.md` (stability scalar, D-055).
|
||||
/// Natural terrain is a LAZY anchor there — this is the value it anchors with.
|
||||
/// </summary>
|
||||
public float NaturalSupportStrength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime-only dense index, assigned by the registry in registration order.
|
||||
///
|
||||
/// ⚠⚠ NEVER SERIALIZE THIS. NEVER SEND IT OVER A WIRE. NEVER STORE IT IN A BLUEPRINT.
|
||||
/// It exists solely so hot loops can index dense arrays instead of hashing strings. It
|
||||
/// changes whenever registration order changes — which a mod adding a row will do. The
|
||||
/// identity that persists is <see cref="Key"/>, always.
|
||||
/// </summary>
|
||||
public int RuntimeIndex { get; internal set; } = -1;
|
||||
|
||||
public TerrainMaterial(
|
||||
MaterialKey key,
|
||||
string displayName,
|
||||
float hardness,
|
||||
MaterialKey yieldKey,
|
||||
int yieldPerVoxel,
|
||||
float naturalSupportStrength)
|
||||
{
|
||||
Key = key;
|
||||
DisplayName = displayName;
|
||||
Hardness = hardness;
|
||||
YieldKey = yieldKey;
|
||||
YieldPerVoxel = yieldPerVoxel;
|
||||
NaturalSupportStrength = naturalSupportStrength;
|
||||
}
|
||||
|
||||
public override string ToString() => $"TerrainMaterial({Key})";
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/TerrainMaterial.cs.uid
Normal file
1
Core/Scripts/TerrainMaterial.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b13bbprjpu3jl
|
||||
114
Core/Scripts/ToolingPaths.cs
Normal file
114
Core/Scripts/ToolingPaths.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace IslaApocalypse.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// ⭐ EVERY PATH THE TOOLING READS OR WRITES, RESOLVED IN ONE PLACE, OVERRIDABLE BY ENVIRONMENT.
|
||||
/// → `Design - Tooling - Iteration and Batching.md` § "Tooling must be safe BY CODE, not by care".
|
||||
///
|
||||
/// ═══ WHY THIS TYPE EXISTS ═══
|
||||
///
|
||||
/// So a batch run CANNOT read over or write to the developer's live config, staged blueprints,
|
||||
/// or output directory. Not "should not" — cannot, because there is no other way to obtain a
|
||||
/// path, and each one has an environment override that a batch script sets before it starts.
|
||||
///
|
||||
/// > ⭐ THE POINT IS THAT IT IS ENFORCED BY CODE, NOT BY CARE.
|
||||
/// > A rule that depends on an executor remembering it will eventually meet an executor who
|
||||
/// > does not.
|
||||
///
|
||||
/// ⚠ This is not hypothetical. These rules were born from a real incident: an executor admitted
|
||||
/// it had been deleting the developer's staged test blueprint. It was owned and fixed IN CODE.
|
||||
/// That is why these are rules and not guidance. → <see cref="FileSafety"/>.
|
||||
///
|
||||
/// ═══ THE OVERRIDES ═══
|
||||
///
|
||||
/// ISLA_CONFIG_PATH the generation config file default: user://config.json
|
||||
/// ISLA_BLUEPRINT_PATH the blueprint read/written default: user://blueprints
|
||||
/// ISLA_OUTPUT_DIR generation output (maps, batches) default: user://output
|
||||
///
|
||||
/// ⚠ user:// RESOLUTION. Defaults sit under the project's own user data directory, which this
|
||||
/// project pins away from the old prototype's — see project.godot's user:// isolation block.
|
||||
/// Core is engine-free by design, so the caller supplies the resolved user directory (from
|
||||
/// Godot's OS.GetUserDataDir()) via <see cref="Configure"/>. Until it does, the defaults resolve
|
||||
/// under the process working directory, which is wrong for a real run and loud enough to notice.
|
||||
///
|
||||
/// No generator exists this phase. The rails are laid before it so it inherits them.
|
||||
/// </summary>
|
||||
public static class ToolingPaths
|
||||
{
|
||||
public const string ConfigPathVar = "ISLA_CONFIG_PATH";
|
||||
public const string BlueprintPathVar = "ISLA_BLUEPRINT_PATH";
|
||||
public const string OutputDirVar = "ISLA_OUTPUT_DIR";
|
||||
|
||||
private static string _userDataDir;
|
||||
|
||||
/// <summary>
|
||||
/// Hand Core the engine-resolved user data directory. Called once at startup by whichever
|
||||
/// layer owns the engine (Tools or Server); Core never asks Godot for it itself.
|
||||
/// </summary>
|
||||
public static void Configure(string userDataDir)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userDataDir))
|
||||
throw new ArgumentException("A user data directory is required.", nameof(userDataDir));
|
||||
_userDataDir = userDataDir;
|
||||
}
|
||||
|
||||
/// <summary>The resolved user data directory, or the working directory if none was configured.</summary>
|
||||
public static string UserDataDir => _userDataDir ?? Directory.GetCurrentDirectory();
|
||||
|
||||
/// <summary>Whether <see cref="Configure"/> has been called. Tooling should assert this before a run.</summary>
|
||||
public static bool IsConfigured => _userDataDir != null;
|
||||
|
||||
/// <summary>The generation config file. Override: ISLA_CONFIG_PATH.</summary>
|
||||
public static string ConfigPath =>
|
||||
Override(ConfigPathVar) ?? Path.Combine(UserDataDir, "config.json");
|
||||
|
||||
/// <summary>Where blueprints are read from and written to. Override: ISLA_BLUEPRINT_PATH.</summary>
|
||||
public static string BlueprintPath =>
|
||||
Override(BlueprintPathVar) ?? Path.Combine(UserDataDir, "blueprints");
|
||||
|
||||
/// <summary>Where a generation run writes its output. Override: ISLA_OUTPUT_DIR.</summary>
|
||||
public static string OutputDir =>
|
||||
Override(OutputDirVar) ?? Path.Combine(UserDataDir, "output");
|
||||
|
||||
/// <summary>
|
||||
/// The batches root. → `Design - Tooling - Iteration and Batching.md`:
|
||||
/// <c>batches/NN_<name>/<seed>_<variant>/</c>, each batch carrying an
|
||||
/// INDEX.md and a persistent scratch/. A/B comparisons are browsed by a human, and a flat
|
||||
/// directory of same-named PNGs is not browsable.
|
||||
///
|
||||
/// ⚠ PROTECTED FROM DELETION. → <see cref="FileSafety"/>.
|
||||
/// </summary>
|
||||
public static string BatchesRoot => Path.Combine(OutputDir, "batches");
|
||||
|
||||
/// <summary>
|
||||
/// The scratch subfolder of a batch. INTERMEDIATES PERSIST HERE AND ARE NEVER CLEANED —
|
||||
/// the whole point is that a run's intermediates survive it, so a surprising result can be
|
||||
/// investigated instead of regenerated.
|
||||
/// </summary>
|
||||
public static string BatchScratch(string batchDir) => Path.Combine(batchDir, "scratch");
|
||||
|
||||
/// <summary>
|
||||
/// A batch directory: <c>batches/NN_<name>/<seed>_<variant>/</c>.
|
||||
/// </summary>
|
||||
public static string BatchDir(int batchNumber, string batchName, long seed, string variant)
|
||||
=> Path.Combine(BatchesRoot, $"{batchNumber:D2}_{batchName}", $"{seed}_{variant}");
|
||||
|
||||
private static string Override(string variable)
|
||||
{
|
||||
string v = Environment.GetEnvironmentVariable(variable);
|
||||
return string.IsNullOrWhiteSpace(v) ? null : v;
|
||||
}
|
||||
|
||||
/// <summary>Every resolved path, for a run report's header. Print this; it is cheap and it has caught things.</summary>
|
||||
public static string Describe() =>
|
||||
$"user data : {UserDataDir}{(IsConfigured ? "" : " ⚠ NOT CONFIGURED — falling back to CWD")}\n" +
|
||||
$"config : {ConfigPath}{Marker(ConfigPathVar)}\n" +
|
||||
$"blueprints: {BlueprintPath}{Marker(BlueprintPathVar)}\n" +
|
||||
$"output : {OutputDir}{Marker(OutputDirVar)}\n" +
|
||||
$"batches : {BatchesRoot}";
|
||||
|
||||
private static string Marker(string variable) => Override(variable) != null ? $" [{variable}]" : "";
|
||||
}
|
||||
}
|
||||
1
Core/Scripts/ToolingPaths.cs.uid
Normal file
1
Core/Scripts/ToolingPaths.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dbgrxcbahn163
|
||||
82
README.md
Normal file
82
README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Isla Apocalypse — v2
|
||||
|
||||
The rewrite (**D-049**). A post-apocalyptic survival voxel game: one island, generated once offline,
|
||||
loaded by everyone.
|
||||
|
||||
**Status: Phase 0 — foundation.** This repo is a *skeleton*. It builds, it resolves its own runtime
|
||||
directory, and it carries the two data contracts and the tooling rails. **It generates nothing.**
|
||||
|
||||
---
|
||||
|
||||
## ⚠ Two repos. Never one.
|
||||
|
||||
| Repo | Role |
|
||||
|---|---|
|
||||
| **`~/celerNexus/islaApocalypse-v2/`** *(this)* | The rewrite. **Every line of new code is written here.** |
|
||||
| **`~/celerNexus/islaApocalypse/`** | ⛔ **READ-ONLY.** The pre-rewrite prototype, archived at `ab78883`, tag `pre-rewrite-reference`. **Ported FROM; never written to.** |
|
||||
| **`~/celerNexus/islaApocalypse-vault-v0.1/`** | The design vault — **the source of truth for WHAT and WHY.** The vault leads. |
|
||||
|
||||
**The old codebase is a spent quarry** (D-050): mined, never tracked, never synced, never canon'd.
|
||||
Writing rewrite code into it would corrupt the very thing being ported from.
|
||||
|
||||
## ⚠ Runtime data isolation — do not weaken
|
||||
|
||||
This project pins its `user://` directory **away from the old prototype's**, which holds the
|
||||
preserved reference seeds, blueprints and batches:
|
||||
|
||||
```
|
||||
user:// → ~/.local/share/islaApocalypse-v2/ ← this project
|
||||
~/.local/share/godot/app_userdata/islaApocalypse/ ← ⛔ the OLD prototype. Never touch.
|
||||
```
|
||||
|
||||
Pinned two ways in `project.godot` (distinct project name **and** `use_custom_user_dir`). After any
|
||||
change to that block, re-run the probe:
|
||||
|
||||
```bash
|
||||
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
|
||||
--path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/UserDirProbe.tscn
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
Core/ math + data only, ENGINE-FREE — depends on nothing above it
|
||||
Server/ authoritative logic — may use Core (empty: Phase 0)
|
||||
Client/ rendering — may use Core (empty: Phase 0)
|
||||
Tools/ the offline generator — ⚠ may NOT use Client (Phase 1 fills it)
|
||||
```
|
||||
|
||||
Each layer has a `README.md` stating its role and its boundary. Read the one for the layer you are
|
||||
about to write in — they carry the reasons, not just the rules.
|
||||
|
||||
## The contracts this phase establishes
|
||||
|
||||
- **★★ The column model** (D-053) — the world as a 2D grid of columns, each a stack of
|
||||
`(material, thickness)` runs. **Air is the top run; there is no air-vs-solid height branch.**
|
||||
→ `Core/Scripts/Column.cs`
|
||||
- **The material schemas** (D-054) — terrain and building, two schemas bridged by a transformation
|
||||
seam that is **deliberately empty.** Identity is a registry key, never an ordinal. Mesh style is
|
||||
per-origin, never a material property. → `Core/Scripts/MaterialRegistry.cs`
|
||||
- **The scaling discipline** — nothing uses a raw pixel number.
|
||||
→ `Core/Scripts/GenerationScale.cs`
|
||||
- **Tooling safety** — every path env-overridable, deletion refused by code.
|
||||
→ `Core/Scripts/ToolingPaths.cs`, `Core/Scripts/FileSafety.cs`
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
dotnet build
|
||||
Godot_v4.7.2-stable_mono_linux.x86_64 --headless --import --path .
|
||||
```
|
||||
|
||||
**Godot 4.7.2** (mono) · **.NET 8** (`net8.0`) · `Godot.NET.Sdk/4.7.2`
|
||||
|
||||
> The reference repo is Godot 4.7.1. This repo deliberately targets **4.7.2** — clean rewrite,
|
||||
> current tooling. 4.7.1 is the *port source*, not a constraint on new code.
|
||||
|
||||
## What this phase is NOT
|
||||
|
||||
No generation, no mesher, no stratigraphy, no biomes, no water, no algorithms of any kind. The
|
||||
pipeline is **2D-maps-first** (D-056): stages 1–5 are flat maps, gotten completely right before a
|
||||
single 3D vertex exists. Building the mesher before the data is right is the specific trap this
|
||||
rewrite exists to undo.
|
||||
20
Server/README.md
Normal file
20
Server/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Server — authoritative logic
|
||||
|
||||
**Holds:** the authoritative simulation. What the world *does*, decided in one place.
|
||||
|
||||
**Boundary:** may use `Core/`. Must not depend on `Client/`.
|
||||
|
||||
## Empty this phase — deliberately
|
||||
|
||||
Phase 0 stands up the skeleton; nothing simulates yet. The folder exists so the first authoritative
|
||||
system has an obvious home and does not get written into `Core/` or a scene.
|
||||
|
||||
## When it fills
|
||||
|
||||
The world is generated **once, offline**, and the runtime **loads** it — the server never
|
||||
regenerates terrain on start (→ D-028, `Design - Tooling - Offline Generation.md`). Every
|
||||
participant reads the *same* immutable blueprint rather than re-deriving it, because two machines
|
||||
that independently compute terrain will eventually disagree about where a mountain is, and in a
|
||||
voxel game that disagreement is a player falling through the world.
|
||||
|
||||
So: **Server loads blueprints. Server does not generate.** Generation is `Tools/`.
|
||||
1
Server/Scripts/.gitkeep
Normal file
1
Server/Scripts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Placeholder so the empty layer folder is tracked. Delete when the first script lands.
|
||||
124
Tools/README.md
Normal file
124
Tools/README.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Tools — the offline generator
|
||||
|
||||
**Holds:** the world generator and its diagnostics. Everything that produces a blueprint, and
|
||||
nothing that consumes one at play time.
|
||||
|
||||
**Boundary — the tools wall:**
|
||||
|
||||
> ### ⚠⚠ TOOLS MAY NOT REFERENCE `Client/`. THE DEPENDENCY RUNS ONE WAY, OR THE WALL IS NOT A WALL.
|
||||
>
|
||||
> No UI, no player controllers, no shaders, no rendering code. `Tools/` may use `Core/`.
|
||||
>
|
||||
> **Two reasons, both from `Design - Tooling - Offline Generation.md`:** a lighter shipped client,
|
||||
> and — less obviously — **not handing players the world-generation logic.** Shipping the generator
|
||||
> lets it be reverse-engineered into an unfair map preview, with the whole island's layout, every
|
||||
> settlement and every route known before anyone sets foot on the beach.
|
||||
>
|
||||
> **Tools emit data; they never write to a live save.** A generator that could touch live server
|
||||
> state is a corruption risk for no benefit.
|
||||
|
||||
## Empty this phase — Phase 1 fills it
|
||||
|
||||
The only thing here now is `Scenes/UserDirProbe.tscn` + `Scripts/UserDirProbe.cs`, a print-and-quit
|
||||
diagnostic that confirms this project's `user://` directory is its own and not the old prototype's.
|
||||
It generates nothing.
|
||||
|
||||
---
|
||||
|
||||
## ⭐ The conventions Phase 1's generator inherits
|
||||
|
||||
These were **earned, not assumed** — each one came out of the prototype's terrain arc.
|
||||
→ `Design - Tooling - Iteration and Batching.md`.
|
||||
|
||||
### 1. `xvfb-run` for unattended generation — NOT `--headless`
|
||||
|
||||
> ### ⚠ `--headless` HANGS on a real generation run.
|
||||
>
|
||||
> The snapshot capture **awaits render frames, and there is no frame loop without a display.**
|
||||
> **Measured, not assumed.** A run left on `--headless` does not fail loudly; it sits there.
|
||||
|
||||
```bash
|
||||
xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 --path ~/celerNexus/islaApocalypse-v2 <scene>
|
||||
```
|
||||
|
||||
A windowed run on the desktop also invites being closed mid-run by whoever is using the machine.
|
||||
|
||||
**The one exception is a job that awaits no frames** — like `UserDirProbe`, which is why that one
|
||||
documents `--headless` explicitly:
|
||||
|
||||
```bash
|
||||
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
|
||||
--path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/UserDirProbe.tscn
|
||||
```
|
||||
|
||||
**If a job awaits a frame, it needs `xvfb-run`. When unsure, use `xvfb-run`** — it is correct in
|
||||
both cases and costs nothing.
|
||||
|
||||
### 2. Environment overrides — safe BY CODE, not by care
|
||||
|
||||
Every path a tool reads or writes resolves through `Core/Scripts/ToolingPaths.cs`, and each has an
|
||||
environment override, so a batch **cannot** read over or write to the developer's live files:
|
||||
|
||||
| Variable | Overrides | Default |
|
||||
|---|---|---|
|
||||
| `ISLA_CONFIG_PATH` | the generation config file | `user://config.json` |
|
||||
| `ISLA_BLUEPRINT_PATH` | blueprints read/written | `user://blueprints` |
|
||||
| `ISLA_OUTPUT_DIR` | generation output (batches live under it) | `user://output` |
|
||||
|
||||
> ⭐ **The point is that it is enforced by CODE, not by care.** A rule that depends on an executor
|
||||
> remembering it will eventually meet an executor who does not.
|
||||
|
||||
There is no second way to obtain these paths. Do not add one.
|
||||
|
||||
### 3. ⚠ File safety — permanent rules
|
||||
|
||||
Enforced by `Core/Scripts/FileSafety.cs`, which throws rather than advises.
|
||||
|
||||
1. **No deletion in the runtime root or anywhere under `batches/`.**
|
||||
2. **Intermediates persist** in a `scratch/` subfolder that is **never cleaned.**
|
||||
3. **The developer's placed files are never touched.**
|
||||
4. **Any deletion is named explicitly in the run report.**
|
||||
|
||||
> ⚠ **These came from a real incident:** an executor admitted it had been deleting the developer's
|
||||
> staged test blueprint. It was owned and fixed **in code.** That is why these are rules and not
|
||||
> guidance.
|
||||
|
||||
### 4. Batch layout
|
||||
|
||||
```
|
||||
batches/NN_<name>/<seed>_<variant>/
|
||||
batches/NN_<name>/INDEX.md
|
||||
batches/NN_<name>/scratch/ ← persistent; never cleaned
|
||||
```
|
||||
|
||||
**A/B comparisons are browsed by a human, and a flat directory of same-named PNGs is not
|
||||
browsable.** The `INDEX.md` is what makes a batch readable a week later.
|
||||
|
||||
⚠ **Batches are OUTPUT.** They live under `ISLA_OUTPUT_DIR` (i.e. under `user://`), **not in this
|
||||
repo.** The `batches/` folder here carries the convention, not the data. No batches are run this
|
||||
phase.
|
||||
|
||||
### 5. Iteration levers, for when the generator exists
|
||||
|
||||
- **Skip the road pass for all terrain and water iteration.** In the prototype this cut a run from
|
||||
~26 min to ~80 s — a **19× cut**, and the difference between iterating on terrain and not
|
||||
iterating on terrain. Blueprints from such a run carry present-but-empty road sections, which is
|
||||
**legitimate, not corrupt.**
|
||||
- **⭐ Build the oracle before the taste-iteration, not after.** Because the classify path was
|
||||
pinned to *uncurved* height, every shaping iteration had a hard automatic correctness check
|
||||
(biome and water maps md5-identical). Five rounds of taste-iteration were safe **because
|
||||
correctness was not being judged by eye.** *Where a future phase has a subjective gate, ask first
|
||||
what the automatic invariant is.*
|
||||
- **Config-gate every shaping change**, with the legacy behaviour surviving on the other side. It
|
||||
keeps changes comparable as an A/B pair, and it means a rejected change costs a flipped default
|
||||
rather than a reverted commit.
|
||||
- **The lever is variants per batch, not speed per pass.** Two Phase B tasks ran ~2 hours each,
|
||||
which looked like the pipeline getting slower. It was not — a single pass stayed at ~2 minutes.
|
||||
The cost was A/B/ablation multiplication. **When a batch is genuinely wide, say so up front.**
|
||||
|
||||
### 6. Scaling discipline
|
||||
|
||||
**Nothing uses a raw pixel number.** Every distance, radius, threshold and noise frequency comes
|
||||
from `Core/Scripts/GenerationScale.cs`. Read its header before adding any constant — including the
|
||||
tightening on **normalized additive noise offsets**, which is a forward-guard for the Phase 1 noise
|
||||
port. → `Design - Tooling - Scaling Discipline.md`.
|
||||
6
Tools/Scenes/UserDirProbe.tscn
Normal file
6
Tools/Scenes/UserDirProbe.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://bqp1x2v3isla0"]
|
||||
|
||||
[ext_resource type="Script" path="res://Tools/Scripts/UserDirProbe.cs" id="1_probe"]
|
||||
|
||||
[node name="UserDirProbe" type="Node"]
|
||||
script = ExtResource("1_probe")
|
||||
68
Tools/Scripts/UserDirProbe.cs
Normal file
68
Tools/Scripts/UserDirProbe.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using Godot;
|
||||
using IslaApocalypse.Core;
|
||||
|
||||
namespace IslaApocalypse.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// A print-and-quit diagnostic: resolves and reports this project's user:// directory, then
|
||||
/// exits. It generates nothing and writes nothing.
|
||||
///
|
||||
/// ═══ WHY IT EXISTS ═══
|
||||
///
|
||||
/// The old prototype is named "islaApocalypse" and owns
|
||||
/// ~/.local/share/godot/app_userdata/islaApocalypse/, which holds the PRESERVED reference
|
||||
/// seeds, blueprints and batches. A v2 project sharing that name would read and clobber them
|
||||
/// at runtime. project.godot pins the isolation two ways (distinct name + custom user dir);
|
||||
/// this probe CONFIRMS the pin actually resolves, rather than trusting that it does.
|
||||
///
|
||||
/// Run it after any change to project.godot's [application] block:
|
||||
///
|
||||
/// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
|
||||
/// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/UserDirProbe.tscn
|
||||
///
|
||||
/// --headless is fine HERE — it hangs only on the snapshot capture's render-frame awaits, and
|
||||
/// this awaits nothing. → Tools/README.md, `Design - Tooling - Iteration and Batching.md`.
|
||||
/// </summary>
|
||||
public partial class UserDirProbe : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
string userDir = OS.GetUserDataDir();
|
||||
|
||||
// Hand Core the engine-resolved directory — Core is engine-free and never asks Godot itself.
|
||||
ToolingPaths.Configure(userDir);
|
||||
|
||||
GD.Print("==================================================================");
|
||||
GD.Print(" user:// PROBE — islaApocalypse-v2");
|
||||
GD.Print("==================================================================");
|
||||
GD.Print($"project name : {ProjectSettings.GetSetting("application/config/name")}");
|
||||
GD.Print($"custom user dir : {ProjectSettings.GetSetting("application/config/use_custom_user_dir")} " +
|
||||
$"(\"{ProjectSettings.GetSetting("application/config/custom_user_dir_name")}\")");
|
||||
GD.Print($"user:// resolves : {userDir}");
|
||||
GD.Print($"globalized : {ProjectSettings.GlobalizePath("user://")}");
|
||||
GD.Print("------------------------------------------------------------------");
|
||||
GD.Print(" ISOLATION CHECK");
|
||||
GD.Print("------------------------------------------------------------------");
|
||||
|
||||
// The one path this project must never resolve to.
|
||||
const string forbidden = "app_userdata/islaApocalypse";
|
||||
bool clash = userDir.Replace('\\', '/').TrimEnd('/').EndsWith(forbidden, System.StringComparison.Ordinal);
|
||||
GD.Print(clash
|
||||
? " ✗ FAIL — resolves to the OLD prototype's dir. Do not run anything else."
|
||||
: " ✓ PASS — not the old prototype's app_userdata/islaApocalypse/.");
|
||||
|
||||
GD.Print("------------------------------------------------------------------");
|
||||
GD.Print(" RESOLVED TOOLING PATHS (env-overridable)");
|
||||
GD.Print("------------------------------------------------------------------");
|
||||
GD.Print(ToolingPaths.Describe());
|
||||
GD.Print("------------------------------------------------------------------");
|
||||
GD.Print($" Core self-check: {MaterialRegistry.TerrainCount} terrain materials, " +
|
||||
$"{MaterialRegistry.BuildingCount} building materials, " +
|
||||
$"{RecipeRegistry.Count} recipes (0 is correct — the seam is deliberately empty).");
|
||||
GD.Print($" Scale self-check: {new GenerationScale(10240)}");
|
||||
GD.Print("==================================================================");
|
||||
|
||||
GetTree().Quit(clash ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Tools/Scripts/UserDirProbe.cs.uid
Normal file
1
Tools/Scripts/UserDirProbe.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bhqfoi5jqjg2b
|
||||
26
Tools/batches/README.md
Normal file
26
Tools/batches/README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# batches/ — convention only; the data lives under `user://`
|
||||
|
||||
This folder documents the batch layout. **It does not hold batches.**
|
||||
|
||||
Real batch output is written under `ISLA_OUTPUT_DIR` (default `user://output/batches`), resolved by
|
||||
`Core/Scripts/ToolingPaths.cs`. Generated worlds are large, regenerable, and do not belong in git.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
batches/NN_<name>/ NN = the task number that ran it
|
||||
├── INDEX.md what varied, what to look at, what was concluded
|
||||
├── scratch/ intermediates — PERSISTENT, never cleaned
|
||||
└── <seed>_<variant>/ one directory per generated world
|
||||
```
|
||||
|
||||
`INDEX.md` is not optional. A/B comparisons are browsed by a human, and a flat directory of
|
||||
same-named PNGs is not browsable.
|
||||
|
||||
## ⚠ No deletions here. Ever.
|
||||
|
||||
`FileSafety.AssertDeletable` refuses any path under the batches root, and it refuses without a
|
||||
stated reason regardless. **Intermediates persist.** Any deletion that does happen elsewhere is
|
||||
named explicitly in the run report.
|
||||
|
||||
→ `Tools/README.md`, `Design - Tooling - Iteration and Batching.md`
|
||||
21
islaApocalypse-v2.csproj
Normal file
21
islaApocalypse-v2.csproj
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Godot.NET.Sdk/4.7.2">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net9.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
|
||||
<RootNamespace>IslaApocalypse</RootNamespace>
|
||||
<Nullable>disable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- Phase 0: the shell must build clean. Warnings are not errors yet — that is a call for a
|
||||
later phase, once the generator exists and the warning surface is known. -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
|
||||
<!-- net8.0 assemblies on a machine carrying only the .NET 10 runtime. Without this the
|
||||
generated runtimeconfig pins Microsoft.NETCore.App 8.0.0 and rolls forward by MINOR
|
||||
only, so the app refuses to start with "You must install .NET 8". The TARGET stays
|
||||
net8.0 as pinned by the task; this only widens what may HOST it. -->
|
||||
<RollForward>Major</RollForward>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
18
islaApocalypse-v2.sln
Normal file
18
islaApocalypse-v2.sln
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "islaApocalypse-v2", "islaApocalypse-v2.csproj", "{6E4C1B2A-9A3D-4C7E-8B21-0D5F4A1C7E90}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6E4C1B2A-9A3D-4C7E-8B21-0D5F4A1C7E90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6E4C1B2A-9A3D-4C7E-8B21-0D5F4A1C7E90}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6E4C1B2A-9A3D-4C7E-8B21-0D5F4A1C7E90}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||
{6E4C1B2A-9A3D-4C7E-8B21-0D5F4A1C7E90}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||
{6E4C1B2A-9A3D-4C7E-8B21-0D5F4A1C7E90}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||
{6E4C1B2A-9A3D-4C7E-8B21-0D5F4A1C7E90}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
30
project.godot
Normal file
30
project.godot
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
; Engine configuration file.
|
||||
; Isla Apocalypse — v2 rewrite (D-049). Phase 0 skeleton.
|
||||
;
|
||||
; ⚠ user:// ISOLATION — DO NOT WEAKEN. The old prototype is named "islaApocalypse" and owns
|
||||
; ~/.local/share/godot/app_userdata/islaApocalypse/, which holds the PRESERVED reference seeds,
|
||||
; blueprints and batches. This project must never resolve there.
|
||||
; 1. application/config/name is distinct ("islaApocalypse-v2"), so name-derivation alone
|
||||
; would already give a separate dir.
|
||||
; 2. use_custom_user_dir + custom_user_dir_name PIN it explicitly, so the isolation survives
|
||||
; any future display-name change rather than depending on derivation.
|
||||
; Changing either line moves the runtime data directory. Confirm the resolved path before you do.
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="islaApocalypse-v2"
|
||||
config/description="Isla Apocalypse — the v2 rewrite (D-049). Phase 0: clean skeleton."
|
||||
config/version="0.1.0"
|
||||
config/use_custom_user_dir=true
|
||||
config/custom_user_dir_name="islaApocalypse-v2"
|
||||
config/features=PackedStringArray("4.7", "C#", "Forward Plus")
|
||||
|
||||
[dotnet]
|
||||
|
||||
project/assembly_name="islaApocalypse-v2"
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="forward_plus"
|
||||
Loading…
Reference in a new issue