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(OnRequestedSave); _currentSaveCts?.Cancel(); } public event Action OnSaveGameLoaded; public event Action OnSaveGameSaved; // Existing synchronous hook public event Action OnSaveRequested; // New async hook that runs BEFORE cloning/serialization public event Func OnBeforeSaveRequestedAsync; public bool Initialized { get; private set; } public void Initialize() { _eventCoordinator.Subscribe(OnRequestedSave); Initialized = true; } private void OnRequestedSave(RequestGameSaveEvent evt) { SaveGameDataLatest().Forget(); } /// /// Queues the latest save and cancels any in-progress save. /// 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 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 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(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); } } }