islaApocalypse-v2/Core/Scripts/ToolingPaths.cs
beezm 9107b0822b 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.
2026-08-19 20:21:12 -04:00

114 lines
5.5 KiB
C#

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_&lt;name&gt;/&lt;seed&gt;_&lt;variant&gt;/</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_&lt;name&gt;/&lt;seed&gt;_&lt;variant&gt;/</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}]" : "";
}
}