MPV of save/load system
This commit is contained in:
3
Assets/Scripts/Core/SaveLoad.meta
Normal file
3
Assets/Scripts/Core/SaveLoad.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0541e0e1c3dd41f7a61773e4bc2c64ed
|
||||
timeCreated: 1761553949
|
||||
29
Assets/Scripts/Core/SaveLoad/SaveLoadData.cs
Normal file
29
Assets/Scripts/Core/SaveLoad/SaveLoadData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Core/SaveLoad/SaveLoadData.cs.meta
Normal file
3
Assets/Scripts/Core/SaveLoad/SaveLoadData.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1704f1fc7e6b4a398f39fb86cec265f8
|
||||
timeCreated: 1761553972
|
||||
238
Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs
Normal file
238
Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs
Normal 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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs.meta
Normal file
3
Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1731f4790be4ec3bab71506427768d7
|
||||
timeCreated: 1761553854
|
||||
Reference in New Issue
Block a user