- Ядро (Bogatyri.Core): контракты модулей, EventBus, GameServices, SaveRegistry - Модули: Экономика, Поселение, Армия, Глобальный мир, Боевая система, Сохранения (каждый — отдельный asmdef, видит только ядро) - App/GameModules: регистрация модулей и связь с GameProgress/Казармами - Сохранения v2 по секциям модулей, перенос старого save.json и походов из progress.json - Сброс сохранений: ISaveSystem.ResetAll, ResetState у модулей, меню Tools - EditMode-тесты модулей, документы по каждому модулю с журналом изменений Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
7.1 KiB
C#
183 lines
7.1 KiB
C#
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("[ЗАГРУЗКА] Файл сохранения не найден, начинаем новую игру.");
|
|
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("[СОХРАНЕНИЕ] Файл сохранения удалён.");
|
|
}
|
|
}
|
|
}
|