using System; using System.Collections.Generic; using System.Linq; using System.Text; using Newtonsoft.Json; namespace Jovian.SaveSystem { /// /// Manages save slot allocation, session tracking, and auto-save rotation. /// Maintains a persistent index file that tracks all sessions and their slots. /// public sealed class SaveSlotManager : ISaveSlotManager { private const string IndexFileName = "index.json"; private const string ManualPrefix = "manual_"; private const string AutoPrefix = "auto_"; private const string QuickFileName = "quick.sav"; private const string SaveExtension = ".sav"; private readonly ISaveStorage storage; private readonly int maxAutoSaves; private SaveIndex index; public SaveSlotManager(ISaveStorage storage, SaveSystemSettings settings) { this.storage = storage; maxAutoSaves = settings.maxAutoSavesPerSession; LoadIndex(); } public string CreateSession() { string sessionId = Guid.NewGuid().ToString("N"); SaveSessionInfo session = new SaveSessionInfo { sessionId = sessionId, creationDateUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), lastSaveDateUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; index.sessions.Add(session); storage.CreateDirectory(sessionId); PersistIndex(); return sessionId; } public SaveSlotInfo AllocateManualSlot(string sessionId) { List sessionSlots = GetOrCreateSessionSlots(sessionId); int nextNumber = sessionSlots .Where(s => s.slotType == SaveSlotType.Manual) .Select(s => s.slotNumber) .DefaultIfEmpty(0) .Max() + 1; SaveSlotInfo slot = new SaveSlotInfo { sessionId = sessionId, slotType = SaveSlotType.Manual, slotNumber = nextNumber, filePath = $"{sessionId}/{ManualPrefix}{nextNumber:D3}{SaveExtension}", timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; index.slots.Add(slot); return slot; } public SaveSlotInfo AllocateAutoSlot(string sessionId) { List sessionSlots = GetOrCreateSessionSlots(sessionId); List autoSlots = sessionSlots .Where(s => s.slotType == SaveSlotType.Auto) .OrderBy(s => s.slotNumber) .ToList(); if(autoSlots.Count >= maxAutoSaves) { // Rotate: reuse the oldest slot var oldest = autoSlots.OrderBy(s => s.timestampUtc).First(); oldest.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); return oldest; } int nextNumber = autoSlots.Count + 1; SaveSlotInfo slot = new SaveSlotInfo { sessionId = sessionId, slotType = SaveSlotType.Auto, slotNumber = nextNumber, filePath = $"{sessionId}/{AutoPrefix}{nextNumber:D3}{SaveExtension}", timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; index.slots.Add(slot); return slot; } public SaveSlotInfo AllocateQuickSlot(string sessionId) { List sessionSlots = GetOrCreateSessionSlots(sessionId); SaveSlotInfo existingQuick = sessionSlots.FirstOrDefault(s => s.slotType == SaveSlotType.Quick); if(existingQuick != null) { existingQuick.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); return existingQuick; } SaveSlotInfo slot = new SaveSlotInfo { sessionId = sessionId, slotType = SaveSlotType.Quick, slotNumber = 1, filePath = $"{sessionId}/{QuickFileName}", timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; index.slots.Add(slot); return slot; } public IReadOnlyList GetSlots(string sessionId) { return index.slots .Where(s => s.sessionId == sessionId) .OrderByDescending(s => s.timestampUtc) .ToList(); } public IReadOnlyList GetAllSessions() { return index.sessions .OrderByDescending(s => s.lastSaveDateUtc) .ToList(); } public bool HasAnySaves() { return index.slots.Count > 0; } public void DeleteSlot(SaveSlotInfo slot) { storage.Delete(slot.filePath); index.slots.RemoveAll(s => s.filePath == slot.filePath); PersistIndex(); } public void DeleteSession(string sessionId) { List slotsToDelete = index.slots .Where(s => s.sessionId == sessionId) .ToList(); foreach(SaveSlotInfo slot in slotsToDelete) { storage.Delete(slot.filePath); } index.slots.RemoveAll(s => s.sessionId == sessionId); index.sessions.RemoveAll(s => s.sessionId == sessionId); PersistIndex(); } public void UpdateSlotMetadata(SaveSlotInfo slot, long timestampUtc, int saveVersion) { slot.timestampUtc = timestampUtc; slot.saveVersion = saveVersion; SaveSessionInfo session = index.sessions.FirstOrDefault(s => s.sessionId == slot.sessionId); if(session != null) { session.lastSaveDateUtc = timestampUtc; } } public void PersistIndex() { string json = JsonConvert.SerializeObject(index, Formatting.Indented); byte[] bytes = Encoding.UTF8.GetBytes(json); storage.Write(IndexFileName, bytes); } private void LoadIndex() { if(!storage.Exists(IndexFileName)) { index = new SaveIndex(); return; } try { byte[] bytes = storage.Read(IndexFileName); string json = Encoding.UTF8.GetString(bytes); index = JsonConvert.DeserializeObject(json) ?? new SaveIndex(); } catch(Exception) { index = new SaveIndex(); } } private List GetOrCreateSessionSlots(string sessionId) { if(!index.sessions.Any(s => s.sessionId == sessionId)) { SaveSessionInfo session = new SaveSessionInfo { sessionId = sessionId, creationDateUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), lastSaveDateUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; index.sessions.Add(session); storage.CreateDirectory(sessionId); } return index.slots.Where(s => s.sessionId == sessionId).ToList(); } /// /// Internal index structure persisted to disk. /// [Serializable] private sealed class SaveIndex { public List sessions = new List(); public List slots = new List(); } } }