8e2efdd878
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>
64 lines
2.5 KiB
C#
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);
|
|
}
|
|
}
|