using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("Zichen-SaveKeeper")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("Zichen-SaveKeeper")] [assembly: AssemblyTitle("Zichen-SaveKeeper")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } [BepInPlugin("zichen.savekeeper", "A.SaveKeeper", "1.0.0")] public sealed class ZichenSaveKeeperPlugin : BaseUnityPlugin { [HarmonyPatch(typeof(MenuPageSaves), "OnDeleteGame")] private static class MenuPageSavesOnDeleteGamePatch { private static bool Prefix() { //IL_002e: Unknown result type (might be due to invalid IL or missing references) if (!IsEnabled()) { return true; } if (allowPlayerDelete != null && allowPlayerDelete.Value) { playerMenuDeleteInProgress = true; return true; } MenuManager.instance.PagePopUp("SaveKeeper", Color.red, "当前配置禁止手动删除存档。", "OK", false); LogInfo("Blocked manual save deletion by config."); return false; } private static void Postfix() { playerMenuDeleteInProgress = false; } } [HarmonyPatch(typeof(StatsManager), "SaveFileDelete")] private static class StatsManagerSaveFileDeletePatch { private static bool Prefix(string saveFileName) { if (!IsEnabled()) { return true; } if (playerMenuDeleteInProgress) { playerMenuDeleteInProgress = false; LogInfo("Allowed manual save deletion: " + saveFileName); return true; } if (blockGameDelete == null || !blockGameDelete.Value) { LogInfo("Allowed game save deletion by config: " + saveFileName); return true; } LogInfo("Blocked game save deletion: " + saveFileName); return false; } } [HarmonyPatch(typeof(StatsManager), "SaveGame")] private static class StatsManagerSaveGamePatch { private static bool Prefix(string fileName) { if (!ShouldBlockDeathOverwrite()) { return true; } if (IsArenaNow()) { LogInfo("Blocked SaveGame in arena/death result flow: " + fileName); return false; } if (playerDeathSaveBlocked && !SemiFunc.IsMultiplayer()) { LogInfo("Blocked SaveGame after singleplayer death: " + fileName); return false; } if (multiplayerDeathSaveBlocked && SemiFunc.IsMultiplayer()) { LogInfo("Blocked SaveGame after multiplayer team death: " + fileName); return false; } return true; } private static void Postfix(string fileName, bool __runOriginal) { if (IsEnabled() && __runOriginal) { CleanupOldBackups(fileName); } } } [HarmonyPatch(typeof(StatsManager), "SaveFileSave")] private static class StatsManagerSaveFileSavePatch { private static void Prefix(StatsManager __instance) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) if (!IsEnabled() || savePublicRooms == null || !savePublicRooms.Value || (Object)(object)__instance == (Object)null || (Object)(object)GameManager.instance == (Object)null || GameManagerLobbyTypeField == null) { return; } object value = GameManagerLobbyTypeField.GetValue(GameManager.instance); if (value is LobbyTypes) { LobbyTypes val = (LobbyTypes)value; if ((int)val != 0 && (__instance.savedLobbyTypes == null || !__instance.savedLobbyTypes.Contains(val))) { LogInfo("Saving non-private room progress."); __instance.SaveGame(GetCurrentSaveFileName()); } } } } [HarmonyPatch(typeof(PlayerAvatar), "PlayerDeath")] private static class PlayerAvatarPlayerDeathPatch { private static void Prefix() { if (!ShouldBlockDeathOverwrite() || SemiFunc.IsMultiplayer() || IsShopNow()) { return; } playerDeathSaveBlocked = true; LogInfo("Singleplayer death detected. Save overwrite is temporarily blocked."); ZichenSaveKeeperPlugin instance = Instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(ResetPlayerDeathBlockLater()); } if (restoreSaveAfterSingleplayerDeath != null && restoreSaveAfterSingleplayerDeath.Value) { string currentSaveFileName = GetCurrentSaveFileName(); if (!string.IsNullOrWhiteSpace(currentSaveFileName)) { ZichenSaveKeeperPlugin instance2 = Instance; if (instance2 != null) { ((MonoBehaviour)instance2).StartCoroutine(RestoreSingleplayerSaveAfterDeathLater(currentSaveFileName)); } } } if (autoReloadSingleplayer == null || !autoReloadSingleplayer.Value) { return; } string currentSaveFileName2 = GetCurrentSaveFileName(); if (!string.IsNullOrWhiteSpace(currentSaveFileName2)) { ZichenSaveKeeperPlugin instance3 = Instance; if (instance3 != null) { ((MonoBehaviour)instance3).StartCoroutine(ReloadSingleplayerLater(currentSaveFileName2)); } } } } [HarmonyPatch(typeof(PlayerAvatar), "PlayerDeathRPC")] private static class PlayerAvatarPlayerDeathRpcPatch { private static void Postfix() { if (ShouldBlockDeathOverwrite() && SemiFunc.IsMultiplayer() && PhotonNetwork.IsMasterClient && AreAllPlayersDead()) { multiplayerDeathSaveBlocked = true; LogInfo("All players are dead. Multiplayer save overwrite is temporarily blocked."); ZichenSaveKeeperPlugin instance = Instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(ResetMultiplayerDeathBlockLater()); } } } } [HarmonyPatch(typeof(PlayerAvatar), "Revive")] private static class PlayerAvatarRevivePatch { private static void Prefix() { playerDeathSaveBlocked = false; multiplayerDeathSaveBlocked = false; } } [HarmonyPatch(typeof(GameDirector), "Update")] private static class GameDirectorUpdatePatch { private static void Postfix() { if (!IsEnabled() || restoreBackupAfterArenaReturn == null || !restoreBackupAfterArenaReturn.Value || !SemiFunc.IsMultiplayer() || !PhotonNetwork.IsMasterClient) { return; } if (IsArenaNow()) { multiplayerArenaSeen = true; } else if (multiplayerArenaSeen && !multiplayerArenaRestoreRunning && IsLobbyOrLobbyMenuNow()) { LogInfo("Returned from multiplayer arena. Restoring latest backup soon."); ZichenSaveKeeperPlugin instance = Instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(RestoreBackupAfterArenaReturnLater()); } } } } [HarmonyPatch(typeof(RunManager), "ChangeLevel")] private static class RunManagerChangeLevelPatch { private static void Prefix(RunManager __instance, bool _levelFailed) { originalArenaLevelsDuringForcedRace = null; if (!IsEnabled() || deathArenaMode == null || deathArenaMode.Value == "官方随机" || (Object)(object)__instance == (Object)null || !_levelFailed || (Object)(object)__instance.levelCurrent == (Object)null || (Object)(object)__instance.levelCurrent == (Object)(object)__instance.levelLobby || IsLevelShop(__instance.levelCurrent) || IsLevelArena(__instance.levelCurrent)) { return; } string value = deathArenaMode.Value; Level val = ((value == "皇冠竞技场") ? FindCrownArenaLevel(__instance.levelArena) : FindArenaRaceLevel(__instance.levelArena)); if ((Object)(object)val == (Object)null) { if (!missingRaceArenaLogged) { missingRaceArenaLogged = true; LogWarning("Could not find requested death arena mode '" + value + "'. Keeping the game's original arena selection."); } } else { originalArenaLevelsDuringForcedRace = new List(__instance.levelArena); __instance.levelArena = new List { val }; LogInfo("Forcing death arena mode '" + value + "' to level: " + ((Object)val).name); } } private static void Postfix(RunManager __instance) { if ((Object)(object)__instance != (Object)null && originalArenaLevelsDuringForcedRace != null) { __instance.levelArena = originalArenaLevelsDuringForcedRace; originalArenaLevelsDuringForcedRace = null; } } } [HarmonyPatch(typeof(StatsManager), "LoadGame")] private static class StatsManagerLoadGamePatch { private static void Postfix() { if (!manualRestoreRunning) { playerDeathSaveBlocked = false; multiplayerDeathSaveBlocked = false; } } } [HarmonyPatch(typeof(RunManager), "LeaveToMainMenu")] private static class RunManagerLeaveToMainMenuPatch { private static void Prefix() { playerDeathSaveBlocked = false; multiplayerDeathSaveBlocked = false; playerMenuDeleteInProgress = false; multiplayerArenaSeen = false; multiplayerArenaRestoreRunning = false; manualRestoreRunning = false; } } [CompilerGenerated] private sealed class d__58 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string saveFileName; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__58(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; manualRestoreRunning = true; LogInfo("Manual latest progress reload started: " + saveFileName); if (SemiFunc.IsMultiplayer()) { if (!PhotonNetwork.IsMasterClient) { LogWarning("Manual restore failed because only the host can reload a multiplayer save."); ShowPopup("只有主机可以手动恢复多人存档进度。", Color.yellow); manualRestoreRunning = false; return false; } multiplayerDeathSaveBlocked = true; } else { playerDeathSaveBlocked = true; } RestoreBestSaveAfterArenaReturn(saveFileName); <>2__current = (object)new WaitForSeconds(0.65f); <>1__state = 1; return true; case 1: <>1__state = -1; try { SemiFunc.MenuActionSingleplayerGame(saveFileName, (List)null); } catch (Exception ex) { LogError("Manual restore failed to trigger game reload: " + ex.Message); TryLoadGameInMemory(saveFileName); ShowPopup("已恢复存档数据,但触发重载失败。", Color.yellow); playerDeathSaveBlocked = false; multiplayerDeathSaveBlocked = false; manualRestoreRunning = false; return false; } <>2__current = (object)new WaitForSeconds(1f); <>1__state = 2; return true; case 2: <>1__state = -1; TryLoadGameInMemory(saveFileName); playerDeathSaveBlocked = false; multiplayerDeathSaveBlocked = false; manualRestoreRunning = false; LogInfo("Manual latest progress reload completed: " + saveFileName); ShowPopup("已重新加载当前存档的最新进度。", Color.green); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__65 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string saveFileName; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__65(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(4f); <>1__state = 1; return true; case 1: <>1__state = -1; if (!IsEnabled() || string.IsNullOrWhiteSpace(saveFileName) || SemiFunc.IsMultiplayer()) { playerDeathSaveBlocked = false; return false; } if (restoreLatestBackupBeforeReload != null && restoreLatestBackupBeforeReload.Value) { RestoreLatestBackup(saveFileName); } LogInfo("Reloading save after singleplayer death: " + saveFileName); SemiFunc.MenuActionSingleplayerGame(saveFileName, (List)null); <>2__current = (object)new WaitForSeconds(1f); <>1__state = 2; return true; case 2: <>1__state = -1; TryLoadGameInMemory(saveFileName); playerDeathSaveBlocked = false; return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__64 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__64(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(12f); <>1__state = 1; return true; case 1: <>1__state = -1; multiplayerDeathSaveBlocked = false; LogInfo("Multiplayer death save block expired."); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__63 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__63(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(12f); <>1__state = 1; return true; case 1: <>1__state = -1; playerDeathSaveBlocked = false; LogInfo("Singleplayer death save block expired."); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__67 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__67(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; multiplayerArenaRestoreRunning = true; <>2__current = (object)new WaitForSeconds(3f); <>1__state = 1; return true; case 1: { <>1__state = -1; string currentSaveFileName = GetCurrentSaveFileName(); if (string.IsNullOrWhiteSpace(currentSaveFileName)) { LogWarning("Could not restore backup after arena return because current save is empty."); } else { RestoreBestSaveAfterArenaReturn(currentSaveFileName); TryLoadGameInMemory(currentSaveFileName); LogInfo("Checked save recovery after returning from multiplayer arena: " + currentSaveFileName); } multiplayerArenaSeen = false; multiplayerArenaRestoreRunning = false; multiplayerDeathSaveBlocked = false; return false; } } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class d__66 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string saveFileName; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__66(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(3f); <>1__state = 1; return true; case 1: <>1__state = -1; if (!IsEnabled() || SemiFunc.IsMultiplayer() || string.IsNullOrWhiteSpace(saveFileName)) { return false; } RestoreBestSaveAfterArenaReturn(saveFileName); TryLoadGameInMemory(saveFileName); playerDeathSaveBlocked = false; LogInfo("Checked singleplayer save recovery after death: " + saveFileName); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } public const string PluginGuid = "zichen.savekeeper"; public const string PluginName = "A.SaveKeeper"; public const string PluginVersion = "1.0.0"; private const string InfoSection = "模组信息"; private const string SaveSection = "A.存档管家"; private const int DeathSaveBlockSeconds = 12; private const string DeathArenaModeRace = "赛车比赛"; private const string DeathArenaModeCrown = "皇冠竞技场"; private const string DeathArenaModeOfficial = "官方随机"; private static readonly FieldInfo StatsManagerCurrentSaveField = typeof(StatsManager).GetField("saveFileCurrent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo PlayerAvatarDeadSetField = typeof(PlayerAvatar).GetField("deadSet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo GameManagerLobbyTypeField = typeof(GameManager).GetField("lobbyType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly Regex BackupNumberRegex = new Regex("_BACKUP(\\d+)", RegexOptions.IgnoreCase); private static ConfigEntry featureEnabled; private static ConfigEntry allowPlayerDelete; private static ConfigEntry blockGameDelete; private static ConfigEntry blockDeathOverwrite; private static ConfigEntry savePublicRooms; private static ConfigEntry autoReloadSingleplayer; private static ConfigEntry restoreLatestBackupBeforeReload; private static ConfigEntry restoreBackupAfterArenaReturn; private static ConfigEntry restoreSaveAfterSingleplayerDeath; private static ConfigEntry manualRestoreShortcut; private static ConfigEntry maxBackupCount; private static ConfigEntry showConflictWarning; private static ConfigEntry verboseLogging; private static ConfigEntry deathArenaMode; private Harmony harmony; private ConfigEntry moduleNameInfo; private ConfigEntry moduleVersionInfo; private ConfigEntry contactInfo; private static bool playerDeathSaveBlocked; private static bool multiplayerDeathSaveBlocked; private static bool playerMenuDeleteInProgress; private static bool multiplayerArenaSeen; private static bool multiplayerArenaRestoreRunning; private static List originalArenaLevelsDuringForcedRace; private static bool missingRaceArenaLogged; private static bool noSaveDeleteConflictDetected; private static bool conflictPopupShown; private static bool manualRestoreRunning; public static ZichenSaveKeeperPlugin Instance { get; private set; } private void Awake() { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown Instance = this; BindConfig(); harmony = new Harmony("zichen.savekeeper.patch"); harmony.PatchAll(typeof(ZichenSaveKeeperPlugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"zichen-savekeeper loaded."); } private void OnDestroy() { Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } harmony = null; if (Instance == this) { Instance = null; } } private void BindConfig() { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Expected O, but got Unknown //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Expected O, but got Unknown //IL_02f9: Unknown result type (might be due to invalid IL or missing references) //IL_03fc: Unknown result type (might be due to invalid IL or missing references) //IL_0406: Expected O, but got Unknown moduleNameInfo = ((BaseUnityPlugin)this).Config.Bind("模组信息", "模组名称", "存档管家", new ConfigDescription("当前模组的中文名称。此处仅用于显示,不影响功能。", (AcceptableValueBase)null, new object[1] { new ConfigurationManagerAttributes { Order = 1000, CustomDrawer = DrawInfo, ReadOnly = true } })); moduleNameInfo.Value = "存档管家"; moduleVersionInfo = ((BaseUnityPlugin)this).Config.Bind("模组信息", "模组版本号", "1.0.0", new ConfigDescription("当前模组版本号。此处仅用于显示,不影响功能。", (AcceptableValueBase)null, new object[1] { new ConfigurationManagerAttributes { Order = 990, CustomDrawer = DrawInfo, ReadOnly = true } })); moduleVersionInfo.Value = "1.0.0"; contactInfo = ((BaseUnityPlugin)this).Config.Bind("模组信息", "REPO交流QQ群", "824639225", new ConfigDescription("REPO游戏交流、BUG反馈、优化建议、功能请求请加QQ群。此处仅用于显示,不影响功能。", (AcceptableValueBase)null, new object[1] { new ConfigurationManagerAttributes { Order = 980, CustomDrawer = DrawInfo, ReadOnly = true } })); contactInfo.Value = "824639225"; featureEnabled = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "启用", true, ConfigDescriptionWithOrder("开启存档保护。关闭后,本模组不会拦截删档或保存。", 900)); allowPlayerDelete = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "允许玩家手动删除存档", true, ConfigDescriptionWithOrder("开启后,玩家在存档菜单里主动删除存档会被放行。关闭后,玩家手动删除也会被阻止。", 890)); blockGameDelete = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "阻止游戏自动删除存档", true, ConfigDescriptionWithOrder("开启后,游戏流程触发的自动删档会被阻止。建议保持开启。", 880)); blockDeathOverwrite = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "阻止死亡覆盖存档", true, ConfigDescriptionWithOrder("开启后,玩家死亡、全员死亡或进入竞技场结算时,会阻止游戏把失败后的状态写进当前存档。", 870)); savePublicRooms = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "公开房间也保存存档", true, ConfigDescriptionWithOrder("开启后,公开匹配房间会像私人房间一样在正常过关、回车、进商店等流程保存进度。死亡和竞技场危险保存仍会被保护逻辑拦截。", 865)); autoReloadSingleplayer = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "单人死亡后自动读档", false, ConfigDescriptionWithOrder("实验功能。开启后,单人模式死亡数秒后会自动重新载入当前存档。多人模式先不自动读档。", 860)); restoreLatestBackupBeforeReload = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "自动读档前恢复最新备份", true, ConfigDescriptionWithOrder("实验功能。单人自动读档前,把当前存档目录里编号最大的备份文件复制回主存档文件。", 850)); restoreBackupAfterArenaReturn = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "死亡比赛后校验最新进度", true, ConfigDescriptionWithOrder("开启后,多人全灭进入死亡比赛再回到房间时,会比较主存档和最新备份,保留进度更高的一份,避免回退关卡。", 840)); restoreSaveAfterSingleplayerDeath = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "单人死亡后恢复最新存档", true, ConfigDescriptionWithOrder("开启后,单人死亡流程结束后会重新读取当前主存档/备份中进度更新的一份,避免重新从第一关开始。", 835)); manualRestoreShortcut = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "手动恢复最新进度快捷键", new KeyboardShortcut((KeyCode)290, Array.Empty()), ConfigDescriptionWithOrder("按下后,会对当前存档执行主存档/最新备份进度比较,并重新读取进度更高的一份。用于出现回退时手动救回进度。", 832)); maxBackupCount = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "最多保留备份数量", 20, ConfigDescriptionWithOrder("每次保存后,自动清理当前存档目录中过旧的 _BACKUP 文件。设为 0 表示不自动清理。", (AcceptableValueBase)(object)new AcceptableValueRange(0, 200), 831)); showConflictWarning = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "提示NoSaveDelete冲突", true, ConfigDescriptionWithOrder("开启后,如果检测到原 No Save Delete 模组同时安装,会在日志和游戏内弹窗提示。建议不要同时启用两个存档保护模组。", 829)); verboseLogging = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "详细日志", false, ConfigDescriptionWithOrder("开启后输出保存、恢复、备份清理等详细日志。发布和日常游玩建议关闭。", 828)); deathArenaMode = ((BaseUnityPlugin)this).Config.Bind("A.存档管家", "死亡后进入比赛类型", "赛车比赛", new ConfigDescription("选择全灭后进入哪种比赛。赛车比赛:强制进入赛车;皇冠竞技场:强制进入普通皇冠竞技场;官方随机:完全使用游戏原版随机逻辑。", (AcceptableValueBase)(object)new AcceptableValueList(new string[3] { "赛车比赛", "皇冠竞技场", "官方随机" }), new object[1] { new ConfigurationManagerAttributes { Order = 830 } })); DetectNoSaveDeleteConflict(); } private void Update() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) ShowConflictWarningOnce(); if (IsEnabled() && manualRestoreShortcut != null) { KeyboardShortcut value = manualRestoreShortcut.Value; if (((KeyboardShortcut)(ref value)).IsDown() && !((Object)(object)StatsManager.instance == (Object)null) && SemiFunc.IsMasterClientOrSingleplayer()) { ManualRestoreLatestProgress(); } } } private void DrawInfo(ConfigEntryBase entry) { GUILayout.Label(entry.BoxedValue?.ToString() ?? string.Empty, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) }); } private static ConfigDescription ConfigDescriptionWithOrder(string description, int order) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown return new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { new ConfigurationManagerAttributes { Order = order } }); } private static ConfigDescription ConfigDescriptionWithOrder(string description, AcceptableValueBase acceptableValues, int order) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown return new ConfigDescription(description, acceptableValues, new object[1] { new ConfigurationManagerAttributes { Order = order } }); } private static bool IsEnabled() { if (featureEnabled != null) { return featureEnabled.Value; } return false; } private static bool ShouldBlockDeathOverwrite() { if (IsEnabled() && blockDeathOverwrite != null) { return blockDeathOverwrite.Value; } return false; } private static void LogInfo(string message) { if (verboseLogging != null && verboseLogging.Value) { ZichenSaveKeeperPlugin instance = Instance; if (instance != null) { ((BaseUnityPlugin)instance).Logger.LogInfo((object)message); } } } private static void LogWarning(string message) { ZichenSaveKeeperPlugin instance = Instance; if (instance != null) { ((BaseUnityPlugin)instance).Logger.LogWarning((object)message); } } private static void LogError(string message) { ZichenSaveKeeperPlugin instance = Instance; if (instance != null) { ((BaseUnityPlugin)instance).Logger.LogError((object)message); } } private static void ManualRestoreLatestProgress() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) if (manualRestoreRunning) { LogInfo("Manual restore ignored because another restore is already running."); return; } string currentSaveFileName = GetCurrentSaveFileName(); if (string.IsNullOrWhiteSpace(currentSaveFileName)) { LogWarning("Manual restore failed because current save is empty."); ShowPopup("当前没有可恢复的存档。", Color.yellow); return; } ZichenSaveKeeperPlugin instance = Instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(ManualRestoreLatestProgressCoroutine(currentSaveFileName)); } } [IteratorStateMachine(typeof(d__58))] private static IEnumerator ManualRestoreLatestProgressCoroutine(string saveFileName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__58(0) { saveFileName = saveFileName }; } private static void ShowPopup(string message, Color color) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)MenuManager.instance != (Object)null) { MenuManager.instance.PagePopUp("SaveKeeper", color, message, "OK", false); } } catch { } } private static void DetectNoSaveDeleteConflict() { try { noSaveDeleteConflictDetected = false; foreach (KeyValuePair pluginInfo in Chainloader.PluginInfos) { string text = pluginInfo.Key ?? string.Empty; PluginInfo value = pluginInfo.Value; object obj; if (value == null) { obj = null; } else { BepInPlugin metadata = value.Metadata; obj = ((metadata != null) ? metadata.Name : null); } if (obj == null) { obj = string.Empty; } string text2 = (string)obj; if (LooksLikeNoSaveDelete(text) || LooksLikeNoSaveDelete(text2)) { noSaveDeleteConflictDetected = true; LogWarning("Detected possible No Save Delete conflict from loaded plugin: " + text + " / " + text2); return; } } string pluginPath = Paths.PluginPath; if (!Directory.Exists(pluginPath)) { return; } foreach (string item in Directory.EnumerateFileSystemEntries(pluginPath, "*", SearchOption.TopDirectoryOnly)) { if (LooksLikeNoSaveDelete(Path.GetFileName(item) ?? string.Empty)) { noSaveDeleteConflictDetected = true; LogWarning("Detected possible No Save Delete conflict in plugin folder: " + item); break; } } } catch (Exception ex) { LogWarning("Failed to scan No Save Delete conflict: " + ex.Message); } } private static bool LooksLikeNoSaveDelete(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } string text = value.Replace("_", string.Empty).Replace("-", string.Empty).Replace(" ", string.Empty); if (text.IndexOf("NoSaveDelete", StringComparison.OrdinalIgnoreCase) < 0) { return text.IndexOf("PxntxrezStudioNoSaveDelete", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static void ShowConflictWarningOnce() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) if (!conflictPopupShown && noSaveDeleteConflictDetected && showConflictWarning != null && showConflictWarning.Value && !((Object)(object)MenuManager.instance == (Object)null)) { conflictPopupShown = true; ShowPopup("检测到 No Save Delete 可能同时安装。建议禁用原模组,避免两个存档保护模组互相抢保存逻辑。", Color.yellow); } } [IteratorStateMachine(typeof(d__63))] private static IEnumerator ResetPlayerDeathBlockLater() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__63(0); } [IteratorStateMachine(typeof(d__64))] private static IEnumerator ResetMultiplayerDeathBlockLater() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__64(0); } [IteratorStateMachine(typeof(d__65))] private static IEnumerator ReloadSingleplayerLater(string saveFileName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__65(0) { saveFileName = saveFileName }; } [IteratorStateMachine(typeof(d__66))] private static IEnumerator RestoreSingleplayerSaveAfterDeathLater(string saveFileName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__66(0) { saveFileName = saveFileName }; } [IteratorStateMachine(typeof(d__67))] private static IEnumerator RestoreBackupAfterArenaReturnLater() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__67(0); } private static string GetCurrentSaveFileName() { if ((Object)(object)StatsManager.instance == (Object)null || StatsManagerCurrentSaveField == null) { return null; } return StatsManagerCurrentSaveField.GetValue(StatsManager.instance) as string; } private static bool AreAllPlayersDead() { if (PlayerAvatarDeadSetField == null) { return false; } List list = SemiFunc.PlayerGetList(); if (list == null || list.Count == 0) { return false; } foreach (PlayerAvatar item in list) { if (!((Object)(object)item == (Object)null)) { object value = PlayerAvatarDeadSetField.GetValue(item); if (!(value is bool) || !(bool)value) { return false; } } } return true; } private static bool IsArenaNow() { try { return SemiFunc.RunIsArena(); } catch { return false; } } private static bool IsShopNow() { try { return SemiFunc.RunIsShop(); } catch { return false; } } private static bool IsLevelArena(Level level) { try { return SemiFunc.IsLevelArena(level); } catch { return false; } } private static bool IsLevelShop(Level level) { try { return SemiFunc.IsLevelShop(level); } catch { return false; } } private static Level FindArenaRaceLevel(List arenaLevels) { if (arenaLevels == null || arenaLevels.Count == 0) { return null; } foreach (Level arenaLevel in arenaLevels) { if (IsRaceLevelName(arenaLevel)) { return arenaLevel; } } if (arenaLevels.Count > 1) { return arenaLevels[1]; } return null; } private static Level FindCrownArenaLevel(List arenaLevels) { if (arenaLevels == null || arenaLevels.Count == 0) { return null; } foreach (Level arenaLevel in arenaLevels) { if (!IsRaceLevelName(arenaLevel)) { return arenaLevel; } } return arenaLevels[0]; } private static bool IsRaceLevelName(Level level) { if ((Object)(object)level == (Object)null) { return false; } string text = ((Object)level).name ?? string.Empty; string text2 = level.NarrativeName ?? string.Empty; if (text.IndexOf("race", StringComparison.OrdinalIgnoreCase) < 0 && text2.IndexOf("race", StringComparison.OrdinalIgnoreCase) < 0 && text.IndexOf("racing", StringComparison.OrdinalIgnoreCase) < 0) { return text2.IndexOf("racing", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static bool IsLobbyOrLobbyMenuNow() { try { return SemiFunc.RunIsLobby() || SemiFunc.RunIsLobbyMenu(); } catch { return false; } } private static void TryLoadGameInMemory(string saveFileName) { try { MethodInfo method = typeof(StatsManager).GetMethod("LoadGame", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method == null || (Object)(object)StatsManager.instance == (Object)null) { LogWarning("StatsManager.LoadGame was not found."); return; } method.Invoke(StatsManager.instance, new object[2] { saveFileName, null }); } catch (Exception ex) { LogError("Failed to reload save in memory: " + ex.Message); } } private static void RestoreLatestBackup(string saveFileName) { try { string text = Path.Combine(Application.persistentDataPath, "saves", saveFileName); if (!Directory.Exists(text)) { LogWarning("Save directory was not found: " + text); return; } string text2 = FindLatestBackup(text, saveFileName); if (string.IsNullOrEmpty(text2)) { LogWarning("No backup file found for save: " + saveFileName); return; } string destFileName = Path.Combine(text, saveFileName + ".es3"); File.Copy(text2, destFileName, overwrite: true); LogInfo("Restored latest backup: " + text2); } catch (Exception ex) { LogError("Failed to restore latest backup: " + ex.Message); } } private static void RestoreBestSaveAfterArenaReturn(string saveFileName) { try { string text = Path.Combine(Application.persistentDataPath, "saves", saveFileName); string text2 = Path.Combine(text, saveFileName + ".es3"); if (!Directory.Exists(text)) { LogWarning("Save directory was not found: " + text); return; } string text3 = FindLatestBackup(text, saveFileName); if (string.IsNullOrEmpty(text3)) { LogWarning("No backup file found after arena return for save: " + saveFileName); return; } if (!File.Exists(text2)) { File.Copy(text3, text2, overwrite: true); LogInfo("Main save was missing. Restored latest backup: " + text3); return; } int num = ReadSaveRunLevel(saveFileName, saveFileName); int num2 = ReadSaveRunLevel(saveFileName, Path.GetFileNameWithoutExtension(text3)); if (num2 > num) { File.Copy(text3, text2, overwrite: true); LogInfo("Backup has newer progress. Restored backup level " + num2 + " over main level " + num + "."); } else { LogInfo("Keeping main save after arena return. Main level=" + num + ", backup level=" + num2 + "."); } } catch (Exception ex) { LogError("Failed to choose best save after arena return: " + ex.Message); } } private static int ReadSaveRunLevel(string folderName, string fileName) { try { StatsManager instance = StatsManager.instance; if (int.TryParse((instance != null) ? instance.SaveFileGetRunLevel(folderName, fileName) : null, out var result)) { return result; } } catch (Exception ex) { LogWarning("Failed to read save level from " + fileName + ": " + ex.Message); } return -1; } private static string FindLatestBackup(string saveDirectory, string saveFileName) { return (from path in Directory.GetFiles(saveDirectory, saveFileName + "_BACKUP*.es3") select new { Path = path, Number = ExtractBackupNumber(path, BackupNumberRegex) } into item where item.Number >= 0 orderby item.Number descending select item.Path).FirstOrDefault(); } private static void CleanupOldBackups(string saveFileName) { try { if (maxBackupCount == null || maxBackupCount.Value <= 0 || string.IsNullOrWhiteSpace(saveFileName)) { return; } string path2 = Path.Combine(Application.persistentDataPath, "saves", saveFileName); if (!Directory.Exists(path2)) { return; } List list = (from path in Directory.GetFiles(path2, saveFileName + "_BACKUP*.es3") select new BackupFileInfo { Path = path, Number = ExtractBackupNumber(path, BackupNumberRegex) } into item where item.Number >= 0 orderby item.Number descending select item).ToList(); if (list.Count <= maxBackupCount.Value) { return; } foreach (BackupFileInfo item in list.Skip(maxBackupCount.Value)) { File.Delete(item.Path); LogInfo("Deleted old backup: " + item.Path); } } catch (Exception ex) { LogError("Failed to clean old backups: " + ex.Message); } } private static int ExtractBackupNumber(string filePath, Regex regex) { Match match = regex.Match(Path.GetFileNameWithoutExtension(filePath)); if (match.Success && int.TryParse(match.Groups[1].Value, out var result)) { return result; } return -1; } } internal sealed class ConfigurationManagerAttributes { public bool? ShowRangeAsPercent; public Action CustomDrawer; public bool? Browsable; public string Category; public object DefaultValue; public bool? HideDefaultButton; public bool? HideSettingName; public string Description; public string DispName; public int? Order; public bool? ReadOnly; public bool? IsAdvanced; } internal sealed class BackupFileInfo { public string Path; public int Number; }