using System.Collections;
using System.Collections.Generic;
using Ichni.Menu;
using Ichni.RhythmGame;
using Ichni.Story;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Ichni
{
///
/// 全局存档入口。
/// 负责可重复游玩的歌曲成绩与谱面记录;
/// 负责章节、歌曲及未来内容共用的 Offline 解锁 Key;
/// 负责章节故事树、对话选项和 Yarn 变量。
/// 两套数据使用不同文件和不同生命周期,修改任一侧时不要把另一侧的字段或迁移规则混入其中。
///
public class GameSaveManager : SerializedMonoBehaviour
{
public static GameSaveManager instance;
public SongSaveModule SongSaveModule;
public UnlockSaveModule UnlockSaveModule;
public StorySaveModule StorySaveModule;
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
private void Start()
{
// 菜单与章节 UI 会在启动后立刻读取歌曲记录,因此游戏记录必须先加载。
SongSaveModule = new SongSaveModule();
SongSaveModule.LoadSongStatuses();
// 内容解锁既不属于歌曲成绩也不属于 Yarn 剧情树。菜单在首次显示前必须先得到它的状态。
UnlockSaveModule = new UnlockSaveModule();
UnlockSaveModule.LoadUnlockKeys();
// 剧情变量属于独立存档,不参与歌曲成绩的 Schema 或 Chart Revision 迁移。
StorySaveModule = new StorySaveModule();
StorySaveModule.LoadVariables();
}
}
public partial class SongSaveModule
{
// ES3 数据 Key。一旦发布,不要重命名;重命名等同于修改存档格式,需要 Schema Migration。
private const string SongSavesKey = "SongSaves";
// 与歌曲记录存放在同一文件。当前项目尚未正式发布,不兼容的旧版本会直接重置为新的 v1 记录。
private const string SongSaveSchemaVersionKey = "SongSaveSchemaVersion";
// v1 使用 Dictionary 保存成绩,彻底与选曲 UI 的列表位置分离。
// 谱面内容改变应递增 Chart Revision,而不是此值。正式发布后升级 Schema 时必须恢复非破坏性迁移策略。
private const int CurrentSongSaveSchemaVersion = 1;
public Dictionary songStatusSaves;
// 歌曲成绩与 Schema Version 的唯一持久化文件;剧情存档不使用此路径。
private string songSavePath => Application.persistentDataPath + "/GameSaves/SongSaves.json";
public SongSaveModule()
{
songStatusSaves = new Dictionary();
//Debug.Log("Song save path: " + songSavePath);
}
}
public partial class SongSaveModule
{
///
/// 使已加载的歌曲记录与当前 Chapter 配置保持一致。
/// 会为新增难度建立以稳定 saveDifficultyId 为 Key 的记录,并检查每个谱面的 Chart Revision。
/// 已删除难度的旧 Key 不自动删除:它不会参与当前 UI 或完成度统计,且可在内容恢复时保留历史成绩。
/// Revision 不一致时只重置该谱面的可比成绩,不回退歌曲完成状态、剧情进度或解锁状态。
///
private static void EnsureSongStatusMatchesDefinition(SongStatusSave songStatus, SongItemData song)
{
songStatus.additionalInfo ??= string.Empty;
songStatus.beatmapSavesByDifficultyId ??= new Dictionary();
foreach (DifficultyData difficulty in song.difficultyDataList)
{
if (difficulty == null || difficulty.saveDifficultyId < 0)
{
continue;
}
BeatmapSave beatmapSave = songStatus.GetOrCreateBeatmapSave(difficulty.saveDifficultyId);
beatmapSave.EnsureChartRevision(difficulty.GetChartRevision());
}
}
///
/// 为新加入首发内容的歌曲创建空记录,并立即按当前难度定义初始化 Revision。
///
private static SongStatusSave CreateSongStatus(SongItemData song)
{
SongStatusSave songStatus = new SongStatusSave(false, string.Empty, new Dictionary());
EnsureSongStatusMatchesDefinition(songStatus, song);
return songStatus;
}
///
/// 首次启动、尚无歌曲存档时调用。以当前 Chapter 配置生成完整的空记录集合。
///
private void InitializeSongStatuses()
{
songStatusSaves = new Dictionary();
foreach (ChapterSelectionUnit chapter in ChapterSelectionManager.instance.chapters)
{
foreach (SongItemData song in chapter.songs)
{
songStatusSaves[song.songName] = CreateSongStatus(song);
}
}
SaveSongStatuses();
}
///
/// 每次加载后执行的内容对账与 Revision 检查。
/// 这是新增/删除歌曲、增加难度或递增 Chart Revision 后自动修正旧存档的入口。
///
private void CheckSongStatuses()
{
foreach (ChapterSelectionUnit chapter in ChapterSelectionManager.instance.chapters)
{
foreach (SongItemData song in chapter.songs)
{
if (songStatusSaves.TryGetValue(song.songName, out SongStatusSave songStatus))
{
if (songStatus == null)
{
songStatusSaves[song.songName] = CreateSongStatus(song);
continue;
}
EnsureSongStatusMatchesDefinition(songStatus, song);
}
else
{
songStatusSaves[song.songName] = CreateSongStatus(song);
}
}
}
SaveSongStatuses();
}
[Button]
///
/// 将歌曲记录与当前 Schema Version 一起写入 ES3。
/// 不要单独保存成绩而遗漏 Version;否则下次启动会把同一份数据错误识别为旧 Schema。
///
public void SaveSongStatuses()
{
ES3.Save(SongSavesKey, songStatusSaves, songSavePath);
ES3.Save(SongSaveSchemaVersionKey, CurrentSongSaveSchemaVersion, songSavePath);
}
///
/// 将一次已完成演奏写入游戏记录存档。
/// 每个谱面保留当前 Chart Revision 下的最高 Accuracy、最高 Max Combo,以及曾达成的 FC/AP;
/// 后续较差成绩不会覆盖既有纪录。本函数不修改剧情树、Yarn 变量或歌曲解锁。
///
/// SongStatusSave 的稳定歌曲 Key。
/// 存档 Key,必须对应 DifficultyData.saveDifficultyId。
/// 当前谱面的 Revision;不一致时由 BeatmapSave 重置该谱面成绩。
/// 仅本局有效的运行时判定记录。
public bool RecordPlayResult(string songName, int saveDifficultyId, int currentChartRevision, PlayingRecorder recorder)
{
if (string.IsNullOrEmpty(songName))
{
Debug.LogWarning("Cannot save play result without a song name.");
return false;
}
if (saveDifficultyId < 0)
{
Debug.LogWarning($"Cannot save play result for '{songName}' with an invalid save difficulty ID: {saveDifficultyId}.");
return false;
}
if (recorder == null || recorder.totalCount <= 0)
{
Debug.LogWarning($"Skipped saving play result for '{songName}' because no judgments were recorded.");
return false;
}
songStatusSaves ??= new Dictionary();
if (!songStatusSaves.TryGetValue(songName, out SongStatusSave songStatus) || songStatus == null)
{
songStatus = new SongStatusSave(false, string.Empty, new Dictionary());
songStatusSaves[songName] = songStatus;
}
songStatus.additionalInfo ??= string.Empty;
BeatmapSave beatmapSave = songStatus.GetOrCreateBeatmapSave(saveDifficultyId);
bool recordChanged = beatmapSave.ApplyPlayResult(recorder, currentChartRevision);
bool completionChanged = !songStatus.isCompleted;
songStatus.isCompleted = true;
SaveSongStatuses();
return recordChanged || completionChanged;
}
///
/// 读取歌曲记录并执行内容对账和 Chart Revision 检查。
/// 当前为正式发布前的重置策略:任何非 v1 的旧测试存档都会直接被新 v1 结构覆盖,
/// 因此开发者需要自行接受成绩清空。正式发布后不得沿用此策略,应改回显式迁移。
///
public void LoadSongStatuses()
{
if (ES3.FileExists(songSavePath) && ES3.KeyExists(SongSavesKey, songSavePath))
{
int loadedSchemaVersion = ES3.KeyExists(SongSaveSchemaVersionKey, songSavePath)
? ES3.Load(SongSaveSchemaVersionKey, songSavePath)
: 0;
if (loadedSchemaVersion != CurrentSongSaveSchemaVersion)
{
Debug.LogWarning($"Resetting pre-release song save schema v{loadedSchemaVersion} to v{CurrentSongSaveSchemaVersion}.");
ES3.DeleteFile(songSavePath);
InitializeSongStatuses();
return;
}
songStatusSaves = ES3.Load>(SongSavesKey, songSavePath) ??
new Dictionary();
CheckSongStatuses();
}
else
{
InitializeSongStatuses();
}
}
[Button]
///
/// 仅清除每张谱面的可比成绩(Accuracy、FC、AP、Max Combo)。
/// 保留 Chart Revision、歌曲完成状态、剧情进度与解锁状态,适合测试成绩而不破坏流程存档。
///
public void ClearBeatmapRecords()
{
foreach (SongStatusSave songStatus in songStatusSaves.Values)
{
if (songStatus?.beatmapSavesByDifficultyId == null)
{
continue;
}
foreach (BeatmapSave beatmapSave in songStatus.beatmapSavesByDifficultyId.Values)
{
if (beatmapSave == null)
{
continue;
}
beatmapSave.accuracy = 0.0f;
beatmapSave.isFullCombo = false;
beatmapSave.isAllPerfect = false;
beatmapSave.maxCombo = 0;
}
}
SaveSongStatuses();
}
///
/// 按歌曲 Key 获取内存中的游戏记录;该方法不触发磁盘读取或创建新记录。
///
public SongStatusSave GetSongStatusSave(string songName)
{
if (songStatusSaves.TryGetValue(songName, out SongStatusSave save))
{
return save;
}
Debug.LogWarning("Song status save for " + songName + " does not exist.");
return null;
}
}
// StorySaveModule 已迁移至 NewStorySystem/Save/StorySaveModule.cs(按章节存树/选项,全局存变量)
}