252 lines
7.9 KiB
C#
252 lines
7.9 KiB
C#
|
|
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;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// M7.1 — runs the 23-stage worldgen pipeline on a background thread
|
||
|
|
/// and shows per-stage progress. Transitions to <see cref="PlayScreenStub"/>
|
||
|
|
/// (which M7.2 will replace with the real PlayScreen) on completion.
|
||
|
|
///
|
||
|
|
/// Mirrors <c>Theriapolis.Game/Screens/WorldGenProgressScreen.cs</c>:
|
||
|
|
/// 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 <see cref="GameSession"/>):
|
||
|
|
/// - <c>Seed</c> — required.
|
||
|
|
/// - <c>PendingHeader</c> — present when restoring from save; triggers
|
||
|
|
/// the post-gen stage-hash diff against <c>WorldState.StageHashes</c>.
|
||
|
|
///
|
||
|
|
/// Outputs:
|
||
|
|
/// - <c>session.Ctx</c> set on success; consumed by the next screen.
|
||
|
|
///
|
||
|
|
/// Escape during generation: cancel the worker (honoured at the next
|
||
|
|
/// stage boundary), return to Title.
|
||
|
|
/// </summary>
|
||
|
|
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).");
|
||
|
|
}
|
||
|
|
}
|