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
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using AppleHills.Data.CardSystem;
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using Core.SaveLoad;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
@@ -36,6 +37,9 @@ namespace Data.CardSystem
|
||||
public event Action<CardData> OnCardRarityUpgraded;
|
||||
public event Action<int> OnBoosterCountChanged;
|
||||
|
||||
// Keep a reference to unsubscribe safely
|
||||
private Action<string> _onSaveDataLoadedHandler;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
@@ -49,9 +53,6 @@ namespace Data.CardSystem
|
||||
// Load card definitions from Addressables
|
||||
LoadCardDefinitionsFromAddressables();
|
||||
|
||||
// Build lookup dictionary
|
||||
BuildDefinitionLookup();
|
||||
|
||||
Logging.Debug("[CardSystemManager] Post-boot initialization complete");
|
||||
}
|
||||
|
||||
@@ -87,9 +88,54 @@ namespace Data.CardSystem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build lookup now that cards are loaded
|
||||
BuildDefinitionLookup();
|
||||
|
||||
// Apply saved state when save data is available
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
if (SaveLoadManager.Instance.IsSaveDataLoaded)
|
||||
{
|
||||
ApplySavedCardStateIfAvailable();
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveLoadManager.Instance.OnLoadCompleted += OnSaveDataLoadedHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply saved state if present in the SaveLoadManager
|
||||
private void ApplySavedCardStateIfAvailable()
|
||||
{
|
||||
var data = SaveLoadManager.Instance?.currentSaveData;
|
||||
if (data?.cardCollection != null)
|
||||
{
|
||||
ApplyCardCollectionState(data.cardCollection);
|
||||
Logging.Debug("[CardSystemManager] Applied saved card collection state after loading definitions");
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler for when save data load completes
|
||||
private void OnSaveDataLoadedHandler(string slot)
|
||||
{
|
||||
ApplySavedCardStateIfAvailable();
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a lookup dictionary for quick access to card definitions by ID
|
||||
/// </summary>
|
||||
@@ -383,5 +429,58 @@ namespace Data.CardSystem
|
||||
|
||||
return (float)collectedOfRarity / totalOfRarity * 100f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export current card collection to a serializable snapshot
|
||||
/// </summary>
|
||||
public CardCollectionState ExportCardCollectionState()
|
||||
{
|
||||
var state = new CardCollectionState
|
||||
{
|
||||
boosterPackCount = playerInventory.BoosterPackCount,
|
||||
cards = new List<SavedCardEntry>()
|
||||
};
|
||||
|
||||
foreach (var card in playerInventory.CollectedCards.Values)
|
||||
{
|
||||
if (string.IsNullOrEmpty(card.DefinitionId)) continue;
|
||||
state.cards.Add(new SavedCardEntry
|
||||
{
|
||||
definitionId = card.DefinitionId,
|
||||
rarity = card.Rarity,
|
||||
copiesOwned = card.CopiesOwned
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a previously saved snapshot to the runtime inventory
|
||||
/// </summary>
|
||||
public void ApplyCardCollectionState(CardCollectionState state)
|
||||
{
|
||||
if (state == null) return;
|
||||
|
||||
playerInventory.ClearAllCards();
|
||||
playerInventory.BoosterPackCount = state.boosterPackCount;
|
||||
OnBoosterCountChanged?.Invoke(playerInventory.BoosterPackCount);
|
||||
|
||||
foreach (var entry in state.cards)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.definitionId)) continue;
|
||||
if (_definitionLookup.TryGetValue(entry.definitionId, out var def))
|
||||
{
|
||||
// Create from definition to ensure links, then overwrite runtime fields
|
||||
var cd = def.CreateCardData();
|
||||
cd.Rarity = entry.rarity;
|
||||
cd.CopiesOwned = entry.copiesOwned;
|
||||
playerInventory.AddCard(cd);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning($"[CardSystemManager] Saved card definition not found: {entry.definitionId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ namespace AppleHills.Tests
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[CustomEditor(typeof(CardSystemTester))]
|
||||
public class CardSystemTesterEditor : Editor
|
||||
public class CardSystemTesterEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace UI.CardSystem
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[CustomEditor(typeof(CardUIElement))]
|
||||
public class CardUIElementEditor : Editor
|
||||
public class CardUIElementEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections;
|
||||
using Bootstrap;
|
||||
using Core;
|
||||
using Core.SaveLoad;
|
||||
using Input;
|
||||
using Pixelplacement;
|
||||
using UI.Core;
|
||||
@@ -31,8 +32,10 @@ namespace UI.Tutorial
|
||||
|
||||
void InitializeTutorial()
|
||||
{
|
||||
if (playTutorial)
|
||||
if (playTutorial && !SaveLoadManager.Instance.currentSaveData.playedDivingTutorial)
|
||||
{
|
||||
// TODO: Possibly do it better, but for now just mark tutorial as played immediately
|
||||
SaveLoadManager.Instance.currentSaveData.playedDivingTutorial = true;
|
||||
// pause the game, hide UI, and register for input overrides
|
||||
GameManager.Instance.RequestPause(this);
|
||||
UIPageController.Instance.HideAllUI();
|
||||
|
||||
Reference in New Issue
Block a user