MPV of save/load system

This commit is contained in:
Michal Pikulski
2025-10-27 14:00:37 +01:00
parent 690e8b4507
commit f5c1ae51cd
15 changed files with 1234 additions and 17 deletions

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0541e0e1c3dd41f7a61773e4bc2c64ed
timeCreated: 1761553949

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Core.SaveLoad
{
[System.Serializable]
public class SaveLoadData
{
public bool playedDivingTutorial = false;
// Snapshot of the player's card collection (MVP)
public CardCollectionState cardCollection;
}
// Minimal DTOs for card persistence
[System.Serializable]
public class CardCollectionState
{
public int boosterPackCount;
public List<SavedCardEntry> cards = new List<SavedCardEntry>();
}
[System.Serializable]
public class SavedCardEntry
{
public string definitionId;
public AppleHills.Data.CardSystem.CardRarity rarity;
public int copiesOwned;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1704f1fc7e6b4a398f39fb86cec265f8
timeCreated: 1761553972

View File

@@ -0,0 +1,238 @@
using System;
using System.Collections;
using System.IO;
using Bootstrap;
using UnityEngine;
namespace Core.SaveLoad
{
/// <summary>
/// Simple Save/Load manager that follows the project's bootstrap pattern.
/// - Singleton instance
/// - Registers a post-boot init action with BootCompletionService
/// - Exposes simple async Save/Load methods (PlayerPrefs-backed placeholder)
/// - Fires events on completion
/// This is intended as boilerplate to be expanded with a real serialization backend.
/// </summary>
public class SaveLoadManager : MonoBehaviour
{
private static SaveLoadManager _instance;
public static SaveLoadManager Instance => _instance;
// Path
private static string DefaultSaveFolder => Path.Combine(Application.persistentDataPath, "GameSaves");
public SaveLoadData currentSaveData;
// State
public bool IsSaving { get; private set; }
public bool IsLoading { get; private set; }
public bool IsSaveDataLoaded { get; private set; }
// Events
public event Action<string> OnSaveCompleted;
public event Action<string> OnLoadCompleted;
void Awake()
{
_instance = this;
IsSaveDataLoaded = false;
BootCompletionService.RegisterInitAction(InitializePostBoot);
}
private void Start()
{
Load();
}
private void OnApplicationQuit()
{
Save();
}
private void InitializePostBoot()
{
Logging.Debug("[SaveLoadManager] Post-boot initialization complete");
}
void OnDestroy()
{
if (_instance == this)
_instance = null;
}
private string GetFilePath(string slot)
{
return Path.Combine(DefaultSaveFolder, $"save_{slot}.json");
}
/// <summary>
/// Entry point to save to a named slot. Starts an async coroutine that writes to PlayerPrefs
/// (placeholder behavior). Fires OnSaveCompleted when finished.
/// </summary>
public void Save(string slot = "default")
{
if (IsSaving)
{
Logging.Warning("[SaveLoadManager] Save called while another save is in progress");
return;
}
StartCoroutine(SaveAsync(slot));
}
/// <summary>
/// Entry point to load from a named slot. Starts an async coroutine that reads from PlayerPrefs
/// (placeholder behavior). Fires OnLoadCompleted when finished.
/// </summary>
public void Load(string slot = "default")
{
if (IsLoading)
{
Logging.Warning("[SaveLoadManager] Load called while another load is in progress");
return;
}
StartCoroutine(LoadAsync(slot));
}
// TODO: This is stupid overkill, but over verbose logging is king for now
private IEnumerator SaveAsync(string slot)
{
Logging.Debug($"[SaveLoadManager] Starting save for slot '{slot}'");
IsSaving = true;
string path = GetFilePath(slot);
string tempPath = path + ".tmp";
string json = null;
bool prepFailed = false;
// Prep phase: ensure folder exists and serialize (no yields allowed inside try/catch)
try
{
Directory.CreateDirectory(DefaultSaveFolder);
if (currentSaveData == null)
{
Logging.Debug("[SaveLoadManager] currentSaveData is null, creating default instance before saving");
currentSaveData = new SaveLoadData();
}
// Pull latest card collection snapshot from CardSystem before serializing (don't overwrite with null)
if (Data.CardSystem.CardSystemManager.Instance != null)
{
currentSaveData.cardCollection = Data.CardSystem.CardSystemManager.Instance.ExportCardCollectionState();
}
json = JsonUtility.ToJson(currentSaveData, true);
}
catch (Exception ex)
{
Logging.Warning($"[SaveLoadManager] Exception during save prep for slot '{slot}': {ex}");
prepFailed = true;
}
if (prepFailed || string.IsNullOrEmpty(json))
{
// Ensure we clean up state and notify listeners outside of the try/catch
IsSaving = false;
OnSaveCompleted?.Invoke(slot);
Logging.Warning($"[SaveLoadManager] Aborting save for slot '{slot}' due to prep failure");
yield break;
}
// Write phase: perform IO and atomic move with fallback
try
{
File.WriteAllText(tempPath, json);
try
{
if (File.Exists(path))
File.Delete(path);
File.Move(tempPath, path);
}
catch (Exception moveEx)
{
Logging.Warning($"[SaveLoadManager] Atomic move failed for '{path}', attempting overwrite: {moveEx}");
File.Copy(tempPath, path, true);
File.Delete(tempPath);
}
Logging.Debug($"[SaveLoadManager] Save data written to '{path}'");
}
catch (Exception ex)
{
Logging.Warning($"[SaveLoadManager] Exception while saving slot '{slot}': {ex}");
}
finally
{
IsSaving = false;
OnSaveCompleted?.Invoke(slot);
Debug.Log($"[SaveLoadManager] Save completed for slot '{slot}'");
}
}
// TODO: This is stupid overkill, but over verbose logging is king for now
private IEnumerator LoadAsync(string slot)
{
Logging.Debug($"[SaveLoadManager] Starting load for slot '{slot}'");
IsLoading = true;
string path = GetFilePath(slot);
// Fast-path: no save file
if (!File.Exists(path))
{
Logging.Debug($"[SaveLoadManager] No save found at '{path}', creating defaults");
currentSaveData = new SaveLoadData();
// Simulate async operation and finish
yield return null;
IsLoading = false;
IsSaveDataLoaded = true;
OnLoadCompleted?.Invoke(slot);
yield break;
}
// Simulate async operation (optional)
yield return null;
try
{
string json = File.ReadAllText(path);
if (string.IsNullOrEmpty(json))
{
Logging.Warning($"[SaveLoadManager] Save file at '{path}' is empty. Creating defaults.");
currentSaveData = new SaveLoadData();
}
else
{
// Attempt to deserialize; if it fails or returns null, fall back to defaults
var loaded = JsonUtility.FromJson<SaveLoadData>(json);
if (loaded == null)
{
Logging.Warning($"[SaveLoadManager] Deserialized save data was null for slot '{slot}'. Creating defaults.");
currentSaveData = new SaveLoadData();
}
else
{
currentSaveData = loaded;
}
}
}
catch (Exception ex)
{
Logging.Warning($"[SaveLoadManager] Exception while reading/deserializing save file at '{path}': {ex}");
currentSaveData = new SaveLoadData();
}
finally
{
IsLoading = false;
IsSaveDataLoaded = true;
OnLoadCompleted?.Invoke(slot);
Logging.Debug($"[SaveLoadManager] Load completed for slot '{slot}'");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a1731f4790be4ec3bab71506427768d7
timeCreated: 1761553854