using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using 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 _scrollInputs = new List(); 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="; } List list = new List(); 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 GetSaveFolderCandidates(object saveGames, string? createdSaveFolderPath, int zeroBasedSlotIndex) { HashSet hashSet = new HashSet(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 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 enumerable = FindRuntimeObjects(contract.SaveDisplayType); List list = new List(); foreach (object item3 in enumerable) { object obj = ToResolvedWrapper(item3, contract.SaveDisplayType); if (obj == null) { list.Add("display="); continue; } object value = contract.SlotsProperty.GetValue(obj); List list2 = new List(); 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}:"); continue; } string arg = string.Join(",", from transform in ((Component)val).GetComponentsInChildren(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() : 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 enumerable = FindRuntimeObjects(contract.SaveDisplayType); if (!enumerable.Any()) { failure = "The vanilla SaveDisplay was not instantiated yet."; return false; } List list = new List(); 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) ?? "") + "' 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