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>
This commit is contained in:
+196
@@ -0,0 +1,196 @@
|
||||
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).");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user