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.
85 lines
3.4 KiB
C#
85 lines
3.4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|