172 lines
5.5 KiB
C#
172 lines
5.5 KiB
C#
|
|
using System;
|
||
|
|
using System.IO;
|
||
|
|
using Godot;
|
||
|
|
using Theriapolis.Core;
|
||
|
|
using Theriapolis.Core.Persistence;
|
||
|
|
using Theriapolis.GodotHost.Platform;
|
||
|
|
using Theriapolis.GodotHost.UI;
|
||
|
|
|
||
|
|
namespace Theriapolis.GodotHost.Scenes;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// M7.3 — slot picker for *load*. Pushed by TitleScreen's "Continue"
|
||
|
|
/// when at least one compatible save exists. Lists the autosave row
|
||
|
|
/// followed by slots 1..<see cref="C.SAVE_SLOT_COUNT"/>; reads each
|
||
|
|
/// slot's header (cheap) for the label and disables incompatible /
|
||
|
|
/// unreadable rows.
|
||
|
|
///
|
||
|
|
/// On slot click: deserialise, stash the body + header + seed on
|
||
|
|
/// <see cref="GameSession"/>, swap to <see cref="WorldGenProgressScreen"/>
|
||
|
|
/// which will hand off to <see cref="PlayScreen"/> with the
|
||
|
|
/// restore-from-save path.
|
||
|
|
///
|
||
|
|
/// Save-from-pause (write) is M7.4 territory and intentionally lives
|
||
|
|
/// in a separate widget — keeps each picker single-purpose.
|
||
|
|
/// </summary>
|
||
|
|
public partial class SaveLoadScreen : Control
|
||
|
|
{
|
||
|
|
public override void _Ready()
|
||
|
|
{
|
||
|
|
Theme = CodexTheme.Build();
|
||
|
|
SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||
|
|
|
||
|
|
var bg = new Panel { MouseFilter = MouseFilterEnum.Ignore };
|
||
|
|
AddChild(bg);
|
||
|
|
bg.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||
|
|
MoveChild(bg, 0);
|
||
|
|
|
||
|
|
BuildUI();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void BuildUI()
|
||
|
|
{
|
||
|
|
var center = new CenterContainer { MouseFilter = MouseFilterEnum.Ignore };
|
||
|
|
AddChild(center);
|
||
|
|
center.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||
|
|
|
||
|
|
var col = new VBoxContainer { CustomMinimumSize = new Vector2(520, 0) };
|
||
|
|
col.AddThemeConstantOverride("separation", 10);
|
||
|
|
center.AddChild(col);
|
||
|
|
|
||
|
|
col.AddChild(new Label
|
||
|
|
{
|
||
|
|
Text = "LOAD GAME",
|
||
|
|
ThemeTypeVariation = "H2",
|
||
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Autosave row first; numbered slots after.
|
||
|
|
AddSlotRow(col, "Autosave", SavePaths.AutosavePath());
|
||
|
|
for (int i = 1; i <= C.SAVE_SLOT_COUNT; i++)
|
||
|
|
AddSlotRow(col, $"Slot {i:D2}", SavePaths.SlotPath(i));
|
||
|
|
|
||
|
|
var spacer = new Control { CustomMinimumSize = new Vector2(0, 12) };
|
||
|
|
col.AddChild(spacer);
|
||
|
|
|
||
|
|
var back = new Button
|
||
|
|
{
|
||
|
|
Text = "← Back",
|
||
|
|
CustomMinimumSize = new Vector2(220, 40),
|
||
|
|
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
|
||
|
|
};
|
||
|
|
back.Pressed += BackToTitle;
|
||
|
|
col.AddChild(back);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void AddSlotRow(VBoxContainer parent, string label, string path)
|
||
|
|
{
|
||
|
|
string text;
|
||
|
|
bool clickable = false;
|
||
|
|
bool exists = File.Exists(path);
|
||
|
|
if (exists)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var bytes = File.ReadAllBytes(path);
|
||
|
|
var header = SaveCodec.DeserializeHeaderOnly(bytes);
|
||
|
|
if (SaveCodec.IsCompatible(header))
|
||
|
|
{
|
||
|
|
text = $"{label} — {header.SlotLabel()}";
|
||
|
|
clickable = true;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
text = $"{label} — <v{header.Version}: {SaveCodec.IncompatibilityReason(header)}>";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
text = $"{label} — <unreadable: {ex.Message}>";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
text = $"{label} — <empty>";
|
||
|
|
}
|
||
|
|
|
||
|
|
var btn = new Button
|
||
|
|
{
|
||
|
|
Text = text,
|
||
|
|
CustomMinimumSize = new Vector2(0, 40),
|
||
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||
|
|
Disabled = !clickable,
|
||
|
|
Alignment = HorizontalAlignment.Left,
|
||
|
|
};
|
||
|
|
if (clickable) btn.Pressed += () => LoadSlot(path);
|
||
|
|
parent.AddChild(btn);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void LoadSlot(string path)
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var bytes = File.ReadAllBytes(path);
|
||
|
|
var (header, body) = SaveCodec.Deserialize(bytes);
|
||
|
|
if (!SaveCodec.IsCompatible(header))
|
||
|
|
{
|
||
|
|
GD.PushError($"[saveload] Refused incompatible save at {path}: "
|
||
|
|
+ SaveCodec.IncompatibilityReason(header));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var session = GameSession.From(this);
|
||
|
|
session.Seed = header.ParseSeed();
|
||
|
|
session.PendingRestore = body;
|
||
|
|
session.PendingHeader = header;
|
||
|
|
session.PendingCharacter = null; // restore path supplies it via body
|
||
|
|
|
||
|
|
// Swap Title → WorldGenProgress (which will swap to PlayScreen
|
||
|
|
// once the pipeline finishes and stage-hash drift is checked).
|
||
|
|
var parent = GetParent();
|
||
|
|
if (parent is null) return;
|
||
|
|
foreach (Node sibling in parent.GetChildren())
|
||
|
|
if (sibling != this) sibling.QueueFree();
|
||
|
|
parent.AddChild(new WorldGenProgressScreen());
|
||
|
|
QueueFree();
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
GD.PushError($"[saveload] Failed to load {path}: {ex}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void BackToTitle()
|
||
|
|
{
|
||
|
|
var parent = GetParent();
|
||
|
|
if (parent is null) return;
|
||
|
|
foreach (Node sibling in parent.GetChildren())
|
||
|
|
if (sibling != this) sibling.QueueFree();
|
||
|
|
parent.AddChild(new TitleScreen());
|
||
|
|
QueueFree();
|
||
|
|
}
|
||
|
|
|
||
|
|
public override void _UnhandledInput(InputEvent @event)
|
||
|
|
{
|
||
|
|
if (@event is InputEventKey { Pressed: true, Keycode: Key.Escape })
|
||
|
|
{
|
||
|
|
GetViewport().SetInputAsHandled();
|
||
|
|
BackToTitle();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|