using System;
using System.IO;
namespace IslaApocalypse.Core
{
///
/// ⭐ 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. → .
///
/// ═══ 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 . 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.
///
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;
///
/// 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.
///
public static void Configure(string userDataDir)
{
if (string.IsNullOrWhiteSpace(userDataDir))
throw new ArgumentException("A user data directory is required.", nameof(userDataDir));
_userDataDir = userDataDir;
}
/// The resolved user data directory, or the working directory if none was configured.
public static string UserDataDir => _userDataDir ?? Directory.GetCurrentDirectory();
/// Whether has been called. Tooling should assert this before a run.
public static bool IsConfigured => _userDataDir != null;
/// The generation config file. Override: ISLA_CONFIG_PATH.
public static string ConfigPath =>
Override(ConfigPathVar) ?? Path.Combine(UserDataDir, "config.json");
/// Where blueprints are read from and written to. Override: ISLA_BLUEPRINT_PATH.
public static string BlueprintPath =>
Override(BlueprintPathVar) ?? Path.Combine(UserDataDir, "blueprints");
/// Where a generation run writes its output. Override: ISLA_OUTPUT_DIR.
public static string OutputDir =>
Override(OutputDirVar) ?? Path.Combine(UserDataDir, "output");
///
/// The batches root. → `Design - Tooling - Iteration and Batching.md`:
/// batches/NN_<name>/<seed>_<variant>/, 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. → .
///
public static string BatchesRoot => Path.Combine(OutputDir, "batches");
///
/// 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.
///
public static string BatchScratch(string batchDir) => Path.Combine(batchDir, "scratch");
///
/// ⭐ A BATCH ROOT: batches/<task>_<descriptor>/.
///
/// ═══ ⚠⚠ THE PREFIX IS THE AUTHORING TASK NUMBER. IT IS NOT A COUNTER. ═══
///
/// 04_review means "the batch task 04 authored". It does NOT mean "the fifth batch".
/// A task that produces six batches produces six 04_* folders, not 04_ through
/// 09_.
///
/// This is enforced here, in code, because it already drifted once: tasks 02 and 03 used the
/// prefix as a global running counter and produced 00_smoke … 05_trophy_10240
/// across two tasks, so nothing in the folder name said which task made what. Passing the
/// task number as an explicit argument — rather than letting a caller compose a free-form
/// string — is what makes the convention unbreakable rather than remembered.
/// → `Design - Tooling - Iteration and Batching.md`.
///
/// The descriptor must NOT carry its own numeric prefix; that is the mistake this method
/// exists to prevent, so it is refused rather than silently accepted.
///
public static string BatchRoot(int taskNumber, string descriptor)
{
if (taskNumber < 0)
throw new ArgumentOutOfRangeException(nameof(taskNumber), taskNumber,
"A batch is named for the task that authored it; there is no negative task.");
if (string.IsNullOrWhiteSpace(descriptor))
throw new ArgumentException("A batch needs a descriptor — '04_' alone is not browsable.", nameof(descriptor));
string d = descriptor.Trim();
// Refuse "04_review", "4_review", "05_foo" — the caller is re-adding a prefix, which is
// exactly how the counter drifted. The task number is this method's job, not theirs.
int us = d.IndexOf('_');
if (us > 0 && int.TryParse(d.Substring(0, us), out _))
throw new ArgumentException(
$"Descriptor '{d}' starts with its own numeric prefix. Pass the task number as " +
$"taskNumber and the descriptor WITHOUT one (e.g. \"review\", not \"04_review\") — " +
"the prefix is composed here so it cannot drift.", nameof(descriptor));
return Path.Combine(BatchesRoot, $"{taskNumber:D2}_{d}");
}
///
/// A variant directory inside a batch:
/// batches/<task>_<descriptor>/<seed>_<variant>/.
///
public static string BatchDir(int taskNumber, string descriptor, long seed, string variant)
=> Path.Combine(BatchRoot(taskNumber, descriptor), $"{seed}_{variant}");
private static string Override(string variable)
{
string v = Environment.GetEnvironmentVariable(variable);
return string.IsNullOrWhiteSpace(v) ? null : v;
}
/// Every resolved path, for a run report's header. Print this; it is cheap and it has caught things.
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}]" : "";
}
}