using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using UnityEngine; namespace Jovian.SaveSystem { /// /// Facade that orchestrates serialization, storage, and slot management. /// Thread-safe: uses SemaphoreSlim for async and lock for sync operations. /// public sealed class SaveSystem : ISaveSystem { private readonly ISaveSerializer serializer; private readonly ISaveStorage storage; private readonly ISaveSlotManager slotManager; private readonly int saveVersion; private readonly object syncLock = new object(); private readonly SemaphoreSlim asyncLock = new SemaphoreSlim(1, 1); public SaveSystem( ISaveSerializer serializer, ISaveStorage storage, ISaveSlotManager slotManager, SaveSystemSettings settings) { this.serializer = serializer; this.storage = storage; this.slotManager = slotManager; saveVersion = settings.currentSaveVersion; } public string CreateSession() { return slotManager.CreateSession(); } public bool HasAnySaves() { return slotManager.HasAnySaves(); } public void Save(string sessionId, TData data, SaveSlotType slotType) { lock(syncLock) { SaveSlotInfo slot = AllocateSlot(sessionId, slotType); long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); SaveEnvelope envelope = new SaveEnvelope { version = saveVersion, timestampUtc = timestamp, slotType = slotType, payload = JToken.FromObject(data) }; byte[] envelopeBytes = serializer.Serialize(envelope); storage.Write(slot.filePath, envelopeBytes); slotManager.UpdateSlotMetadata(slot, timestamp, saveVersion); slotManager.PersistIndex(); Debug.Log($"[SaveSystem] Saved {slotType} to {slot.filePath}"); } } public TData Load(SaveSlotInfo slot) { lock(syncLock) { return LoadInternal(slot); } } public async Task SaveAsync(string sessionId, TData data, SaveSlotType slotType) { await asyncLock.WaitAsync().ConfigureAwait(false); try { SaveSlotInfo slot = AllocateSlot(sessionId, slotType); long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); SaveEnvelope envelope = new SaveEnvelope { version = saveVersion, timestampUtc = timestamp, slotType = slotType, payload = JToken.FromObject(data) }; byte[] envelopeBytes = serializer.Serialize(envelope); await storage.WriteAsync(slot.filePath, envelopeBytes).ConfigureAwait(false); slotManager.UpdateSlotMetadata(slot, timestamp, saveVersion); slotManager.PersistIndex(); Debug.Log($"[SaveSystem] Saved {slotType} to {slot.filePath}"); } finally { asyncLock.Release(); } } public async Task LoadAsync(SaveSlotInfo slot) { await asyncLock.WaitAsync().ConfigureAwait(false); try { return LoadInternal(slot); } finally { asyncLock.Release(); } } public IReadOnlyList GetSlots(string sessionId) { return slotManager.GetSlots(sessionId); } public IReadOnlyList GetAllSessions() { return slotManager.GetAllSessions(); } public void DeleteSlot(SaveSlotInfo slot) { lock(syncLock) { slotManager.DeleteSlot(slot); } } public void DeleteSession(string sessionId) { lock(syncLock) { slotManager.DeleteSession(sessionId); } } private SaveSlotInfo AllocateSlot(string sessionId, SaveSlotType slotType) { return slotType switch { SaveSlotType.Manual => slotManager.AllocateManualSlot(sessionId), SaveSlotType.Auto => slotManager.AllocateAutoSlot(sessionId), SaveSlotType.Quick => slotManager.AllocateQuickSlot(sessionId), _ => throw new ArgumentOutOfRangeException(nameof(slotType), slotType, "Unknown slot type.") }; } private TData LoadInternal(SaveSlotInfo slot) { byte[] envelopeBytes = storage.Read(slot.filePath); SaveEnvelope envelope = serializer.Deserialize(envelopeBytes); return envelope.payload.ToObject(); } } }