Files

184 lines
7.2 KiB
C#
Raw Permalink Normal View History

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<SaveSection> sections = new List<SaveSection>();
}
private string SavePath => Path.Combine(Application.persistentDataPath, "save.json");
// Последние известные секции: секция модуля, которого сейчас нет (например, город на карте мира),
// не должна пропасть из файла при следующем сохранении
private readonly Dictionary<string, string> sections = new Dictionary<string, string>();
private bool loaded; // сохранение уже загружено (или файла нет) -- только после этого можно писать
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
GameServices.Register<ISaveSystem>(this);
}
void OnDestroy()
{
if (Instance == this) GameServices.Unregister<ISaveSystem>(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<SaveFile>(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("[СОХРАНЕНИЕ] Файл сохранения удалён.");
}
}
}