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_smoke05_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}]" : ""; } }