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; /// /// 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..; reads each /// slot's header (cheap) for the label and disables incompatible / /// unreadable rows. /// /// On slot click: deserialise, stash the body + header + seed on /// , swap to /// which will hand off to 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. /// 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 = SaveSlotFormat.FormatRow(label, header); clickable = true; } else { text = $"{label} — "; } } catch (Exception ex) { text = $"{label} — "; } } else { text = $"{label} — "; } 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(); } } }