using System; using System.Collections.Generic; using System.IO; using UnityEngine; // МОДУЛЬ СОХРАНЕНИЙ: игровое состояние. Документ: Docs/Modules/Save.md // // Модуль не знает, ЧТО сохраняют другие модули: каждый модуль сам регистрирует себя в SaveRegistry // (ISaveParticipant) и отдаёт свою секцию -- "economy", "settlement", "world" и т.д. // // Формат save.json, версия 2: { version: 2, sections: [ { key, json }, ... ] }. // Версия 1 (старый плоский файл: resources, townHallLevel, placedBuildings) читается автоматически -- // имена полей в секциях Экономики и Поселения совпадают со старыми. // // Файл лежит в Application.persistentDataPath -- папка переживает переустановку/обновление игры на телефоне. public class SaveManager : MonoBehaviour, ISaveSystem { public const int CurrentVersion = 2; public static SaveManager Instance { get; private set; } [Header("Автосохранение")] [Tooltip("Как часто автоматически сохранять прогресс, в секундах")] public float autoSaveInterval = 30f; [Serializable] class SaveSection { public string key; public string json; } [Serializable] class SaveFile { public int version; public List sections = new List(); } private string SavePath => Path.Combine(Application.persistentDataPath, "save.json"); // Последние известные секции: секция модуля, которого сейчас нет (например, город на карте мира), // не должна пропасть из файла при следующем сохранении private readonly Dictionary sections = new Dictionary(); private bool loaded; // сохранение уже загружено (или файла нет) -- только после этого можно писать void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); GameServices.Register(this); } void OnDestroy() { if (Instance == this) GameServices.Unregister(this); } void Start() { InvokeRepeating(nameof(SaveGame), autoSaveInterval, autoSaveInterval); } // Мобильные ОС часто "убивают" приложение без вызова OnApplicationQuit, // поэтому OnApplicationPause -- самый надёжный момент для сохранения на телефоне. void OnApplicationPause(bool pauseStatus) { if (pauseStatus) SaveGame(); } void OnApplicationQuit() { SaveGame(); } // ---------------- Контракт ISaveSystem ---------------- public bool HasSave => HasSaveFile(); public void SaveNow() => SaveGame(); // ---------------- СОХРАНЕНИЕ ---------------- public void SaveGame() { // Запись раньше загрузки затёрла бы файл стартовыми значениями модулей (ресурсы ещё нулевые) if (!loaded && HasSaveFile()) { Debug.LogWarning("[СОХРАНЕНИЕ] Пропущено: сохранение ещё не загружено."); return; } loaded = true; foreach (var participant in SaveRegistry.All) { string json = participant.Capture(); if (json != null) sections[participant.SaveKey] = json; // null -- модуль сейчас не может сохраниться } var file = new SaveFile { version = CurrentVersion }; foreach (var kvp in sections) file.sections.Add(new SaveSection { key = kvp.Key, json = kvp.Value }); File.WriteAllText(SavePath, JsonUtility.ToJson(file, true)); EventBus.Publish(new GameSavedEvent()); Debug.Log($"[СОХРАНЕНИЕ] Игра сохранена: {SavePath}"); } // ---------------- ЗАГРУЗКА ---------------- public bool HasSaveFile() { return File.Exists(SavePath); } public void LoadGame() { loaded = true; if (!HasSaveFile()) { Debug.Log("[ЗАГРУЗКА] Файл сохранения не найден, начинаем новую игру."); EventBus.Publish(new GameLoadedEvent()); // загрузка завершена, просто без файла return; } string text = File.ReadAllText(SavePath); ReadSections(text); foreach (var participant in SaveRegistry.All) { if (sections.TryGetValue(participant.SaveKey, out string json)) participant.Restore(json); } EventBus.Publish(new GameLoadedEvent()); Debug.Log($"[ЗАГРУЗКА] Игра загружена, секций: {sections.Count}"); } void ReadSections(string text) { sections.Clear(); SaveFile file = JsonUtility.FromJson(text); if (file != null && file.version >= 2) { foreach (var s in file.sections) sections[s.key] = s.json; return; } // Версия 1: старый плоский файл. Отдаём его целиком Экономике и Поселению -- // их секции читают свои поля (resources / townHallLevel, placedBuildings) по старым именам. sections["economy"] = text; sections["settlement"] = text; Debug.Log("[ЗАГРУЗКА] Старый формат save.json (версия 1) -- перенесён в секции."); } // СБРОС СОХРАНЕНИЙ: новая игра. Каждый модуль сам возвращается к старту, файл пишется заново. // GameProgress (progress.json) сбрасывает себя сам -- см. GameProgress.ResetAll. public void ResetAll() { DeleteSave(); foreach (var participant in SaveRegistry.All) participant.ResetState(); loaded = true; SaveGame(); // сразу записать новую игру, чтобы старый прогресс не вернулся после перезапуска EventBus.Publish(new GameResetEvent()); Debug.Log("[СОХРАНЕНИЕ] Сохранения сброшены, начата новая игра."); } // Только стирает файл сохранения (состояние модулей в памяти не трогает) public void DeleteSave() { sections.Clear(); if (File.Exists(SavePath)) { File.Delete(SavePath); Debug.Log("[СОХРАНЕНИЕ] Файл сохранения удалён."); } } }