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; /// /// Runs the world-generation pipeline on a background thread and shows per-stage progress. /// Transitions to WorldMapScreen when generation is complete. /// 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)."); } }