using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Godot; using Theriapolis.Core.Persistence; using Theriapolis.Core.World.Generation; using Theriapolis.GodotHost.Platform; using Theriapolis.GodotHost.UI; namespace Theriapolis.GodotHost.Scenes; /// /// M7.1 — runs the 23-stage worldgen pipeline on a background thread /// and shows per-stage progress. Transitions to /// (which M7.2 will replace with the real PlayScreen) on completion. /// /// Mirrors Theriapolis.Game/Screens/WorldGenProgressScreen.cs: /// same volatile-field hand-off between the worker and the UI thread, /// same soft stage-hash warning when restoring from a saved header. /// /// Inputs (from ): /// - Seed — required. /// - PendingHeader — present when restoring from save; triggers /// the post-gen stage-hash diff against WorldState.StageHashes. /// /// Outputs: /// - session.Ctx set on success; consumed by the next screen. /// /// Escape during generation: cancel the worker (honoured at the next /// stage boundary), return to Title. /// public partial class WorldGenProgressScreen : Control { private WorldGenContext? _ctx; private Task? _genTask; private CancellationTokenSource? _cts; private volatile float _progress; private volatile string _stageName = "Initialising…"; private volatile bool _complete; private volatile string? _error; private Label _titleLabel = null!; private ProgressBar _progressBar = null!; private Label _stageLabel = null!; private bool _transitioned; public override void _Ready() { Theme = CodexTheme.Build(); SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); // Backing panel so the dark palette Bg fills the viewport (the // Control itself paints nothing). Mirrors TitleScreen.cs. var bg = new Panel { MouseFilter = MouseFilterEnum.Ignore }; AddChild(bg); bg.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); MoveChild(bg, 0); BuildUI(); StartGeneration(); } private void BuildUI() { var center = new CenterContainer { MouseFilter = MouseFilterEnum.Ignore }; AddChild(center); center.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect); var col = new VBoxContainer { CustomMinimumSize = new Vector2(480, 0) }; col.AddThemeConstantOverride("separation", 14); center.AddChild(col); var session = GameSession.From(this); col.AddChild(new Label { Text = "FORGING THE WORLD", ThemeTypeVariation = "Eyebrow", HorizontalAlignment = HorizontalAlignment.Center, }); _titleLabel = new Label { Text = $"Seed 0x{session.Seed:X}", ThemeTypeVariation = "H2", HorizontalAlignment = HorizontalAlignment.Center, }; col.AddChild(_titleLabel); _progressBar = new ProgressBar { MinValue = 0, MaxValue = 1, Step = 0.001, ShowPercentage = true, CustomMinimumSize = new Vector2(0, 22), }; col.AddChild(_progressBar); _stageLabel = new Label { Text = "Starting…", HorizontalAlignment = HorizontalAlignment.Center, AutowrapMode = TextServer.AutowrapMode.WordSmart, }; col.AddChild(_stageLabel); col.AddChild(new Label { Text = "Esc to cancel · returns to title.", ThemeTypeVariation = "Eyebrow", HorizontalAlignment = HorizontalAlignment.Center, }); } private void StartGeneration() { _cts = new CancellationTokenSource(); var token = _cts.Token; var session = GameSession.From(this); ulong seed = session.Seed; string dataDir = ContentPaths.DataDir; _genTask = Task.Run(() => { try { var ctx = new WorldGenContext(seed, dataDir) { ProgressCallback = (name, frac) => { _stageName = name; _progress = frac; }, Log = msg => GD.Print($"[worldgen] {msg}"), }; WorldGenerator.RunAll(ctx); if (token.IsCancellationRequested) return; _ctx = ctx; _complete = true; } catch (Exception ex) { var inner = ex is AggregateException ae ? ae.Flatten().InnerException ?? ex : ex; _error = inner.ToString(); } }, token); } public override void _Process(double delta) { if (_transitioned) return; if (_error is not null) { ShowError(_error); return; } if (_complete && _ctx is not null) { _transitioned = true; Transition(); return; } _progressBar.Value = _progress; _stageLabel.Text = _stageName; } private void Transition() { var session = GameSession.From(this); if (session.PendingHeader is not null) CompareStageHashes(session.PendingHeader); session.Ctx = _ctx; // M7.2 — the real PlayScreen. PlayScreenStub is kept around as // a fallback for any future code path that hasn't been wired up // (e.g. mid-development load flows), but the live hand-off lands // in the play view. SwapTo(new PlayScreen()); } private void SwapTo(Node next) { var parent = GetParent(); if (parent is null) return; foreach (Node sibling in parent.GetChildren()) if (sibling != this) sibling.QueueFree(); parent.AddChild(next); QueueFree(); } private void ShowError(string error) { _stageLabel.Text = "ERROR — press Escape to return to title"; _progressBar.Value = 0; // Crop to the first line + 100 chars so the title label stays legible. int newline = error.IndexOf('\n'); string headline = newline > 0 ? error[..newline] : error; _titleLabel.Text = headline.Length > 100 ? headline[..100] + "…" : headline; try { string logPath = ProjectSettings.GlobalizePath("user://worldgen_error.log"); File.WriteAllText(logPath, $"[{DateTime.Now:u}] WorldGen ERROR\n{error}\n"); GD.PushError($"[worldgen] Wrote {logPath}"); } catch { /* best-effort */ } } public override void _UnhandledInput(InputEvent @event) { if (@event is InputEventKey { Pressed: true, Keycode: Key.Escape }) { _cts?.Cancel(); BackToTitle(); } } public override void _ExitTree() { _cts?.Cancel(); _cts?.Dispose(); _cts = null; } private void BackToTitle() { var session = GameSession.From(this); session.ClearPending(); session.Ctx = null; SwapTo(new TitleScreen()); } private void CompareStageHashes(SaveHeader savedHeader) { if (_ctx is null) return; int mismatches = 0; foreach (var kv in _ctx.World.StageHashes) { if (!savedHeader.StageHashes.TryGetValue(kv.Key, out var sv)) continue; string current = $"0x{kv.Value:X}"; if (!string.Equals(sv, current, StringComparison.OrdinalIgnoreCase)) { mismatches++; GD.PushWarning($"[save-migration] Stage '{kv.Key}' hash drift: saved={sv}, current={current}"); } } if (mismatches > 0) GD.PushWarning($"[save-migration] {mismatches} stage(s) drifted; loading anyway (soft)."); } }