Files
TheriapolisV3/Theriapolis.Godot/Platform/SavePaths.cs
T
Christopher Wiebe 8e2efdd878 M7.3: Save/load round-trip — F5 quicksave, Continue → slot picker
SavePaths ported verbatim from Theriapolis.Game/Platform/. Same OS
directories as MonoGame (%LOCALAPPDATA%\Theriapolis\Saves on
Windows, ~/Library/Application Support/Theriapolis/Saves on macOS,
$XDG_DATA_HOME/Theriapolis/saves on Linux) so saves round-trip
across the two builds without migration.

PlayScreen save layer. Wired PlayerReputation + Flags + QuestEngine
+ QuestContext + _killedByChunk + _pendingEncounterRestore in
_Ready, even though M7.3 doesn't actively drive any of those —
they're round-trip-required, so a save written by the MonoGame
build with non-empty rep/flags/quest state loads here and re-saves
without data loss. SaveTo/BuildHeader/CaptureBody/ApplyRestoredBody
are field-for-field ports of the MonoGame methods (Phase 5 M3 + M5,
Phase 6 M2 + M4); CaptureBody flushes the streamer first so chunk
deltas land in the store before serialisation. HandleChunkLoaded
now honours _killedByChunk so a killed spawn stays dead across
chunk reload + save round-trip.

F5 quicksaves to the autosave slot. Save-flash toast (bottom-center
Label, fade-out via Modulate.A) confirms each write.

_Ready branches on session.PendingRestore: when set (load path),
calls ApplyRestoredBody and skips the new-game spawn; otherwise
spawns at the Tier-1 anchor with the M6 character. The
mid-combat encounter snapshot is captured on save but the push to
CombatHUDScreen is the M8 stub (logs a console diagnostic).

SaveLoadScreen — load-only slot picker. Header-only deserialise
per row (SaveCodec.DeserializeHeaderOnly reads just the JSON
prefix, body untouched), so opening the picker is cheap even with
many large saves. Slot label matches MonoGame's SlotLabel() format
exactly. Incompatible / unreadable rows render disabled with the
reason inline.

TitleScreen Continue. Enable-gate replaced — was "user://character.json
exists" (M7.1 placeholder), now scans SavesDir for *.trps + checks
SaveCodec.IsCompatible. OnContinue swaps to SaveLoadScreen instead
of the print stub. Manual play-test loop confirmed: F5 in run #1,
quit, relaunch, Continue → Autosave row → progress bar → PlayScreen
with character restored at saved tile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 19:03:18 -07:00

64 lines
2.5 KiB
C#

using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Theriapolis.GodotHost.Platform;
/// <summary>
/// OS-aware save directory resolution. Direct port of
/// <c>Theriapolis.Game/Platform/SavePaths.cs</c>; deliberately uses the
/// same directories as the MonoGame build so saves are interoperable
/// across the two ports.
///
/// Locations:
/// Windows: <c>%LOCALAPPDATA%\Theriapolis\Saves\</c>
/// macOS: <c>~/Library/Application Support/Theriapolis/Saves/</c>
/// Linux: <c>$XDG_DATA_HOME/Theriapolis/saves/</c> (default
/// <c>~/.local/share/Theriapolis/saves/</c>)
/// </summary>
public static class SavePaths
{
/// <summary>Top-level Theriapolis save directory. Created on first
/// call if missing.</summary>
public static string SavesDir
{
get
{
string dir = ResolveBase();
Directory.CreateDirectory(dir);
return dir;
}
}
public static string SlotPath(int slot) => Path.Combine(SavesDir, $"slot_{slot:D2}.trps");
public static string AutosavePath() => Path.Combine(SavesDir, "autosave.trps");
private static string ResolveBase()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Theriapolis", "Saves");
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library", "Application Support", "Theriapolis", "Saves");
// Linux + others: respect XDG_DATA_HOME, fall back to ~/.local/share.
string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME") ?? "";
if (string.IsNullOrEmpty(xdg))
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".local", "share");
return Path.Combine(xdg, "Theriapolis", "saves");
}
/// <summary>Atomic-rename file write so a crash mid-save can't
/// corrupt the slot.</summary>
public static void WriteAtomic(string path, byte[] bytes)
{
string dir = Path.GetDirectoryName(path)!;
Directory.CreateDirectory(dir);
string tmp = path + ".tmp";
File.WriteAllBytes(tmp, bytes);
if (File.Exists(path)) File.Replace(tmp, path, destinationBackupFileName: null);
else File.Move(tmp, path);
}
}