Files

422 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Extensions;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
using VContainer;
using SaveGame = BriarQueen.Data.IO.Saves.SaveGame;
namespace BriarQueen.Framework.Managers.IO
{
public class SaveManager : IDisposable, IManager
{
private const int MAX_RETRY_COUNT = 3;
private const int RETRY_DELAY_MS = 100;
private readonly EventCoordinator _eventCoordinator;
private readonly object _saveLock = new();
private CancellationTokenSource _currentSaveCts;
private DateTime _lastSaveTime;
[Inject]
public SaveManager(EventCoordinator eventCoordinator)
{
_eventCoordinator = eventCoordinator;
}
public bool IsGameLoaded { get; private set; }
public SaveGame CurrentSave { get; set; }
public void Dispose()
{
_eventCoordinator.Unsubscribe<RequestGameSaveEvent>(OnRequestedSave);
_currentSaveCts?.Cancel();
}
public event Action<SaveGame> OnSaveGameLoaded;
public event Action OnSaveGameSaved;
// Existing synchronous hook
public event Action<SaveGame> OnSaveRequested;
// New async hook that runs BEFORE cloning/serialization
public event Func<UniTask> OnBeforeSaveRequestedAsync;
public bool Initialized { get; private set; }
public void Initialize()
{
_eventCoordinator.Subscribe<RequestGameSaveEvent>(OnRequestedSave);
Initialized = true;
}
private void OnRequestedSave(RequestGameSaveEvent evt)
{
SaveGameDataLatest().Forget();
}
/// <summary>
/// Queues the latest save and cancels any in-progress save.
/// </summary>
public async UniTask SaveGameDataLatest()
{
CancellationTokenSource oldCts = null;
CancellationTokenSource myCts;
lock (_saveLock)
{
if (_currentSaveCts != null)
{
oldCts = _currentSaveCts;
_currentSaveCts.Cancel();
}
myCts = new CancellationTokenSource();
_currentSaveCts = myCts;
}
oldCts?.Dispose();
try
{
await SaveGameDataInternal(myCts.Token);
}
catch (OperationCanceledException)
{
// A newer save was requested; ignore.
}
finally
{
lock (_saveLock)
{
if (ReferenceEquals(_currentSaveCts, myCts))
{
_currentSaveCts.Dispose();
_currentSaveCts = null;
}
}
myCts.Dispose();
}
}
private async UniTask SaveGameDataInternal(CancellationToken ct)
{
if ((DateTime.UtcNow - _lastSaveTime).TotalMilliseconds < 250)
{
Debug.Log("[SaveManager] Last save within 250ms, skipping.");
return;
}
if (CurrentSave == null)
CurrentSave = new SaveGame { SaveFileName = "NewGame" };
// NEW: let systems write into CurrentSave before the clone is made
await InvokeBeforeSaveRequestedAsync();
var saveClone = CurrentSave.DeepClone();
OnSaveRequested?.Invoke(saveClone);
Debug.Log($"[SaveManager] After OnSaveRequested, clone inventory count = {saveClone.InventoryData?.Count ?? 0}");
byte[] binaryData = null;
await UniTask.SwitchToThreadPool();
try
{
binaryData = MemoryPackSerializer.Serialize(saveClone);
}
finally
{
await UniTask.SwitchToMainThread(ct);
}
if (binaryData == null || binaryData.Length == 0)
{
Debug.LogWarning("[SaveManager] Empty serialized data, skipping save.");
return;
}
var saveDir = FilePaths.SaveDataFolder;
var backupDir = FilePaths.SaveBackupDataFolder;
Directory.CreateDirectory(saveDir);
Directory.CreateDirectory(backupDir);
var saveFileName = CurrentSave.SaveFileName + ".sav";
var saveFilePath = Path.Combine(saveDir, saveFileName);
var backupFilePath = Path.Combine(backupDir, saveFileName);
var tempSavePath = saveFilePath + ".tmp";
var attempt = 0;
var success = false;
while (attempt < MAX_RETRY_COUNT && !success)
{
attempt++;
try
{
await File.WriteAllBytesAsync(tempSavePath, binaryData, ct);
if (File.Exists(saveFilePath))
File.Replace(tempSavePath, saveFilePath, backupFilePath);
else
File.Move(tempSavePath, saveFilePath);
success = true;
}
catch (Exception ex)
{
Debug.LogError($"[SaveManager] Save attempt {attempt} failed: {ex}");
await UniTask.Delay(RETRY_DELAY_MS, cancellationToken: ct);
}
}
if (!success)
{
Debug.LogError("[SaveManager] All save attempts failed, save aborted.");
return;
}
CurrentSave = saveClone;
IsGameLoaded = true;
_lastSaveTime = DateTime.UtcNow;
OnSaveGameSaved?.Invoke();
Debug.Log($"[SaveManager] Save complete: {CurrentSave.SaveFileName}");
}
private async UniTask InvokeBeforeSaveRequestedAsync()
{
var handlers = OnBeforeSaveRequestedAsync;
if (handlers == null)
return;
foreach (Func<UniTask> handler in handlers.GetInvocationList())
{
try
{
await handler();
}
catch (Exception ex)
{
Debug.LogError($"[SaveManager] OnBeforeSaveRequestedAsync handler failed: {ex}");
}
}
}
public async UniTask CreateNewSaveGame(string saveFileName)
{
CurrentSave = new SaveGame
{
SaveFileName = saveFileName,
SaveVersion = "0.0.1a",
OpeningCinematicPlayed = false
};
IsGameLoaded = true;
OnSaveGameLoaded?.Invoke(CurrentSave);
await SaveGameDataLatest();
}
public List<(string FileName, DateTime LastModified)> GetAvailableSaves()
{
var saveDir = FilePaths.SaveDataFolder;
if (!Directory.Exists(saveDir)) return new List<(string, DateTime)>();
try
{
return Directory.GetFiles(saveDir, "*.sav")
.Select(file => (Path.GetFileNameWithoutExtension(file),
File.GetLastWriteTimeUtc(file))
)
.OrderByDescending(x => x.Item2)
.ToList();
}
catch (Exception e)
{
Debug.LogError($"[SaveManager] Failed to enumerate saves: {e.Message}");
return new List<(string, DateTime)>();
}
}
public async UniTask LoadLatestSave()
{
var available = GetAvailableSaves();
if (available.Count > 0)
{
await LoadGameData(available[0].FileName);
}
else
{
Debug.LogWarning("[SaveManager] No save files found. Creating new game.");
await CreateNewSaveGame("NewGame");
}
}
public async UniTask LoadGameData(string saveFileName)
{
var mainPath = Path.Combine(FilePaths.SaveDataFolder, saveFileName + ".sav");
var backupPath = Path.Combine(FilePaths.SaveBackupDataFolder, saveFileName + ".sav");
var loadedSave = await LoadFromFileAsync(mainPath);
if (loadedSave == null)
{
Debug.LogWarning($"[SaveManager] Main save load failed, trying backup: {saveFileName}");
loadedSave = await LoadFromFileAsync(backupPath);
if (loadedSave != null)
{
CurrentSave = loadedSave;
await SaveGameDataLatest();
Debug.Log("[SaveManager] Restored save from backup.");
}
}
CurrentSave = loadedSave ?? new SaveGame { SaveFileName = saveFileName, SaveVersion = "0.0.1-Pre-Alpha" };
Debug.Log($"[SaveManager] Loading save {loadedSave?.SaveFileName}");
IsGameLoaded = true;
OnSaveGameLoaded?.Invoke(CurrentSave);
}
private async UniTask<SaveGame> LoadFromFileAsync(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
try
{
var bytes = await File.ReadAllBytesAsync(path);
if (bytes == null || bytes.Length == 0) return null;
await UniTask.SwitchToThreadPool();
SaveGame result = null;
try
{
result = MemoryPackSerializer.Deserialize<SaveGame>(bytes);
}
finally
{
await UniTask.SwitchToMainThread();
}
return result;
}
catch (Exception e)
{
Debug.LogError($"[SaveManager] Failed to load or deserialize '{path}': {e}");
return null;
}
}
public bool Delete(string saveFileName)
{
if (string.IsNullOrWhiteSpace(saveFileName))
return false;
var normalizedName = Path.GetFileNameWithoutExtension(saveFileName);
var mainPath = Path.Combine(FilePaths.SaveDataFolder, normalizedName + ".sav");
var backupPath = Path.Combine(FilePaths.SaveBackupDataFolder, normalizedName + ".sav");
var deletedAny = false;
try
{
if (File.Exists(mainPath))
{
File.Delete(mainPath);
deletedAny = true;
}
}
catch (Exception ex)
{
Debug.LogError($"[SaveManager] Failed to delete main save '{mainPath}': {ex}");
}
try
{
if (File.Exists(backupPath))
{
File.Delete(backupPath);
deletedAny = true;
}
}
catch (Exception ex)
{
Debug.LogError($"[SaveManager] Failed to delete backup save '{backupPath}': {ex}");
}
if (CurrentSave != null &&
string.Equals(CurrentSave.SaveFileName, normalizedName, StringComparison.Ordinal))
{
CurrentSave = null;
IsGameLoaded = false;
}
return deletedAny;
}
public bool DoesSaveExist(string saveInfoFileName)
{
if (string.IsNullOrWhiteSpace(saveInfoFileName))
return false;
var mainPath = Path.Combine(FilePaths.SaveDataFolder, saveInfoFileName + ".sav");
var backupPath = Path.Combine(FilePaths.SaveBackupDataFolder, saveInfoFileName + ".sav");
try
{
return File.Exists(mainPath) || File.Exists(backupPath);
}
catch (Exception ex)
{
Debug.LogError($"[SaveManager] Error checking save existence for '{saveInfoFileName}': {ex}");
return false;
}
}
public void SetLevelFlag(LevelFlag levelFlag, bool value, bool requestSave = true)
{
if (CurrentSave?.PersistentVariables?.Game == null)
{
Debug.LogWarning($"[SaveManager] Could not set level flag '{levelFlag}' because CurrentSave or PersistentVariables.Game is null.");
return;
}
CurrentSave.PersistentVariables.Game.SetLevelFlag(levelFlag, value);
if (requestSave)
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
}
public void SetPuzzleCompleted(PuzzleKey puzzleKey, bool value, bool requestSave = true)
{
if (CurrentSave?.PersistentVariables?.Game == null)
{
Debug.LogWarning($"[SaveManager] Could not set level flag '{puzzleKey}' because CurrentSave or PersistentVariables.Game is null.");
return;
}
CurrentSave.PersistentVariables.Game.SetPuzzleCompleted(puzzleKey, value);
if (requestSave)
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
}
public bool GetLevelFlag(LevelFlag levelFlag)
{
if (CurrentSave?.PersistentVariables?.Game == null)
{
Debug.LogWarning($"[SaveManager] Could not get level flag '{levelFlag}' because CurrentSave or PersistentVariables.Game is null.");
return false;
}
return CurrentSave.PersistentVariables.Game.GetLevelFlag(levelFlag);
}
}
}