Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of SaveExpansion MONO v1.0.1
Mods/SaveExpansion-Mono.dll
Decompiled 20 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 HarmonyLib; using MelonLoader; using MelonLoader.Preferences; using Microsoft.CodeAnalysis; using SaveExpansion; using SaveExpansion.Compatibility; using SaveExpansion.Configuration; using SaveExpansion.Features; using SaveExpansion.Logging; using SaveExpansion.UI; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(SaveExpansionMod), "Save Expansion", "1.0.1", "SirTidez", null)] [assembly: MelonColor(67, 151, 196, 255)] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SaveExpansion-Mono")] [assembly: AssemblyConfiguration("Release Mono")] [assembly: AssemblyFileVersion("1.0.1.0")] [assembly: AssemblyInformationalVersion("1.0.1")] [assembly: AssemblyProduct("SaveExpansion-Mono")] [assembly: AssemblyTitle("SaveExpansion-Mono")] [assembly: AssemblyVersion("1.0.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [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; } } } namespace SaveExpansion { public static class BuildInfo { public const string Name = "Save Expansion"; public const string Description = "Extends the native Schedule I save picker with additional slots."; public const string Author = "SirTidez"; public const string Version = "1.0.1"; } public sealed class SaveExpansionMod : MelonMod { private SaveExpansionFeature? _feature; public override void OnInitializeMelon() { SaveExpansionSettings.Instance.Load(); ModLog.DebugEnabled = SaveExpansionSettings.Instance.EnableDebugLogging; _feature = new SaveExpansionFeature(); ModLog.Debug("Save Expansion v1.0.1 initialized."); } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { _feature?.NotifySceneLoaded(sceneName); } public override void OnUpdate() { _feature?.TickMenuDiscovery(); } } } namespace SaveExpansion.UI { public sealed class VanillaSaveUiExtender { private sealed class DisplayPlan { public object Display { get; } public object Slots { get; } public int CurrentCount { get; } public RectTransform? Template { get; } public RectTransform? Previous { get; } public RectTransform? Content { get; } public Button? SlotButton { get; } public object? ActionTarget { get; } public MethodInfo? ActionMethod { get; } public bool IsNewGame { get; } public DisplayPlan(object display, object slots, int currentCount, RectTransform? template, RectTransform? previous, RectTransform? content, Button? slotButton, object? actionTarget, MethodInfo? actionMethod, bool isNewGame = false) { Display = display; Slots = slots; CurrentCount = currentCount; Template = template; Previous = previous; Content = content; SlotButton = slotButton; ActionTarget = actionTarget; ActionMethod = actionMethod; IsNewGame = isNewGame; } } private readonly struct VerticalBounds { public readonly float min; public readonly float max; public float size => max - min; public VerticalBounds(float min, float max) { this.min = min; this.max = max; } } private sealed class ScrollInputState { public ScrollRect ScrollRect { get; } public bool IsDragging { get; set; } public float LastPointerY { get; set; } public ScrollInputState(ScrollRect scrollRect) { ScrollRect = scrollRect; } } private readonly List<ScrollInputState> _scrollInputs = new List<ScrollInputState>(); public void TickScrollInput() { //IL_0069: 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) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) for (int num = _scrollInputs.Count - 1; num >= 0; num--) { ScrollInputState scrollInputState = _scrollInputs[num]; ScrollRect scrollRect = scrollInputState.ScrollRect; RectTransform val = (((Object)(object)scrollRect != (Object)null) ? scrollRect.viewport : null); if ((Object)(object)scrollRect == (Object)null || (Object)(object)val == (Object)null) { _scrollInputs.RemoveAt(num); } else { if (RectTransformUtility.RectangleContainsScreenPoint(val, Vector2.op_Implicit(Input.mousePosition))) { float y = Input.mouseScrollDelta.y; if (Mathf.Abs(y) > 0.001f) { scrollRect.verticalNormalizedPosition = Mathf.Clamp01(scrollRect.verticalNormalizedPosition + y * 0.12f); } if (Input.GetMouseButtonDown(0)) { scrollInputState.IsDragging = true; scrollInputState.LastPointerY = Input.mousePosition.y; } } if (!Input.GetMouseButton(0)) { scrollInputState.IsDragging = false; } else if (scrollInputState.IsDragging) { float y2 = Input.mousePosition.y; float num2 = y2 - scrollInputState.LastPointerY; scrollInputState.LastPointerY = y2; if (Mathf.Abs(num2) > 0.001f) { float verticalNormalizedPosition = scrollRect.verticalNormalizedPosition; Rect rect = val.rect; scrollRect.verticalNormalizedPosition = Mathf.Clamp01(verticalNormalizedPosition + num2 / Mathf.Max(1f, ((Rect)(ref rect)).height)); } } } } } public bool TryRefreshNativeSaveInfo(ResolvedGameContract contract, out string failure) { foreach (object item in FindRuntimeObjects(contract.LoadManagerType)) { object obj = ToResolvedWrapper(item, contract.LoadManagerType); if (obj == null) { continue; } contract.RefreshSaveInfoMethod.Invoke(obj, null); failure = string.Empty; return true; } failure = "The native LoadManager was not instantiated, so save metadata could not be refreshed safely."; return false; } public bool HasExpandedSlotArray(ResolvedGameContract contract, object display, int requestedCapacity) { try { return GetCount(contract.SlotsProperty.GetValue(display)) >= requestedCapacity; } catch (Exception ex) { ModLog.Warn("Could not inspect SaveDisplay slot capacity during refresh: " + ex.Message); return false; } } public bool TryExpandSaveGames(ResolvedGameContract contract, int requestedCapacity, out object? originalSaveGames, out string failure) { originalSaveGames = contract.SaveGamesProperty.GetValue(null); int count = GetCount(originalSaveGames); if (originalSaveGames == null) { failure = "LoadManager.SaveGames was not initialized after the native metadata refresh."; return false; } if (count >= requestedCapacity) { ClearMismatchedExtendedSaveInfo(originalSaveGames, count); PopulateExtendedSaveInfo(contract, originalSaveGames, count); failure = string.Empty; return true; } if (!TryCreateSameRuntimeArray(originalSaveGames, requestedCapacity, out object expanded)) { failure = "Could not create a native-compatible LoadManager.SaveGames array."; return false; } for (int i = 0; i < count; i++) { SetItem(expanded, i, GetItem(originalSaveGames, i)); } contract.SaveGamesProperty.SetValue(null, expanded); ClearMismatchedExtendedSaveInfo(expanded, requestedCapacity); PopulateExtendedSaveInfo(contract, expanded, requestedCapacity); failure = string.Empty; return true; } public bool TryPopulateSaveSlot(ResolvedGameContract contract, int zeroBasedSlotIndex, string? createdSaveFolderPath, out string failure) { object value = contract.SaveGamesProperty.GetValue(null); if (value == null || zeroBasedSlotIndex < 0 || zeroBasedSlotIndex >= GetCount(value)) { failure = $"LoadManager.SaveGames does not contain expanded slot {zeroBasedSlotIndex + 1}."; return false; } if (GetItem(value, zeroBasedSlotIndex) != null) { failure = string.Empty; return true; } foreach (string saveFolderCandidate in GetSaveFolderCandidates(value, createdSaveFolderPath, zeroBasedSlotIndex)) { if (!TryLoadSaveInfo(contract, saveFolderCandidate, zeroBasedSlotIndex, out object saveInfo)) { continue; } SetItem(value, zeroBasedSlotIndex, saveInfo); failure = string.Empty; return true; } failure = $"The native metadata reader did not find a usable save for slot {zeroBasedSlotIndex + 1}."; return false; } public bool TryRouteExpandedNewGameSlot(ResolvedGameContract contract, object newGameScreen, int zeroBasedSlotIndex, out string failure) { if (zeroBasedSlotIndex < 5) { failure = "Vanilla slot; no expanded-slot routing was needed."; return false; } if (!TryExpandSaveGames(contract, Math.Max(zeroBasedSlotIndex + 1, 6), out object _, out failure)) { return false; } object value = contract.SaveGamesProperty.GetValue(null); if (value == null || zeroBasedSlotIndex >= GetCount(value)) { failure = $"LoadManager.SaveGames does not contain expanded slot {zeroBasedSlotIndex + 1}."; return false; } bool flag = GetItem(value, zeroBasedSlotIndex) != null; object obj = (flag ? contract.ConfirmOverwriteScreenProperty.GetValue(newGameScreen) : contract.SetupScreenProperty.GetValue(newGameScreen)); MethodInfo methodInfo = (flag ? contract.ConfirmOverwriteInitializeMethod : contract.SetupScreenInitializeMethod); if (obj == null) { failure = "The native " + (flag ? "overwrite" : "setup") + " screen was unavailable."; return false; } methodInfo.Invoke(obj, new object[1] { zeroBasedSlotIndex }); InvokeMenuScreenAction(obj, "Open"); if (!flag) { InvokeMenuScreenAction(newGameScreen, "Close"); } ModLog.Debug(string.Format("Opened native {0} for expanded save slot {1}.", flag ? "overwrite confirmation" : "setup", zeroBasedSlotIndex + 1)); failure = string.Empty; return true; } public string DescribeExpandedSaveMetadata(ResolvedGameContract contract) { object value = contract.SaveGamesProperty.GetValue(null); if (value == null) { return "expanded metadata=<unavailable>"; } List<string> list = new List<string>(); for (int i = 5; i < GetCount(value); i++) { object item = GetItem(value, i); string text = item?.GetType().GetProperty("SavePath", BindingFlags.Instance | BindingFlags.Public)?.GetValue(item) as string; list.Add(string.Format("{0}={1}", i + 1, string.IsNullOrWhiteSpace(text) ? "empty" : Path.GetFileName(text))); } return "expanded metadata=" + string.Join(", ", list); } private static void PopulateExtendedSaveInfo(ResolvedGameContract contract, object saveGames, int count) { for (int i = 5; i < count; i++) { if (GetItem(saveGames, i) != null) { continue; } foreach (string saveFolderCandidate in GetSaveFolderCandidates(saveGames, null, i)) { if (!TryLoadSaveInfo(contract, saveFolderCandidate, i, out object saveInfo)) { continue; } SetItem(saveGames, i, saveInfo); break; } } } private static bool TryLoadSaveInfo(ResolvedGameContract contract, string saveRoot, int zeroBasedSlotIndex, out object? saveInfo) { saveInfo = null; try { object[] array = new object[4] { saveRoot, zeroBasedSlotIndex + 1, null, false }; if (!(contract.TryLoadSaveInfoMethod.Invoke(null, array) is int num) || num == 0) { return false; } object obj = array[2]; if (obj == null) { return false; } if (!IsSaveInfoForSlot(obj, zeroBasedSlotIndex)) { return false; } saveInfo = obj; return true; } catch (Exception ex) { ModLog.Warn($"Native metadata lookup failed for expanded slot {zeroBasedSlotIndex + 1}: {ex.Message}"); return false; } } private static void InvokeMenuScreenAction(object screen, string methodName) { screen.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo candidate) => string.Equals(candidate.Name, methodName, StringComparison.OrdinalIgnoreCase) && candidate.GetParameters().Length == 0)?.Invoke(screen, null); } private static PropertyInfo? GetSavePathProperty(object saveGames) { for (int i = 0; i < GetCount(saveGames); i++) { PropertyInfo propertyInfo = GetItem(saveGames, i)?.GetType().GetProperty("SavePath", BindingFlags.Instance | BindingFlags.Public); if (propertyInfo?.PropertyType == typeof(string)) { return propertyInfo; } } return null; } private static void ClearMismatchedExtendedSaveInfo(object saveGames, int count) { for (int i = 5; i < count; i++) { object item = GetItem(saveGames, i); if (item != null && !IsSaveInfoForSlot(item, i)) { SetItem(saveGames, i, null); } } } private static bool IsSaveInfoForSlot(object saveInfo, int zeroBasedSlotIndex) { string text = saveInfo.GetType().GetProperty("SavePath", BindingFlags.Instance | BindingFlags.Public)?.GetValue(saveInfo) as string; if (string.IsNullOrWhiteSpace(text)) { return false; } return string.Equals(Path.GetFileName(text.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), $"SaveGame_{zeroBasedSlotIndex + 1}", StringComparison.OrdinalIgnoreCase); } private static IEnumerable<string> GetSaveFolderCandidates(object saveGames, string? createdSaveFolderPath, int zeroBasedSlotIndex) { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string expectedFolderName = $"SaveGame_{zeroBasedSlotIndex + 1}"; AddExactOrDerivedSlotPath(hashSet, createdSaveFolderPath, expectedFolderName); PropertyInfo savePathProperty = GetSavePathProperty(saveGames); if (savePathProperty == null) { return hashSet; } for (int i = 0; i < GetCount(saveGames); i++) { object item = GetItem(saveGames, i); if (item != null && savePathProperty.GetValue(item) is string text && !string.IsNullOrWhiteSpace(text)) { AddExactOrDerivedSlotPath(hashSet, text, expectedFolderName); } } return hashSet; } private static void AddExactOrDerivedSlotPath(ISet<string> candidates, string? path, string expectedFolderName) { if (string.IsNullOrWhiteSpace(path)) { return; } string fileName = Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); if (string.Equals(fileName, expectedFolderName, StringComparison.OrdinalIgnoreCase)) { candidates.Add(path); return; } string text = ((!fileName.StartsWith("SaveGame_", StringComparison.OrdinalIgnoreCase)) ? path : Directory.GetParent(path)?.FullName); if (!string.IsNullOrWhiteSpace(text)) { candidates.Add(Path.Combine(text, expectedFolderName)); } } public string Describe(ResolvedGameContract contract) { IEnumerable<object> enumerable = FindRuntimeObjects(contract.SaveDisplayType); List<string> list = new List<string>(); foreach (object item3 in enumerable) { object obj = ToResolvedWrapper(item3, contract.SaveDisplayType); if (obj == null) { list.Add("display=<could-not-wrap-resolved-type>"); continue; } object value = contract.SlotsProperty.GetValue(obj); List<string> list2 = new List<string>(); for (int i = 0; i < GetCount(value); i++) { object? item = GetItem(value, i); RectTransform val = (RectTransform)((item is RectTransform) ? item : null); if (val == null) { list2.Add($"{i}:<not-RectTransform>"); continue; } string arg = string.Join(",", from transform in ((Component)val).GetComponentsInChildren<Transform>(true).Take(12) select ((Object)transform).name); list2.Add($"{i}:{((Object)val).name}[children={arg}]"); } Component val2 = (Component)((obj is Component) ? obj : null); bool flag = (Object)(object)((val2 != null) ? val2.GetComponentInParent<ScrollRect>() : null) != (Object)null; object? item2 = GetItem(value, 0); string layoutDescription = GetLayoutDescription((RectTransform?)((item2 is RectTransform) ? item2 : null)); list.Add(string.Format("display={0}; slots={1}; parentScrollRect={2}; layout={3}; {4}", ((val2 != null) ? ((Object)val2).name : null) ?? obj.GetType().Name, GetCount(value), flag, layoutDescription, string.Join(" | ", list2))); } return "Save-menu layout receipt: " + string.Join(" || ", list); } public bool TryExtend(ResolvedGameContract contract, int requestedCapacity, out string failure) { IEnumerable<object> enumerable = FindRuntimeObjects(contract.SaveDisplayType); if (!enumerable.Any()) { failure = "The vanilla SaveDisplay was not instantiated yet."; return false; } List<DisplayPlan> list = new List<DisplayPlan>(); foreach (object item in enumerable) { object obj = ToResolvedWrapper(item, contract.SaveDisplayType); if (obj == null) { failure = "A native SaveDisplay could not be converted to its resolved runtime wrapper type."; return false; } if (!TryCreatePlan(obj, contract, requestedCapacity, out DisplayPlan plan, out failure)) { return false; } if (plan != null) { list.Add(plan); } } foreach (DisplayPlan item2 in list) { ExtendDisplay(item2, contract, requestedCapacity); } RenderKnownExtendedSaves(contract); failure = string.Empty; return true; } private static bool TryCreatePlan(object display, ResolvedGameContract contract, int requestedCapacity, out DisplayPlan? plan, out string failure) { plan = null; object value = contract.SlotsProperty.GetValue(display); int count = GetCount(value); if (count >= requestedCapacity) { plan = new DisplayPlan(display, value, count, null, null, null, null, null, null); failure = string.Empty; return true; } if (count >= 2) { object? item = GetItem(value, count - 1); RectTransform val = (RectTransform)((item is RectTransform) ? item : null); if (val != null) { object? item2 = GetItem(value, count - 2); RectTransform val2 = (RectTransform)((item2 is RectTransform) ? item2 : null); if (val2 != null) { Button val3 = FindNamedButton(val, "Button"); if ((Object)(object)val3 == (Object)null) { failure = "The native save slot's primary Button could not be identified."; return false; } Component val4 = (Component)((display is Component) ? display : null); object obj = (((Object)(object)val4 == (Object)null) ? null : FindOwningScreen(val4.transform, contract.NewGameScreenType)); MethodInfo actionMethod = contract.SlotSelectedMethod; if (obj == null && (Object)(object)val4 != (Object)null) { obj = FindOwningScreen(val4.transform, contract.ContinueScreenType); actionMethod = contract.LoadGameMethod; } if (obj == null) { failure = "The SaveDisplay could not be associated with its New Game or Continue screen."; return false; } RectTransform val5 = ResolveRectTransform(((Transform)val).parent); if ((Object)(object)val5 == (Object)null) { Transform parent = ((Transform)val).parent; failure = "The native slot container '" + (((parent != null) ? ((Object)parent).name : null) ?? "<none>") + "' does not expose a RectTransform component."; return false; } if (!TryCreateSameRuntimeArray(value, requestedCapacity, out object _)) { failure = "Could not allocate a native-compatible slot array for this runtime."; return false; } bool isNewGame = contract.NewGameScreenType.IsInstanceOfType(obj); plan = new DisplayPlan(display, value, count, val, val2, val5, val3, obj, actionMethod, isNewGame); failure = string.Empty; return true; } } } failure = "The vanilla slot array does not expose reusable RectTransform entries."; return false; } private void ExtendDisplay(DisplayPlan plan, ResolvedGameContract contract, int requestedCapacity) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0069: 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) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) if (plan.CurrentCount >= requestedCapacity) { return; } object obj = CreateSameRuntimeArray(plan.Slots, requestedCapacity); for (int i = 0; i < plan.CurrentCount; i++) { SetItem(obj, i, GetItem(plan.Slots, i)); } RectTransform template = plan.Template; Vector2 val = template.anchoredPosition - plan.Previous.anchoredPosition; LayoutRebuilder.ForceRebuildLayoutImmediate(plan.Content); float contentHeight = GetContentHeight(plan.Slots, plan.CurrentCount, plan.Content, template); List<Button> list = new List<Button>(); for (int j = 0; j < plan.CurrentCount; j++) { object? item = GetItem(plan.Slots, j); RectTransform val2 = (RectTransform)((item is RectTransform) ? item : null); if (val2 != null) { Button val3 = FindNamedButton(val2, "Button"); if (val3 != null) { list.Add(val3); } } } for (int k = plan.CurrentCount; k < requestedCapacity; k++) { RectTransform val4 = Object.Instantiate<RectTransform>(template, ((Transform)template).parent, false); ((Object)val4).name = $"SaveSlot_{k + 1}"; val4.anchoredPosition = template.anchoredPosition + val * (float)(k - plan.CurrentCount + 1); Button val5 = FindNamedButton(val4, "Button"); BindSlot(val5, plan.ActionTarget, plan.ActionMethod, k); RebindSlotUtilities(template, val4, k, plan.CurrentCount, contract); ApplyEmptySlotPresentation(val4, k + 1, contract); list.Add(val5); SetItem(obj, k, val4); } contract.SlotsProperty.SetValue(plan.Display, obj); LayoutRebuilder.ForceRebuildLayoutImmediate(plan.Content); float contentHeight2 = GetContentHeight(obj, requestedCapacity, plan.Content, template); ScrollRect scrollRect = EnsureScrollViewport(plan.Content, contentHeight, contentHeight2); _scrollInputs.Add(new ScrollInputState(scrollRect)); RebuildVerticalNavigation(list); } private static void BindSlot(Button button, object actionTarget, MethodInfo actionMethod, int zeroBasedSlotIndex) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown button.onClick = new ButtonClickedEvent(); ((UnityEvent)button.onClick).AddListener((UnityAction)delegate { actionMethod.Invoke(actionTarget, new object[1] { zeroBasedSlotIndex }); }); } public void RenderKnownExtendedSaves(ResolvedGameContract contract) { if (contract.SetDisplayedSaveMethod == null) { return; } object value = contract.SaveGamesProperty.GetValue(null); if (value == null) { return; } foreach (object item3 in FindRuntimeObjects(contract.SaveDisplayType)) { object obj = ToResolvedWrapper(item3, contract.SaveDisplayType); if (obj == null) { continue; } object value2 = contract.SlotsProperty.GetValue(obj); int num = Math.Min(GetCount(value2), GetCount(value)); for (int i = 5; i < num; i++) { object item = GetItem(value, i); if (item == null) { continue; } object? item2 = GetItem(value2, i); RectTransform val = (RectTransform)((item2 is RectTransform) ? item2 : null); if (val != null) { try { contract.SetDisplayedSaveMethod.Invoke(obj, new object[2] { i, item }); ApplyLoadedSlotPresentation(val, contract); } catch (Exception ex) { ModLog.Warn($"Could not render expanded save slot {i + 1}: {ex.Message}"); } } } } } private static void RebindSlotUtilities(RectTransform template, RectTransform clone, int zeroBasedSlotIndex, int originalCount, ResolvedGameContract contract) { RebindUtilitySlotIndex(template, clone, zeroBasedSlotIndex, originalCount, contract.SaveExportButtonType, contract.ExportSlotIndexProperty); RebindUtilitySlotIndex(template, clone, zeroBasedSlotIndex, originalCount, contract.SaveImportButtonType, contract.ImportSlotIndexProperty); } private static void RebindUtilitySlotIndex(RectTransform template, RectTransform clone, int zeroBasedSlotIndex, int originalCount, Type utilityType, PropertyInfo slotIndexProperty) { object obj = FindComponent(template, utilityType); object obj2 = FindComponent(clone, utilityType); if (obj != null && obj2 != null && slotIndexProperty.GetValue(obj) is int num) { slotIndexProperty.SetValue(obj2, num + zeroBasedSlotIndex - (originalCount - 1)); } } private static object? FindComponent(RectTransform root, Type componentType) { Component[] componentsInChildren = ((Component)root).GetComponentsInChildren<Component>(true); foreach (Component rawObject in componentsInChildren) { object obj = ToResolvedWrapper(rawObject, componentType); if (obj != null) { return obj; } } return null; } private static void ApplyEmptySlotPresentation(RectTransform slot, int displayNumber, ResolvedGameContract contract) { Transform val = FindNamedTransform(slot, "Info"); if ((Object)(object)val != (Object)null) { ((Component)val).gameObject.SetActive(false); } object? obj = FindComponent(slot, contract.SaveExportButtonType); Component val2 = (Component)((obj is Component) ? obj : null); if (val2 != null) { val2.gameObject.SetActive(false); } Transform val3 = FindNamedTransform(slot, "Index"); TMP_Text val4 = ((val3 != null) ? ((Component)val3).GetComponentInChildren<TMP_Text>(true) : null); if ((Object)(object)val4 != (Object)null) { val4.text = displayNumber.ToString(); ConfigureIndexText(val3, val4); } } private static void ApplyLoadedSlotPresentation(RectTransform slot, ResolvedGameContract contract) { Transform val = FindNamedTransform(slot, "Info"); if ((Object)(object)val != (Object)null) { ((Component)val).gameObject.SetActive(true); } object? obj = FindComponent(slot, contract.SaveExportButtonType); Component val2 = (Component)((obj is Component) ? obj : null); if (val2 != null) { val2.gameObject.SetActive(true); } Transform val3 = FindNamedTransform(slot, "Index"); TMP_Text val4 = ((val3 != null) ? ((Component)val3).GetComponentInChildren<TMP_Text>(true) : null); if ((Object)(object)val4 != (Object)null) { ConfigureIndexText(val3, val4); } } private static void ConfigureIndexText(Transform? index, TMP_Text text) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) text.textWrappingMode = (TextWrappingModes)0; text.overflowMode = (TextOverflowModes)0; float requiredWidth = Mathf.Ceil(text.GetPreferredValues("20").x) + 8f; EnsureMinimumWidth((RectTransform?)(object)((index is RectTransform) ? index : null), requiredWidth); EnsureMinimumWidth(text.rectTransform, requiredWidth); } private static void EnsureMinimumWidth(RectTransform? rect, float requiredWidth) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)rect == (Object)null)) { Rect rect2 = rect.rect; if (!(((Rect)(ref rect2)).width >= requiredWidth)) { rect.SetSizeWithCurrentAnchors((Axis)0, requiredWidth); } } } private static ScrollRect EnsureScrollViewport(RectTransform content, float viewportHeight, float contentHeight) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) ScrollRect componentInParent = ((Component)content).GetComponentInParent<ScrollRect>(); if ((Object)(object)componentInParent != (Object)null) { return componentInParent; } Transform val = ((Transform)content).parent ?? throw new InvalidOperationException("Save slot content has no parent."); GameObject val2 = new GameObject("SaveExpansionViewport"); val2.AddComponent<RectMask2D>(); val2.AddComponent<ScrollRect>(); RectTransform component = val2.GetComponent<RectTransform>(); ((Transform)component).SetParent(val, false); CopyLayout(content, component); component.pivot = new Vector2(component.pivot.x, 1f); component.sizeDelta = new Vector2(component.sizeDelta.x, viewportHeight); ((Transform)content).SetParent((Transform)(object)component, false); content.anchorMin = new Vector2(0f, 1f); content.anchorMax = new Vector2(1f, 1f); content.pivot = new Vector2(content.pivot.x, 1f); content.sizeDelta = new Vector2(content.sizeDelta.x, contentHeight); content.anchoredPosition = new Vector2(content.anchoredPosition.x, 0f); LayoutRebuilder.ForceRebuildLayoutImmediate(content); ScrollRect component2 = val2.GetComponent<ScrollRect>(); component2.content = content; component2.viewport = component; component2.horizontal = false; component2.vertical = true; component2.movementType = (MovementType)2; component2.scrollSensitivity = 45f; AddScrollbar(component, component2); Canvas.ForceUpdateCanvases(); component2.verticalNormalizedPosition = 1f; return component2; } private static void AddScrollbar(RectTransform viewport, ScrollRect scrollRect) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Expected O, but got Unknown //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("SaveExpansionScrollbar"); val.AddComponent<CanvasRenderer>(); val.AddComponent<Image>(); val.AddComponent<Scrollbar>(); RectTransform component = val.GetComponent<RectTransform>(); ((Transform)component).SetParent((Transform)(object)viewport, false); component.anchorMin = new Vector2(1f, 0f); component.anchorMax = new Vector2(1f, 1f); component.pivot = new Vector2(1f, 0.5f); component.anchoredPosition = new Vector2(-4f, 0f); component.sizeDelta = new Vector2(6f, -12f); Image component2 = val.GetComponent<Image>(); ((Graphic)component2).color = new Color(1f, 1f, 1f, 0.12f); GameObject val2 = new GameObject("Handle"); val2.AddComponent<CanvasRenderer>(); val2.AddComponent<Image>(); RectTransform component3 = val2.GetComponent<RectTransform>(); ((Transform)component3).SetParent((Transform)(object)component, false); component3.anchorMin = new Vector2(0f, 0f); component3.anchorMax = new Vector2(1f, 1f); component3.offsetMin = Vector2.zero; component3.offsetMax = Vector2.zero; Image component4 = val2.GetComponent<Image>(); ((Graphic)component4).color = new Color(1f, 1f, 1f, 0.65f); Scrollbar component5 = val.GetComponent<Scrollbar>(); component5.direction = (Direction)2; ((Selectable)component5).targetGraphic = (Graphic)(object)component4; component5.handleRect = component3; scrollRect.verticalScrollbar = component5; scrollRect.verticalScrollbarVisibility = (ScrollbarVisibility)0; } private static void CopyLayout(RectTransform source, RectTransform target) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0010: 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) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) target.anchorMin = source.anchorMin; target.anchorMax = source.anchorMax; target.pivot = source.pivot; target.anchoredPosition = source.anchoredPosition; target.sizeDelta = source.sizeDelta; ((Transform)target).localScale = ((Transform)source).localScale; } private static RectTransform? ResolveRectTransform(Transform? transform) { if ((Object)(object)transform == (Object)null) { return null; } return (RectTransform?)(((object)((transform is RectTransform) ? transform : null)) ?? ((object)((Component)transform).GetComponent<RectTransform>())); } private static string GetLayoutDescription(RectTransform? slot) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0086: 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_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)slot == (Object)null) { return "slot=<none>"; } Transform parent = ((Transform)slot).parent; RectTransform val = ResolveRectTransform(parent); RectTransform val2 = (((Object)(object)val == (Object)null) ? null : ResolveRectTransform(((Transform)val).parent)); object[] obj = new object[7] { slot.rect, slot.anchoredPosition, ((parent != null) ? ((Object)parent).name : null) ?? "<none>", ((val != null) ? ((object)val.rect/*cast due to .constrained prefix*/).ToString() : null) ?? "<none>", ((val != null) ? ((object)val.sizeDelta/*cast due to .constrained prefix*/).ToString() : null) ?? "<none>", null, null }; object obj2; if (val == null) { obj2 = null; } else { Transform parent2 = ((Transform)val).parent; obj2 = ((parent2 != null) ? ((Object)parent2).name : null); } if (obj2 == null) { obj2 = "<none>"; } obj[5] = obj2; obj[6] = ((val2 != null) ? ((object)val2.rect/*cast due to .constrained prefix*/).ToString() : null) ?? "<none>"; return string.Format("slotRect={0}; slotPos={1}; content={2},rect={3},size={4}; outer={5},rect={6}", obj); } private static void RebuildVerticalNavigation(IReadOnlyList<Button> buttons) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < buttons.Count; i++) { Navigation navigation = ((Selectable)buttons[i]).navigation; ((Navigation)(ref navigation)).mode = (Mode)4; ((Navigation)(ref navigation)).selectOnUp = (Selectable)(object)((i > 0) ? buttons[i - 1] : null); ((Navigation)(ref navigation)).selectOnDown = (Selectable)(object)((i + 1 < buttons.Count) ? buttons[i + 1] : null); ((Selectable)buttons[i]).navigation = navigation; } } private static object? ToResolvedWrapper(object rawObject, Type resolvedType) { if (resolvedType.IsInstanceOfType(rawObject)) { return rawObject; } return null; } private static IEnumerable<object> FindRuntimeObjects(Type type) { return Resources.FindObjectsOfTypeAll(type).Cast<object>(); } private static object? FindOwningScreen(Transform displayTransform, Type screenType) { foreach (object item in FindRuntimeObjects(screenType)) { object? obj = ToResolvedWrapper(item, screenType); Component val = (Component)((obj is Component) ? obj : null); if ((Object)(object)val != (Object)null && displayTransform.IsChildOf(val.transform)) { return val; } } return null; } private static Button? FindNamedButton(RectTransform root, string name) { Transform? obj = FindNamedTransform(root, name); return (obj != null) ? ((Component)obj).GetComponent<Button>() : null; } private static Transform? FindNamedTransform(RectTransform root, string name) { return ((IEnumerable<Transform>)((Component)root).GetComponentsInChildren<Transform>(true)).FirstOrDefault((Func<Transform, bool>)((Transform transform) => string.Equals(((Object)transform).name, name, StringComparison.Ordinal))); } private static float GetContentHeight(object slots, int count, RectTransform content, RectTransform template) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) VerticalLayoutGroup component = ((Component)content).GetComponent<VerticalLayoutGroup>(); Rect rect; if ((Object)(object)component != (Object)null) { RectOffset padding = ((LayoutGroup)component).padding; float num = padding.top + padding.bottom; rect = template.rect; return num + ((Rect)(ref rect)).height * (float)count + ((HorizontalOrVerticalLayoutGroup)component).spacing * (float)(count - 1); } VerticalBounds verticalBounds = GetVerticalBounds(slots, count, content); if (verticalBounds.size > 0.01f) { return verticalBounds.size; } rect = template.rect; return ((Rect)(ref rect)).height * (float)count; } private static VerticalBounds GetVerticalBounds(object slots, int count, RectTransform relativeTo) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) float num = float.PositiveInfinity; float num2 = float.NegativeInfinity; Vector3[] array = (Vector3[])(object)new Vector3[4]; for (int i = 0; i < count; i++) { object? item = GetItem(slots, i); RectTransform val = (RectTransform)((item is RectTransform) ? item : null); if (val != null) { val.GetWorldCorners(array); for (int j = 0; j < array.Length; j++) { Vector3 val2 = ((Transform)relativeTo).InverseTransformPoint(array[j]); num = Mathf.Min(num, val2.y); num2 = Mathf.Max(num2, val2.y); } } } if (float.IsInfinity(num) || float.IsInfinity(num2)) { throw new InvalidOperationException("Could not measure the native save-slot bounds."); } return new VerticalBounds(num, num2); } private static int GetCount(object? source) { if (source is Array { Length: var length }) { return length; } if (!(source is ICollection { Count: var count })) { return (source?.GetType().GetProperty("Length")?.GetValue(source) as int?) ?? (source?.GetType().GetProperty("Count")?.GetValue(source) as int?).GetValueOrDefault(); } return count; } private static object? GetItem(object? source, int index) { if (source is Array array) { return array.GetValue(index); } if (source is IList list) { return list[index]; } return source?.GetType().GetMethod("get_Item", new Type[1] { typeof(int) })?.Invoke(source, new object[1] { index }); } private static bool TryCreateSameRuntimeArray(object source, int length, out object expanded) { if (source is Array array) { expanded = Array.CreateInstance(array.GetType().GetElementType(), length); return true; } ConstructorInfo constructor = source.GetType().GetConstructor(new Type[1] { typeof(int) }); if (constructor == null) { expanded = null; return false; } expanded = constructor.Invoke(new object[1] { length }); return true; } private static object CreateSameRuntimeArray(object source, int length) { if (!TryCreateSameRuntimeArray(source, length, out object expanded)) { throw new InvalidOperationException("Could not create a runtime-compatible slot array."); } return expanded; } private static void SetItem(object target, int index, object? value) { if (target is Array array) { array.SetValue(value, index); return; } target.GetType().GetMethod("set_Item")?.Invoke(target, new object[2] { index, value }); } } } namespace SaveExpansion.Logging { public static class ModLog { public static bool DebugEnabled { get; set; } public static void Info(string message) { MelonLogger.Msg(message); } public static void Warn(string message) { MelonLogger.Warning(message); } public static void Error(string message) { MelonLogger.Error(message); } public static void Debug(string message) { if (DebugEnabled) { MelonLogger.Msg("[DEBUG] " + message); } } } } namespace SaveExpansion.Features { public sealed class SaveExpansionFeature { private enum ActivationState { Discovering, Active, Disabled } private readonly GameContractResolver _resolver = new GameContractResolver(); private readonly VanillaSaveUiExtender _uiExtender = new VanillaSaveUiExtender(); private ActivationState _activationState = ActivationState.Discovering; private int _framesUntilActivation; private int _framesUntilNextProbe; private bool _loggedDiscoveryStart; private bool _loggedContractWait; private bool _loggedDisplayWait; private bool _refreshGuardInstalled; private Harmony? _harmony; private ResolvedGameContract? _activeContract; private int _activeCapacity; private static SaveExpansionFeature? _refreshGuardOwner; public void NotifySceneLoaded(string sceneName) { if (string.Equals(sceneName, "Menu", StringComparison.OrdinalIgnoreCase)) { _activationState = ActivationState.Discovering; _framesUntilActivation = 2; _framesUntilNextProbe = 0; _loggedDisplayWait = false; ModLog.Debug("Scene loaded; scheduling save-menu discovery: " + sceneName); } } public void TickMenuDiscovery() { _uiExtender.TickScrollInput(); if (_activationState == ActivationState.Active || _activationState == ActivationState.Disabled) { return; } if (!_loggedDiscoveryStart) { _loggedDiscoveryStart = true; ModLog.Debug("Save-menu discovery is active; waiting for the native menu contract."); } if (_framesUntilActivation-- > 0 || _framesUntilNextProbe-- > 0) { return; } if (!_resolver.TryResolve(out ResolvedGameContract contract, out string failure) || contract == null) { _framesUntilNextProbe = 60; if (!_loggedContractWait) { _loggedContractWait = true; ModLog.Debug("Save-menu discovery is waiting: " + failure); } return; } int value = contract.SaveSlotCount.GetValue(); object originalSaveGames = null; int slotCapacity = SaveExpansionSettings.Instance.SlotCapacity; try { InstallRefreshGuard(contract, slotCapacity); contract.SaveSlotCount.SetValue(slotCapacity); if (!_uiExtender.TryRefreshNativeSaveInfo(contract, out failure)) { contract.SaveSlotCount.SetValue(value); _activationState = ActivationState.Disabled; ModLog.Warn("Save expansion is disabled for this menu instance: " + failure); return; } if (!_uiExtender.TryExpandSaveGames(contract, slotCapacity, out originalSaveGames, out failure)) { contract.SaveSlotCount.SetValue(value); _activationState = ActivationState.Disabled; ModLog.Warn("Save expansion is disabled for this menu instance: " + failure); return; } ModLog.Debug($"Native LoadManager.SaveGames capacity is {slotCapacity} slots."); if (!_uiExtender.TryExtend(contract, slotCapacity, out failure)) { contract.SaveSlotCount.SetValue(value); contract.SaveGamesProperty.SetValue(null, originalSaveGames); if (failure.StartsWith("The vanilla SaveDisplay was not instantiated", StringComparison.Ordinal)) { _framesUntilNextProbe = 30; if (!_loggedDisplayWait) { _loggedDisplayWait = true; ModLog.Debug("Save-menu contract resolved; waiting for a live SaveDisplay instance."); } } else { _activationState = ActivationState.Disabled; ModLog.Debug(_uiExtender.Describe(contract)); ModLog.Warn("Save expansion is disabled for this menu instance: " + failure); } } else { _activationState = ActivationState.Active; ModLog.Debug($"Native save capacity extended from {value} to {slotCapacity} slots."); ModLog.Debug(_uiExtender.Describe(contract)); } } catch (Exception ex) { contract.SaveSlotCount.SetValue(value); if (originalSaveGames != null) { contract.SaveGamesProperty.SetValue(null, originalSaveGames); } _activationState = ActivationState.Disabled; ModLog.Error("Save expansion was rolled back after a compatibility error: " + ex.Message); } } private void InstallRefreshGuard(ResolvedGameContract contract, int capacity) { //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Expected O, but got Unknown //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Expected O, but got Unknown //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Expected O, but got Unknown //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Expected O, but got Unknown //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown _activeContract = contract; _activeCapacity = capacity; _refreshGuardOwner = this; if (!_refreshGuardInstalled) { if (_harmony == null) { _harmony = new Harmony("SirTidez.SaveExpansion.SaveFlowGuards"); } MethodInfo method = typeof(SaveExpansionFeature).GetMethod("RefreshSaveInfoPostfix", BindingFlags.Static | BindingFlags.NonPublic); MethodInfo method2 = typeof(SaveExpansionFeature).GetMethod("NewGameSlotSelectedPrefix", BindingFlags.Static | BindingFlags.NonPublic); MethodInfo method3 = typeof(SaveExpansionFeature).GetMethod("ConfirmOverwritePrefix", BindingFlags.Static | BindingFlags.NonPublic); MethodInfo method4 = typeof(SaveExpansionFeature).GetMethod("SetupScreenStartGamePrefix", BindingFlags.Static | BindingFlags.NonPublic); MethodInfo method5 = typeof(SaveExpansionFeature).GetMethod("SaveDisplayRefreshPrefix", BindingFlags.Static | BindingFlags.NonPublic); _harmony.Patch((MethodBase)contract.RefreshSaveInfoMethod, (HarmonyMethod)null, new HarmonyMethod(method), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony.Patch((MethodBase)contract.SlotSelectedMethod, new HarmonyMethod(method2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony.Patch((MethodBase)contract.ConfirmOverwriteMethod, new HarmonyMethod(method3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony.Patch((MethodBase)contract.SetupScreenStartGameMethod, new HarmonyMethod(method4), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony.Patch((MethodBase)contract.RefreshDisplayMethod, new HarmonyMethod(method5), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _refreshGuardInstalled = true; ModLog.Debug("Installed expanded save-flow guards: metadata refresh, new-game selection, overwrite, and game start."); } } private static void RefreshSaveInfoPostfix() { SaveExpansionFeature refreshGuardOwner = _refreshGuardOwner; if (refreshGuardOwner?._activeContract != null && refreshGuardOwner._activeCapacity > 0) { if (!refreshGuardOwner._uiExtender.TryExpandSaveGames(refreshGuardOwner._activeContract, refreshGuardOwner._activeCapacity, out object _, out string failure)) { ModLog.Warn("Could not preserve expanded SaveGames after a native refresh: " + failure); return; } refreshGuardOwner._uiExtender.RenderKnownExtendedSaves(refreshGuardOwner._activeContract); ModLog.Debug(refreshGuardOwner._uiExtender.DescribeExpandedSaveMetadata(refreshGuardOwner._activeContract)); } } private static bool NewGameSlotSelectedPrefix(object __instance, int slotIndex) { SaveExpansionFeature refreshGuardOwner = _refreshGuardOwner; if (slotIndex < 5) { return true; } if (refreshGuardOwner?._activeContract == null || slotIndex >= refreshGuardOwner._activeCapacity) { ModLog.Warn($"Blocked native new-game selection for unsupported expanded slot {slotIndex + 1}."); return false; } if (!refreshGuardOwner._uiExtender.TryRouteExpandedNewGameSlot(refreshGuardOwner._activeContract, __instance, slotIndex, out string failure)) { ModLog.Warn($"Blocked native new-game selection for expanded slot {slotIndex + 1}: {failure}"); } return false; } private static bool ConfirmOverwritePrefix() { return EnsureExpandedSaveGames("overwrite confirmation"); } private static bool SetupScreenStartGamePrefix() { return EnsureExpandedSaveGames("new-game start"); } private static bool SaveDisplayRefreshPrefix(object __instance) { SaveExpansionFeature refreshGuardOwner = _refreshGuardOwner; ResolvedGameContract resolvedGameContract = refreshGuardOwner?._activeContract; if (refreshGuardOwner == null || resolvedGameContract == null || refreshGuardOwner._activeCapacity <= 0) { return true; } if (refreshGuardOwner._uiExtender.HasExpandedSlotArray(resolvedGameContract, __instance, refreshGuardOwner._activeCapacity)) { return true; } ModLog.Debug("Deferred vanilla SaveDisplay.Refresh until the recreated menu's expanded slot rows are ready."); return false; } private static bool EnsureExpandedSaveGames(string operation) { SaveExpansionFeature refreshGuardOwner = _refreshGuardOwner; if (refreshGuardOwner?._activeContract == null || refreshGuardOwner._activeCapacity <= 0) { ModLog.Warn("Blocked " + operation + ": the expanded save contract was unavailable."); return false; } if (refreshGuardOwner._uiExtender.TryExpandSaveGames(refreshGuardOwner._activeContract, refreshGuardOwner._activeCapacity, out object _, out string failure)) { return true; } ModLog.Warn("Blocked " + operation + ": " + failure); return false; } } } namespace SaveExpansion.Configuration { public sealed class SaveExpansionSettings { private MelonPreferences_Entry<int>? _slotCapacity; private MelonPreferences_Entry<bool>? _enableDebugLogging; public static SaveExpansionSettings Instance { get; } = new SaveExpansionSettings(); public int SlotCapacity => NormalizeCapacity(_slotCapacity?.Value ?? 10); public bool EnableDebugLogging => _enableDebugLogging?.Value ?? false; private SaveExpansionSettings() { } public void Load() { MelonPreferences_Category val = MelonPreferences.CreateCategory("SaveExpansion", "Save Expansion"); if (_slotCapacity == null) { _slotCapacity = val.CreateEntry<int>("SlotCapacity", 10, "Save-slot capacity", "Supported values are 10, 15, and 20. The game continues to own all SaveGame_N data.", false, false, (ValueValidator)null, (string)null); } if (_enableDebugLogging == null) { _enableDebugLogging = val.CreateEntry<bool>("EnableDebugLogging", false, "Enable diagnostic logging", "When enabled, logs compatibility discovery, save-flow details, and UI-extension diagnostics. Warnings and errors are always logged.", false, false, (ValueValidator)null, (string)null); } MelonPreferences.Load(); MelonPreferences.Save(); } private static int NormalizeCapacity(int requested) { if (requested <= 10) { return 10; } if (requested <= 15) { return 15; } return 20; } } } namespace SaveExpansion.Compatibility { public sealed class GameContractResolver { private const BindingFlags InstanceMembers = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags StaticMembers = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; public bool TryResolve(out ResolvedGameContract? contract, out string failure) { contract = null; Type type = FindType("ScheduleOne.Persistence.SaveManager", "Il2CppScheduleOne.Persistence.SaveManager"); Type type2 = FindType("ScheduleOne.Persistence.LoadManager", "Il2CppScheduleOne.Persistence.LoadManager"); Type type3 = FindType("ScheduleOne.UI.MainMenu.SaveDisplay", "Il2CppScheduleOne.UI.MainMenu.SaveDisplay"); Type type4 = FindType("ScheduleOne.UI.MainMenu.SaveExportButton", "Il2CppScheduleOne.UI.MainMenu.SaveExportButton"); Type type5 = FindType("ScheduleOne.UI.MainMenu.SaveImportButton", "Il2CppScheduleOne.UI.MainMenu.SaveImportButton"); Type type6 = FindType("ScheduleOne.UI.MainMenu.NewGameScreen", "Il2CppScheduleOne.UI.MainMenu.NewGameScreen"); Type type7 = FindType("ScheduleOne.UI.MainMenu.ContinueScreen", "Il2CppScheduleOne.UI.MainMenu.ContinueScreen"); if (type == null || type2 == null || type3 == null || type4 == null || type5 == null || type6 == null || type7 == null) { List<string> list = new List<string>(); if (type == null) { list.Add("SaveManager"); } if (type2 == null) { list.Add("LoadManager"); } if (type3 == null) { list.Add("SaveDisplay"); } if (type4 == null) { list.Add("SaveExportButton"); } if (type5 == null) { list.Add("SaveImportButton"); } if (type6 == null) { list.Add("NewGameScreen"); } if (type7 == null) { list.Add("ContinueScreen"); } failure = "Could not resolve the expected Schedule I save-menu types: " + string.Join(", ", list) + "."; return false; } StaticIntMember staticIntMember = FindStaticIntMember(type, "SAVE_SLOT_COUNT", "SaveSlotCount"); MethodInfo methodInfo = FindNoArgumentMethod(type2, "RefreshSaveInfo"); PropertyInfo propertyInfo = FindStaticReadableWritableProperty(type2, "SaveGames"); MethodInfo methodInfo2 = FindTryLoadSaveInfoMethod(type2); PropertyInfo propertyInfo2 = FindReadableWritableProperty(type3, "Slots"); MethodInfo methodInfo3 = FindNoArgumentMethod(type3, "Refresh"); MethodInfo setDisplayedSaveMethod = FindSetDisplayedSaveMethod(type3); PropertyInfo propertyInfo3 = FindReadableWritableProperty(type4, "SaveSlotIndex"); PropertyInfo propertyInfo4 = FindReadableWritableProperty(type5, "SaveSlotIndex"); MethodInfo methodInfo4 = FindSingleIntMethod(type6, "SlotSelected"); PropertyInfo propertyInfo5 = FindReadableProperty(type6, "SetupScreen"); PropertyInfo propertyInfo6 = FindReadableProperty(type6, "ConfirmOverwriteScreen"); MethodInfo methodInfo5 = ((propertyInfo5 == null) ? null : FindSingleIntMethod(propertyInfo5.PropertyType, "Initialize")); MethodInfo methodInfo6 = ((propertyInfo6 == null) ? null : FindSingleIntMethod(propertyInfo6.PropertyType, "Initialize")); MethodInfo methodInfo7 = ((propertyInfo5 == null) ? null : FindNoArgumentMethod(propertyInfo5.PropertyType, "StartGame")); MethodInfo methodInfo8 = ((propertyInfo5 == null) ? null : FindSingleStringMethod(propertyInfo5.PropertyType, "CopyDefaultSaveToFolder")); InstanceIntMember instanceIntMember = ((propertyInfo5 == null) ? null : FindInstanceIntMember(propertyInfo5.PropertyType, "slotIndex")); MethodInfo methodInfo9 = ((propertyInfo6 == null) ? null : FindNoArgumentMethod(propertyInfo6.PropertyType, "Confirm")); MethodInfo methodInfo10 = FindSingleIntMethod(type7, "LoadGame"); if (staticIntMember == null || methodInfo == null || propertyInfo == null || methodInfo2 == null || propertyInfo2 == null || methodInfo3 == null || propertyInfo3 == null || propertyInfo4 == null || methodInfo4 == null || propertyInfo5 == null || propertyInfo6 == null || methodInfo5 == null || methodInfo6 == null || methodInfo7 == null || methodInfo8 == null || instanceIntMember == null || methodInfo9 == null || methodInfo10 == null) { List<string> list2 = new List<string>(); if (staticIntMember == null) { list2.Add("SaveManager.SAVE_SLOT_COUNT"); } if (methodInfo == null) { list2.Add("LoadManager.RefreshSaveInfo()"); } if (propertyInfo == null) { list2.Add("LoadManager.SaveGames"); } if (methodInfo2 == null) { list2.Add("LoadManager.TryLoadSaveInfo(string, int, out SaveInfo, bool)"); } if (propertyInfo2 == null) { list2.Add("SaveDisplay.Slots"); } if (methodInfo3 == null) { list2.Add("SaveDisplay.Refresh()"); } if (propertyInfo3 == null) { list2.Add("SaveExportButton.SaveSlotIndex"); } if (propertyInfo4 == null) { list2.Add("SaveImportButton.SaveSlotIndex"); } if (methodInfo4 == null) { list2.Add("NewGameScreen.SlotSelected(int)"); } if (propertyInfo5 == null || methodInfo5 == null) { list2.Add("NewGameScreen.SetupScreen.Initialize(int)"); } if (propertyInfo6 == null || methodInfo6 == null) { list2.Add("NewGameScreen.ConfirmOverwriteScreen.Initialize(int)"); } if (methodInfo7 == null) { list2.Add("SetupScreen.StartGame()"); } if (methodInfo8 == null) { list2.Add("SetupScreen.CopyDefaultSaveToFolder(string)"); } if (instanceIntMember == null) { list2.Add("SetupScreen.slotIndex"); } if (methodInfo9 == null) { list2.Add("ConfirmOverwriteScreen.Confirm()"); } if (methodInfo10 == null) { list2.Add("ContinueScreen.LoadGame(int)"); } failure = "The Schedule I save-menu contract changed: " + string.Join(", ", list2) + " was unavailable."; return false; } contract = new ResolvedGameContract(type, staticIntMember, type2, methodInfo, propertyInfo, methodInfo2, type3, propertyInfo2, methodInfo3, setDisplayedSaveMethod, type4, propertyInfo3, type5, propertyInfo4, type6, methodInfo4, propertyInfo5, propertyInfo6, methodInfo5, methodInfo6, methodInfo7, methodInfo8, instanceIntMember, methodInfo9, type7, methodInfo10); failure = string.Empty; return true; } private static Type? FindType(params string[] candidates) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { foreach (Type item in SafeGetTypes(assembly)) { string fullName = item.FullName; if (fullName != null && candidates.Any((string candidate) => string.Equals(fullName, candidate, StringComparison.Ordinal))) { return item; } } } return null; } private static IEnumerable<Type> SafeGetTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where((Type type) => type != null).Cast<Type>(); } catch { return Array.Empty<Type>(); } } private static StaticIntMember? FindStaticIntMember(Type type, params string[] names) { FieldInfo fieldInfo = type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((FieldInfo candidate) => candidate.FieldType == typeof(int) && names.Any((string name) => string.Equals(candidate.Name, name, StringComparison.OrdinalIgnoreCase))); if (fieldInfo != null) { return new StaticIntMember(fieldInfo); } PropertyInfo propertyInfo = type.GetProperties(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(PropertyInfo candidate) { int result; if (candidate.PropertyType == typeof(int) && candidate.CanRead && candidate.CanWrite) { MethodInfo? getMethod = candidate.GetMethod; if ((object)getMethod != null && getMethod.IsStatic) { MethodInfo? setMethod = candidate.SetMethod; if ((object)setMethod != null && setMethod.IsStatic) { result = (names.Any((string name) => string.Equals(candidate.Name, name, StringComparison.OrdinalIgnoreCase)) ? 1 : 0); goto IL_008f; } } } result = 0; goto IL_008f; IL_008f: return (byte)result != 0; }); return (propertyInfo == null) ? null : new StaticIntMember(propertyInfo); } private static PropertyInfo? FindReadableWritableProperty(Type type, string name) { return type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((PropertyInfo property) => property.CanRead && property.CanWrite && string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)); } private static PropertyInfo? FindReadableProperty(Type type, string name) { return type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((PropertyInfo property) => property.CanRead && string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)); } private static PropertyInfo? FindStaticReadableWritableProperty(Type type, string name) { return type.GetProperties(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(PropertyInfo property) { int result; if (property.CanRead && property.CanWrite) { MethodInfo? getMethod = property.GetMethod; if ((object)getMethod != null && getMethod.IsStatic) { MethodInfo? setMethod = property.SetMethod; if ((object)setMethod != null && setMethod.IsStatic) { result = (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase) ? 1 : 0); goto IL_004d; } } } result = 0; goto IL_004d; IL_004d: return (byte)result != 0; }); } private static MethodInfo? FindSingleIntMethod(Type type, string name) { return type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo method) { int result; if (string.Equals(method.Name, name, StringComparison.OrdinalIgnoreCase)) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 1) { result = ((parameters[0].ParameterType == typeof(int)) ? 1 : 0); goto IL_003b; } } result = 0; goto IL_003b; IL_003b: return (byte)result != 0; }); } private static MethodInfo? FindNoArgumentMethod(Type type, string name) { return type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo method) => string.Equals(method.Name, name, StringComparison.OrdinalIgnoreCase) && method.GetParameters().Length == 0); } private static MethodInfo? FindSingleStringMethod(Type type, string name) { return type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo method) { int result; if (string.Equals(method.Name, name, StringComparison.OrdinalIgnoreCase)) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 1) { result = ((parameters[0].ParameterType == typeof(string)) ? 1 : 0); goto IL_003b; } } result = 0; goto IL_003b; IL_003b: return (byte)result != 0; }); } private static MethodInfo? FindSetDisplayedSaveMethod(Type type) { return type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo method) { int result; if (string.Equals(method.Name, "SetDisplayedSave", StringComparison.OrdinalIgnoreCase)) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 2) { result = ((parameters[0].ParameterType == typeof(int)) ? 1 : 0); goto IL_003a; } } result = 0; goto IL_003a; IL_003a: return (byte)result != 0; }); } private static InstanceIntMember? FindInstanceIntMember(Type type, string name) { FieldInfo fieldInfo = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((FieldInfo candidate) => candidate.FieldType == typeof(int) && string.Equals(candidate.Name, name, StringComparison.OrdinalIgnoreCase)); if (fieldInfo != null) { return new InstanceIntMember(fieldInfo); } PropertyInfo propertyInfo = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((PropertyInfo candidate) => candidate.PropertyType == typeof(int) && candidate.CanRead && string.Equals(candidate.Name, name, StringComparison.OrdinalIgnoreCase)); return (propertyInfo == null) ? null : new InstanceIntMember(propertyInfo); } private static MethodInfo? FindTryLoadSaveInfoMethod(Type type) { return type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo method) { int result; if (string.Equals(method.Name, "TryLoadSaveInfo", StringComparison.OrdinalIgnoreCase) && method.ReturnType == typeof(bool)) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 4 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == typeof(int) && parameters[2].IsOut) { result = ((parameters[3].ParameterType == typeof(bool)) ? 1 : 0); goto IL_008d; } } result = 0; goto IL_008d; IL_008d: return (byte)result != 0; }); } } public sealed class InstanceIntMember { private readonly FieldInfo? _field; private readonly PropertyInfo? _property; public string Name => _field?.DeclaringType?.FullName + "." + _field?.Name; public InstanceIntMember(FieldInfo field) { _field = field; } public InstanceIntMember(PropertyInfo property) { _property = property; } public int GetValue(object instance) { return (int)(_field?.GetValue(instance) ?? _property?.GetValue(instance) ?? throw new InvalidOperationException("Could not read " + Name + ".")); } } public sealed class ResolvedGameContract { public Type SaveManagerType { get; } public StaticIntMember SaveSlotCount { get; } public Type LoadManagerType { get; } public MethodInfo RefreshSaveInfoMethod { get; } public PropertyInfo SaveGamesProperty { get; } public MethodInfo TryLoadSaveInfoMethod { get; } public Type SaveDisplayType { get; } public PropertyInfo SlotsProperty { get; } public MethodInfo RefreshDisplayMethod { get; } public MethodInfo? SetDisplayedSaveMethod { get; } public Type SaveExportButtonType { get; } public PropertyInfo ExportSlotIndexProperty { get; } public Type SaveImportButtonType { get; } public PropertyInfo ImportSlotIndexProperty { get; } public Type NewGameScreenType { get; } public MethodInfo SlotSelectedMethod { get; } public PropertyInfo SetupScreenProperty { get; } public PropertyInfo ConfirmOverwriteScreenProperty { get; } public MethodInfo SetupScreenInitializeMethod { get; } public MethodInfo ConfirmOverwriteInitializeMethod { get; } public MethodInfo SetupScreenStartGameMethod { get; } public MethodInfo SetupScreenCopyDefaultSaveMethod { get; } public InstanceIntMember SetupScreenSlotIndex { get; } public MethodInfo ConfirmOverwriteMethod { get; } public Type ContinueScreenType { get; } public MethodInfo LoadGameMethod { get; } public ResolvedGameContract(Type saveManagerType, StaticIntMember saveSlotCount, Type loadManagerType, MethodInfo refreshSaveInfoMethod, PropertyInfo saveGamesProperty, MethodInfo tryLoadSaveInfoMethod, Type saveDisplayType, PropertyInfo slotsProperty, MethodInfo refreshDisplayMethod, MethodInfo? setDisplayedSaveMethod, Type saveExportButtonType, PropertyInfo exportSlotIndexProperty, Type saveImportButtonType, PropertyInfo importSlotIndexProperty, Type newGameScreenType, MethodInfo slotSelectedMethod, PropertyInfo setupScreenProperty, PropertyInfo confirmOverwriteScreenProperty, MethodInfo setupScreenInitializeMethod, MethodInfo confirmOverwriteInitializeMethod, MethodInfo setupScreenStartGameMethod, MethodInfo setupScreenCopyDefaultSaveMethod, InstanceIntMember setupScreenSlotIndex, MethodInfo confirmOverwriteMethod, Type continueScreenType, MethodInfo loadGameMethod) { SaveManagerType = saveManagerType; SaveSlotCount = saveSlotCount; LoadManagerType = loadManagerType; RefreshSaveInfoMethod = refreshSaveInfoMethod; SaveGamesProperty = saveGamesProperty; TryLoadSaveInfoMethod = tryLoadSaveInfoMethod; SaveDisplayType = saveDisplayType; SlotsProperty = slotsProperty; RefreshDisplayMethod = refreshDisplayMethod; SetDisplayedSaveMethod = setDisplayedSaveMethod; SaveExportButtonType = saveExportButtonType; ExportSlotIndexProperty = exportSlotIndexProperty; SaveImportButtonType = saveImportButtonType; ImportSlotIndexProperty = importSlotIndexProperty; NewGameScreenType = newGameScreenType; SlotSelectedMethod = slotSelectedMethod; SetupScreenProperty = setupScreenProperty; ConfirmOverwriteScreenProperty = confirmOverwriteScreenProperty; SetupScreenInitializeMethod = setupScreenInitializeMethod; ConfirmOverwriteInitializeMethod = confirmOverwriteInitializeMethod; SetupScreenStartGameMethod = setupScreenStartGameMethod; SetupScreenCopyDefaultSaveMethod = setupScreenCopyDefaultSaveMethod; SetupScreenSlotIndex = setupScreenSlotIndex; ConfirmOverwriteMethod = confirmOverwriteMethod; ContinueScreenType = continueScreenType; LoadGameMethod = loadGameMethod; } } public sealed class StaticIntMember { private readonly FieldInfo? _field; private readonly PropertyInfo? _property; public string Name => _field?.DeclaringType?.FullName + "." + _field?.Name; public StaticIntMember(FieldInfo field) { _field = field; } public StaticIntMember(PropertyInfo property) { _property = property; } public int GetValue() { return (int)(_field?.GetValue(null) ?? _property?.GetValue(null) ?? throw new InvalidOperationException("Could not read " + Name + ".")); } public void SetValue(int value) { if (_field != null) { _field.SetValue(null, value); } else { _property?.SetValue(null, value); } } } }