Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of SaveGameModChecker v1.0.0
SaveGameModChecker.dll
Decompiled 2 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Security; using System.Security.Permissions; using HarmonyLib; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.Persistence; using Il2CppScheduleOne.UI.MainMenu; using Il2CppSystem; using Il2CppSystem.Reflection; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using SaveGameModCheckerNs; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(SaveGameModCheckerClass), "SaveGameModChecker", "1.0.0", "xVilho", null)] [assembly: MelonColor(255, 200, 150, 255)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] namespace SaveGameModCheckerNs; internal static class SaveGameModCheckerConfig { private static MelonPreferences_Category Category; public static MelonPreferences_Entry<bool> EnableMod; public static MelonPreferences_Entry<bool> WarnOnMissingMods; public static MelonPreferences_Entry<bool> WarnOnNewMods; public static MelonPreferences_Entry<bool> EnableDebugLogging; public static void Setup() { Category = MelonPreferences.CreateCategory("SaveGameModChecker", "SaveGame Mod Settings"); EnableMod = Category.CreateEntry<bool>("EnableMod", true, "Master Switch", "If false, the mod will be completely disabled.", false, false, (ValueValidator)null, (string)null); WarnOnMissingMods = Category.CreateEntry<bool>("WarnOnMissingMods", true, "Warn on Missing Mods", "Show mismatch popup if mods from the save are missing.", false, false, (ValueValidator)null, (string)null); WarnOnNewMods = Category.CreateEntry<bool>("WarnOnNewMods", true, "Warn on New Mods", "Show mismatch popup if currently installed mods were not in the save.", false, false, (ValueValidator)null, (string)null); EnableDebugLogging = Category.CreateEntry<bool>("EnableDebugLogging", false, "Enable Debug Logging", "Whether to output debug logs to the MelonLoader console.", false, false, (ValueValidator)null, (string)null); Category.SaveToFile(true); if (!EnableMod.Value) { SaveGameModCheckerClass.Log("[Config] Master switch is OFF. Mod functionality is suspended."); } else { SaveGameModCheckerClass.Log("[Config] SaveGameModCheckerConfig initialized."); } } } [HarmonyPatch(typeof(LoadManager), "StartGame")] public static class LoadManager_StartGame_Patch { public static SaveInfo StoredSaveInfoToLoad; public static bool BypassNextLoad; public static bool Prefix(LoadManager __instance, SaveInfo info) { //IL_03d3: Unknown result type (might be due to invalid IL or missing references) //IL_03da: Expected O, but got Unknown try { if (!SaveGameModCheckerConfig.EnableMod.Value) { return true; } if (info == null) { return true; } if (BypassNextLoad) { BypassNextLoad = false; return true; } int saveSlotNumber = info.SaveSlotNumber; string text = $"SAVEGAME_{saveSlotNumber}"; string normalizedSaveFolderName = SaveGameModCheckerClass.GetNormalizedSaveFolderName(info.SavePath); if (!string.IsNullOrEmpty(normalizedSaveFolderName)) { text = normalizedSaveFolderName; } string text2 = Path.Combine(Path.Combine(MelonEnvironment.UserDataDirectory, "SaveGameModChecker", text), "Mods.txt"); List<string> list = MelonTypeBase<MelonMod>.RegisteredMelons.Select((MelonMod m) => ((MelonBase)m).Info.Name).ToList(); bool flag = false; bool flag2 = false; List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); List<string> list4 = new List<string>(); if (File.Exists(text2)) { string[] array = (from s in File.ReadAllLines(text2) select s.Trim() into s where !string.IsNullOrWhiteSpace(s) select s).ToArray(); HashSet<string> hashSet = new HashSet<string>(array); string[] array2 = array; foreach (string item in array2) { if (!list.Contains(item)) { list2.Add(item); flag = true; } else { list4.Add(item); } } foreach (string item2 in list) { if (!hashSet.Contains(item2)) { list3.Add(item2); flag2 = true; } } } if (!flag && !flag2 && File.Exists(text2)) { SaveGameModCheckerClass.Log("Mods match perfectly for " + text + ". Read from " + text2); } bool flag3 = false; if (flag && SaveGameModCheckerConfig.WarnOnMissingMods.Value) { flag3 = true; } if (flag2 && SaveGameModCheckerConfig.WarnOnNewMods.Value) { flag3 = true; } if (flag3) { list2.Sort(); list3.Sort(); list4.Sort(); string text3 = "[WARNING] " + text + " mod mismatch."; if (flag) { text3 = text3 + "\nMissing:\n- " + string.Join("\n- ", list2); } if (flag2) { text3 = text3 + "\nNew (Not in save):\n- " + string.Join("\n- ", list3); } SaveGameModCheckerClass.LogWarning(text3); string text4 = ""; if (flag) { text4 += "<size=120%><b><color=red>MISSING FROM SAVE:</color></b></size>\n"; foreach (string item3 in list2) { text4 = text4 + "- <color=red>" + item3 + "</color>\n"; } } if (flag2) { if (text4 != "") { text4 += "\n"; } text4 += "<size=120%><b><color=#FFA500>NEW (NOT IN SAVE):</color></b></size>\n"; foreach (string item4 in list3) { text4 = text4 + "- <color=#FFA500>" + item4 + "</color>\n"; } } if (list4.Count > 0) { if (text4 != "") { text4 += "\n"; } text4 += "<size=120%><b><color=green>MATCHED MODS:</color></b></size>\n"; foreach (string item5 in list4) { text4 = text4 + "- <color=green>" + item5 + "</color>\n"; } } Data val = new Data("Mods Mismatch", text4.Trim(), true); StoredSaveInfoToLoad = info; MainMenuPopup val2 = Object.FindObjectOfType<MainMenuPopup>(true); if ((Object)(object)val2 != (Object)null) { val2.Open(val); } else { __instance.ExitToMenu((SaveInfo)null, val, false); } return false; } } catch (Exception value) { SaveGameModCheckerClass.LogError($"Error in LoadManager.StartGame prefix: {value}"); } return true; } } [HarmonyPatch(typeof(SaveManager), "Save", new Type[] { })] public static class SaveManager_Save_NoArg_Patch { [HarmonyPostfix] public static void Postfix() { try { SaveGameModCheckerClass.SaveCurrentMods(); } catch (Exception value) { SaveGameModCheckerClass.LogError($"Error in SaveManager.Save() postfix: {value}"); } } } [HarmonyPatch(typeof(SaveManager), "Save", new Type[] { typeof(string) })] public static class SaveManager_Save_String_Patch { [HarmonyPostfix] public static void Postfix(string saveFolderPath) { try { SaveGameModCheckerClass.SaveCurrentMods(saveFolderPath); } catch (Exception value) { SaveGameModCheckerClass.LogError($"Error in SaveManager.Save(string) postfix: {value}"); } } } [HarmonyPatch(typeof(MainMenuPopup), "Open", new Type[] { typeof(Data) })] public static class MainMenuPopup_Open_Patch { private static Button btnSafety; private static Button btnQuit; private static Button btnContinue; private static GameObject scrollContainer; public static void Postfix(MainMenuPopup __instance, Data data) { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Expected O, but got Unknown //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Expected O, but got Unknown //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01e9: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_0215: Unknown result type (might be due to invalid IL or missing references) //IL_022b: Unknown result type (might be due to invalid IL or missing references) //IL_0241: Unknown result type (might be due to invalid IL or missing references) //IL_02f3: Unknown result type (might be due to invalid IL or missing references) //IL_0308: Unknown result type (might be due to invalid IL or missing references) //IL_031d: Unknown result type (might be due to invalid IL or missing references) //IL_0332: Unknown result type (might be due to invalid IL or missing references) //IL_033c: Unknown result type (might be due to invalid IL or missing references) //IL_0419: Unknown result type (might be due to invalid IL or missing references) //IL_042f: Unknown result type (might be due to invalid IL or missing references) //IL_0442: Unknown result type (might be due to invalid IL or missing references) try { if (data == null || data.Title != "Mods Mismatch") { return; } Image componentInChildren = ((Component)__instance).GetComponentInChildren<Image>(); if ((Object)(object)componentInChildren != (Object)null) { ((Graphic)componentInChildren).color = new Color(0.01f, 0.01f, 0.01f, 1f); } RectTransform component = ((Component)__instance).GetComponent<RectTransform>(); float num = Mathf.Min((float)Screen.width * 0.7f, 950f); float num2 = Mathf.Min((float)Screen.height * 0.8f, 750f); component.sizeDelta = new Vector2(num, num2); Component val = FindTMPro(((Component)__instance).gameObject, "Description"); if ((Object)(object)val != (Object)null && (Object)(object)scrollContainer == (Object)null) { GameObject gameObject = val.gameObject; scrollContainer = new GameObject("ModScrollContainer"); scrollContainer.transform.SetParent(gameObject.transform.parent, false); ScrollRect val2 = scrollContainer.AddComponent<ScrollRect>(); RectTransform component2 = scrollContainer.GetComponent<RectTransform>(); component2.anchorMin = new Vector2(0.05f, 0.25f); component2.anchorMax = new Vector2(0.95f, 0.88f); Vector2 offsetMin = (component2.offsetMax = Vector2.zero); component2.offsetMin = offsetMin; GameObject val3 = new GameObject("Viewport"); val3.transform.SetParent(scrollContainer.transform, false); RectTransform val4 = val3.AddComponent<RectTransform>(); val4.anchorMin = Vector2.zero; val4.anchorMax = Vector2.one; val4.sizeDelta = Vector2.zero; ((Graphic)val3.AddComponent<Image>()).color = new Color(0.05f, 0.05f, 0.05f, 0.9f); val3.AddComponent<Mask>().showMaskGraphic = true; gameObject.transform.SetParent(val3.transform, false); RectTransform component3 = gameObject.GetComponent<RectTransform>(); component3.anchorMin = new Vector2(0f, 1f); component3.anchorMax = new Vector2(1f, 1f); component3.pivot = new Vector2(0.5f, 1f); component3.offsetMin = new Vector2(20f, 0f); component3.offsetMax = new Vector2(-20f, 0f); (gameObject.GetComponent<ContentSizeFitter>() ?? gameObject.AddComponent<ContentSizeFitter>()).verticalFit = (FitMode)2; val2.content = component3; val2.viewport = val4; val2.horizontal = false; val2.vertical = true; val2.scrollSensitivity = 35f; Il2CppReferenceArray<Object> val5 = Resources.FindObjectsOfTypeAll(Il2CppType.Of<Scrollbar>()); if (val5 != null && ((Il2CppArrayBase<Object>)(object)val5).Length > 0) { Scrollbar component4 = Object.Instantiate<GameObject>(((Component)((Il2CppObjectBase)((Il2CppArrayBase<Object>)(object)val5)[0]).Cast<Scrollbar>()).gameObject, scrollContainer.transform).GetComponent<Scrollbar>(); component4.direction = (Direction)2; RectTransform component5 = ((Component)component4).GetComponent<RectTransform>(); component5.anchorMin = new Vector2(1f, 0f); component5.anchorMax = new Vector2(1f, 1f); component5.pivot = new Vector2(1f, 0.5f); component5.sizeDelta = new Vector2(25f, 0f); component5.anchoredPosition = Vector2.zero; val2.verticalScrollbar = component4; } } Button val6 = ((IEnumerable<Button>)((Component)__instance).GetComponentsInChildren<Button>(true)).FirstOrDefault((Func<Button, bool>)((Button b) => (Object)(object)((Component)b).GetComponent<ModCheckerButtonHandler>() == (Object)null && ((Component)b).gameObject.activeSelf)); if ((Object)(object)val6 != (Object)null) { ((Component)val6).gameObject.SetActive(false); if ((Object)(object)btnSafety == (Object)null) { btnSafety = CreateButton(val6, "Back to Safety", "Safety"); } if ((Object)(object)btnQuit == (Object)null) { btnQuit = CreateButton(val6, "Quit Game", "Quit"); } if ((Object)(object)btnContinue == (Object)null) { btnContinue = CreateButton(val6, "Continue Anyway", "Continue"); } float num3 = 70f; float num4 = num * 0.3f; SetupButton(btnSafety, new Vector2(0f - num4, num3)); SetupButton(btnQuit, new Vector2(0f, num3)); SetupButton(btnContinue, new Vector2(num4, num3)); } } catch (Exception value) { SaveGameModCheckerClass.LogError($"Error in MainMenuPopup.Open postfix: {value}"); } } private static Button CreateButton(Button template, string label, string action) { Button component = Object.Instantiate<GameObject>(((Component)template).gameObject, ((Component)template).transform.parent).GetComponent<Button>(); ((Object)((Component)component).gameObject).name = action + "Btn"; ((Component)component).gameObject.SetActive(true); SetTMProText(FindTMPro(((Component)component).gameObject), label); ((Component)component).gameObject.AddComponent<ModCheckerButtonHandler>().ActionType = action; return component; } private static void SetupButton(Button btn, Vector2 anchoredPos) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) RectTransform component = ((Component)btn).GetComponent<RectTransform>(); Vector2 val = default(Vector2); ((Vector2)(ref val))..ctor(0.5f, 0f); component.anchorMax = val; component.anchorMin = val; component.anchoredPosition = anchoredPos; component.sizeDelta = new Vector2(240f, 60f); } private static Component FindTMPro(GameObject go, string nameContains = "") { return ((IEnumerable<Component>)go.GetComponentsInChildren<Component>(true)).FirstOrDefault((Func<Component, bool>)((Component c) => ((MemberInfo)((Object)c).GetIl2CppType()).Name.Contains("TextMeshProUGUI") && (string.IsNullOrEmpty(nameContains) || ((Object)c.gameObject).name.Contains(nameContains)))); } private static void SetTMProText(Component comp, string text) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown if (!((Object)(object)comp == (Object)null)) { PropertyInfo property = ((Object)comp).GetIl2CppType().GetProperty("text"); if (property != null) { property.SetValue((Object)(object)comp, new Object(IL2CPP.ManagedStringToIl2Cpp(text))); } } } } public class ModCheckerButtonHandler : MonoBehaviour { public string ActionType; public ModCheckerButtonHandler(IntPtr ptr) : base(ptr) { } private void OnEnable() { ((UnityEvent)((Component)this).GetComponent<Button>().onClick).AddListener(UnityAction.op_Implicit((Action)OnClick)); } private void OnClick() { try { switch (ActionType) { case "Continue": if (LoadManager_StartGame_Patch.StoredSaveInfoToLoad != null) { LoadManager_StartGame_Patch.BypassNextLoad = true; Singleton<MainMenuPopup>.Instance.Screen.Close(false); Singleton<LoadManager>.Instance.StartGame(LoadManager_StartGame_Patch.StoredSaveInfoToLoad, false, true); } else { SaveGameModCheckerClass.LogError("StoredSaveInfoToLoad is null during 'Continue Anyway'. Cannot proceed."); Singleton<MainMenuPopup>.Instance.Screen.Close(false); } break; case "Quit": Application.Quit(); break; case "Safety": Singleton<MainMenuPopup>.Instance.Screen.Close(false); break; } } catch (Exception value) { SaveGameModCheckerClass.LogError($"Error in ModCheckerButtonHandler.OnClick: {value}"); } } } public static class BuildInfo { public const string Name = "SaveGameModChecker"; public const string Author = "xVilho"; public const string Version = "1.0.0"; } public class SaveGameModCheckerClass : MelonMod { internal static Instance Logger; public static void Log(object msg) { if (SaveGameModCheckerConfig.EnableDebugLogging == null || SaveGameModCheckerConfig.EnableDebugLogging.Value) { Logger.Msg(msg?.ToString() ?? "null"); } } public static void LogWarning(object msg) { if (SaveGameModCheckerConfig.EnableDebugLogging == null || SaveGameModCheckerConfig.EnableDebugLogging.Value) { Logger.Warning(msg?.ToString() ?? "null"); } } public static void LogError(object msg) { Logger.Error(msg?.ToString() ?? "null"); } public override void OnInitializeMelon() { try { Logger = ((MelonBase)this).LoggerInstance; ClassInjector.RegisterTypeInIl2Cpp<ModCheckerButtonHandler>(); SaveGameModCheckerConfig.Setup(); Log("[Init] Successfully initialized."); } catch (Exception value) { LogError($"Failed to initialize mod: {value}"); } } public static string GetNormalizedSaveFolderName(string path) { if (string.IsNullOrEmpty(path)) { return null; } try { if (!path.Contains("/") && !path.Contains("\\") && (path.StartsWith("SAVEGAME_", StringComparison.OrdinalIgnoreCase) || path.StartsWith("SaveGame_", StringComparison.OrdinalIgnoreCase))) { return path.ToUpperInvariant(); } for (DirectoryInfo directoryInfo = new DirectoryInfo(path); directoryInfo != null; directoryInfo = directoryInfo.Parent) { if (directoryInfo.Name.StartsWith("SAVEGAME_", StringComparison.OrdinalIgnoreCase) || directoryInfo.Name.StartsWith("SaveGame_", StringComparison.OrdinalIgnoreCase)) { return directoryInfo.Name.ToUpperInvariant(); } } } catch { } return null; } public static void SaveCurrentMods(string overridePath = null) { try { if (!SaveGameModCheckerConfig.EnableMod.Value) { return; } SaveManager instance = Singleton<SaveManager>.Instance; string text = overridePath; if (string.IsNullOrEmpty(text) && (Object)(object)instance != (Object)null) { text = instance.PlayersSavePath; } if (string.IsNullOrEmpty(text) && (Object)(object)instance != (Object)null && !string.IsNullOrEmpty(instance.SaveName)) { text = instance.SaveName; } if (string.IsNullOrEmpty(text)) { LogWarning("Could not determine save path or name. Skipping mod list save."); return; } string normalizedSaveFolderName = GetNormalizedSaveFolderName(text); if (string.IsNullOrEmpty(normalizedSaveFolderName) && (Object)(object)instance != (Object)null && !string.IsNullOrEmpty(instance.SaveName)) { normalizedSaveFolderName = GetNormalizedSaveFolderName(instance.SaveName); } if (string.IsNullOrEmpty(normalizedSaveFolderName)) { LogWarning("[Save] Could not identify slot from source path or SaveName. Skipping mod list save."); return; } Log("[Save] Game triggered save for slot: " + normalizedSaveFolderName); string text2 = Path.Combine(MelonEnvironment.UserDataDirectory, "SaveGameModChecker", normalizedSaveFolderName); string path = Path.Combine(text2, "Mods.txt"); Log("[Save] Target storage: Steam/steamapps/common/Schedule I/UserData/SaveGameModChecker/" + normalizedSaveFolderName + "/Mods.txt"); if (!Directory.Exists(text2)) { Directory.CreateDirectory(text2); } List<string> list = MelonTypeBase<MelonMod>.RegisteredMelons.Select((MelonMod m) => ((MelonBase)m).Info.Name).ToList(); File.WriteAllLines(path, list); Log($"[Save] Successfully saved {list.Count} mods."); } catch (Exception value) { LogError($"Failed to save current mods: {value}"); } } }