Files
TheriapolisV3/Theriapolis.Game/Screens/WorldGenProgressScreen.cs
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

197 lines
6.6 KiB
C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Myra;
using Myra.Graphics2D.UI;
using Theriapolis.Core.Persistence;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.World.Generation;
namespace Theriapolis.Game.Screens;
/// <summary>
/// Runs the world-generation pipeline on a background thread and shows per-stage progress.
/// Transitions to WorldMapScreen when generation is complete.
/// </summary>
public sealed class WorldGenProgressScreen : IScreen
{
private readonly ulong _seed;
private readonly SaveBody? _restoreFromSave;
private readonly SaveHeader? _savedHeader;
private readonly Character? _pendingCharacter;
private readonly string? _pendingName;
private Game1 _game = null!;
private Desktop _desktop = null!;
private Label? _stageLabel;
private Label? _progressLabel;
private WorldGenContext? _ctx;
private Task? _genTask;
private volatile float _progress;
private volatile string _stageName = "Initialising...";
private volatile bool _complete;
private volatile string? _error;
public WorldGenProgressScreen(
ulong seed,
SaveBody? restoreFromSave = null,
SaveHeader? savedHeader = null,
Character? pendingCharacter = null,
string? pendingName = null)
{
_seed = seed;
_restoreFromSave = restoreFromSave;
_savedHeader = savedHeader;
_pendingCharacter = pendingCharacter;
_pendingName = pendingName;
}
public void Initialize(Game1 game)
{
_game = game;
BuildUI();
StartGeneration();
}
private void BuildUI()
{
var root = new VerticalStackPanel
{
Spacing = 16,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
root.Widgets.Add(new Label
{
Text = $"Generating world... (seed: 0x{_seed:X})",
HorizontalAlignment = HorizontalAlignment.Center,
});
_progressLabel = new Label
{
Text = "[ ] 0%",
HorizontalAlignment = HorizontalAlignment.Center,
};
root.Widgets.Add(_progressLabel);
_stageLabel = new Label
{
Text = "Starting...",
HorizontalAlignment = HorizontalAlignment.Center,
};
root.Widgets.Add(_stageLabel);
_desktop = new Desktop { Root = root };
}
private void StartGeneration()
{
string dataDir = _game.ContentDataDirectory;
_genTask = Task.Run(() =>
{
try
{
_ctx = new WorldGenContext(_seed, dataDir)
{
ProgressCallback = (name, frac) =>
{
_stageName = name;
_progress = frac;
},
Log = msg => System.Diagnostics.Debug.WriteLine(msg),
};
WorldGenerator.RunAll(_ctx);
_complete = true;
}
catch (Exception ex)
{
// Unwrap AggregateException to get the real inner message
var inner = ex is AggregateException ae ? ae.Flatten().InnerException ?? ex : ex;
_error = inner.ToString(); // full type + message + stack trace
}
});
}
public void Update(GameTime gameTime)
{
if (_error is not null)
{
// Show error on screen so it is visible; do NOT pop automatically.
System.Diagnostics.Debug.WriteLine($"[WorldGen ERROR] {_error}");
if (_stageLabel is not null) _stageLabel.Text = "ERROR — press Escape to go back";
if (_progressLabel is not null) _progressLabel.Text = _error.Length > 80
? _error[..80] + "..."
: _error;
// Write full error to a log file next to the exe for post-mortem diagnosis
try
{
string logPath = Path.Combine(
AppContext.BaseDirectory, "worldgen_error.log");
File.WriteAllText(logPath,
$"[{DateTime.Now:u}] WorldGen ERROR\n{_error}\n");
}
catch { /* best-effort */ }
// Only pop when the user presses Escape
if (Microsoft.Xna.Framework.Input.Keyboard.GetState()
.IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Escape))
_game.Screens.Pop();
return;
}
if (_complete && _ctx is not null)
{
// Stage-hash check: a soft warning is fine for Phase 4. We log
// mismatches but proceed — saves anchored only by player position
// and chunk deltas tolerate small worldgen drift.
if (_savedHeader is not null) CompareStageHashes();
if (_restoreFromSave is not null)
_game.Screens.Push(new PlayScreen(_ctx, _restoreFromSave));
else if (_pendingCharacter is not null)
_game.Screens.Push(new PlayScreen(_ctx, _pendingCharacter, _pendingName ?? "Wanderer"));
else
_game.Screens.Push(new PlayScreen(_ctx));
_complete = false;
return;
}
// Update UI progress on game thread
int pct = (int)(_progress * 100f);
int filled = pct / 10;
string bar = new string('#', filled) + new string(' ', 10 - filled);
if (_progressLabel is not null) _progressLabel.Text = $"[{bar}] {pct,3}%";
if (_stageLabel is not null) _stageLabel.Text = _stageName;
}
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
_game.GraphicsDevice.Clear(new Color(10, 10, 20));
_desktop.Render();
}
public void Deactivate() { }
public void Reactivate() { }
private void CompareStageHashes()
{
if (_savedHeader is null || _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++;
System.Diagnostics.Debug.WriteLine(
$"[Save migration] Stage '{kv.Key}' hash drift: saved={sv}, current={current}");
}
}
if (mismatches > 0)
System.Diagnostics.Debug.WriteLine(
$"[Save migration] {mismatches} stage(s) drifted; loading anyway (soft).");
}
}