Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of SaveKeeper v1.0.0
Zichen-SaveKeeper.dll
Decompiled 3 hours agousing 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<Level>(__instance.levelArena); __instance.levelArena = new List<Level> { 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 <ManualRestoreLatestProgressCoroutine>d__58 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string saveFileName; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ManualRestoreLatestProgressCoroutine>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<string>)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 <ReloadSingleplayerLater>d__65 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string saveFileName; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ReloadSingleplayerLater>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<string>)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 <ResetMultiplayerDeathBlockLater>d__64 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ResetMultiplayerDeathBlockLater>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 <ResetPlayerDeathBlockLater>d__63 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ResetPlayerDeathBlockLater>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 <RestoreBackupAfterArenaReturnLater>d__67 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <RestoreBackupAfterArenaReturnLater>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 <RestoreSingleplayerSaveAfterDeathLater>d__66 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string saveFileName; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <RestoreSingleplayerSaveAfterDeathLater>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<bool> featureEnabled; private static ConfigEntry<bool> allowPlayerDelete; private static ConfigEntry<bool> blockGameDelete; private static ConfigEntry<bool> blockDeathOverwrite; private static ConfigEntry<bool> savePublicRooms; private static ConfigEntry<bool> autoReloadSingleplayer; private static ConfigEntry<bool> restoreLatestBackupBeforeReload; private static ConfigEntry<bool> restoreBackupAfterArenaReturn; private static ConfigEntry<bool> restoreSaveAfterSingleplayerDeath; private static ConfigEntry<KeyboardShortcut> manualRestoreShortcut; private static ConfigEntry<int> maxBackupCount; private static ConfigEntry<bool> showConflictWarning; private static ConfigEntry<bool> verboseLogging; private static ConfigEntry<string> deathArenaMode; private Harmony harmony; private ConfigEntry<string> moduleNameInfo; private ConfigEntry<string> moduleVersionInfo; private ConfigEntry<string> contactInfo; private static bool playerDeathSaveBlocked; private static bool multiplayerDeathSaveBlocked; private static bool playerMenuDeleteInProgress; private static bool multiplayerArenaSeen; private static bool multiplayerArenaRestoreRunning; private static List<Level> 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<string>("模组信息", "模组名称", "存档管家", new ConfigDescription("当前模组的中文名称。此处仅用于显示,不影响功能。", (AcceptableValueBase)null, new object[1] { new ConfigurationManagerAttributes { Order = 1000, CustomDrawer = DrawInfo, ReadOnly = true } })); moduleNameInfo.Value = "存档管家"; moduleVersionInfo = ((BaseUnityPlugin)this).Config.Bind<string>("模组信息", "模组版本号", "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<string>("模组信息", "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<bool>("A.存档管家", "启用", true, ConfigDescriptionWithOrder("开启存档保护。关闭后,本模组不会拦截删档或保存。", 900)); allowPlayerDelete = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "允许玩家手动删除存档", true, ConfigDescriptionWithOrder("开启后,玩家在存档菜单里主动删除存档会被放行。关闭后,玩家手动删除也会被阻止。", 890)); blockGameDelete = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "阻止游戏自动删除存档", true, ConfigDescriptionWithOrder("开启后,游戏流程触发的自动删档会被阻止。建议保持开启。", 880)); blockDeathOverwrite = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "阻止死亡覆盖存档", true, ConfigDescriptionWithOrder("开启后,玩家死亡、全员死亡或进入竞技场结算时,会阻止游戏把失败后的状态写进当前存档。", 870)); savePublicRooms = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "公开房间也保存存档", true, ConfigDescriptionWithOrder("开启后,公开匹配房间会像私人房间一样在正常过关、回车、进商店等流程保存进度。死亡和竞技场危险保存仍会被保护逻辑拦截。", 865)); autoReloadSingleplayer = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "单人死亡后自动读档", false, ConfigDescriptionWithOrder("实验功能。开启后,单人模式死亡数秒后会自动重新载入当前存档。多人模式先不自动读档。", 860)); restoreLatestBackupBeforeReload = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "自动读档前恢复最新备份", true, ConfigDescriptionWithOrder("实验功能。单人自动读档前,把当前存档目录里编号最大的备份文件复制回主存档文件。", 850)); restoreBackupAfterArenaReturn = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "死亡比赛后校验最新进度", true, ConfigDescriptionWithOrder("开启后,多人全灭进入死亡比赛再回到房间时,会比较主存档和最新备份,保留进度更高的一份,避免回退关卡。", 840)); restoreSaveAfterSingleplayerDeath = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "单人死亡后恢复最新存档", true, ConfigDescriptionWithOrder("开启后,单人死亡流程结束后会重新读取当前主存档/备份中进度更新的一份,避免重新从第一关开始。", 835)); manualRestoreShortcut = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("A.存档管家", "手动恢复最新进度快捷键", new KeyboardShortcut((KeyCode)290, Array.Empty<KeyCode>()), ConfigDescriptionWithOrder("按下后,会对当前存档执行主存档/最新备份进度比较,并重新读取进度更高的一份。用于出现回退时手动救回进度。", 832)); maxBackupCount = ((BaseUnityPlugin)this).Config.Bind<int>("A.存档管家", "最多保留备份数量", 20, ConfigDescriptionWithOrder("每次保存后,自动清理当前存档目录中过旧的 _BACKUP 文件。设为 0 表示不自动清理。", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 200), 831)); showConflictWarning = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "提示NoSaveDelete冲突", true, ConfigDescriptionWithOrder("开启后,如果检测到原 No Save Delete 模组同时安装,会在日志和游戏内弹窗提示。建议不要同时启用两个存档保护模组。", 829)); verboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("A.存档管家", "详细日志", false, ConfigDescriptionWithOrder("开启后输出保存、恢复、备份清理等详细日志。发布和日常游玩建议关闭。", 828)); deathArenaMode = ((BaseUnityPlugin)this).Config.Bind<string>("A.存档管家", "死亡后进入比赛类型", "赛车比赛", new ConfigDescription("选择全灭后进入哪种比赛。赛车比赛:强制进入赛车;皇冠竞技场:强制进入普通皇冠竞技场;官方随机:完全使用游戏原版随机逻辑。", (AcceptableValueBase)(object)new AcceptableValueList<string>(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(<ManualRestoreLatestProgressCoroutine>d__58))] private static IEnumerator ManualRestoreLatestProgressCoroutine(string saveFileName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ManualRestoreLatestProgressCoroutine>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<string, PluginInfo> 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(<ResetPlayerDeathBlockLater>d__63))] private static IEnumerator ResetPlayerDeathBlockLater() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ResetPlayerDeathBlockLater>d__63(0); } [IteratorStateMachine(typeof(<ResetMultiplayerDeathBlockLater>d__64))] private static IEnumerator ResetMultiplayerDeathBlockLater() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ResetMultiplayerDeathBlockLater>d__64(0); } [IteratorStateMachine(typeof(<ReloadSingleplayerLater>d__65))] private static IEnumerator ReloadSingleplayerLater(string saveFileName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ReloadSingleplayerLater>d__65(0) { saveFileName = saveFileName }; } [IteratorStateMachine(typeof(<RestoreSingleplayerSaveAfterDeathLater>d__66))] private static IEnumerator RestoreSingleplayerSaveAfterDeathLater(string saveFileName) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <RestoreSingleplayerSaveAfterDeathLater>d__66(0) { saveFileName = saveFileName }; } [IteratorStateMachine(typeof(<RestoreBackupAfterArenaReturnLater>d__67))] private static IEnumerator RestoreBackupAfterArenaReturnLater() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <RestoreBackupAfterArenaReturnLater>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<PlayerAvatar> 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<Level> 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<Level> 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<BackupFileInfo> 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<ConfigEntryBase> 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; }