using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; using HarmonyLib; using Hash.Api; using Il2CppFishNet; using Il2CppFishNet.Managing; using Il2CppFishNet.Managing.Object; using Il2CppFishNet.Object; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppScheduleOne; using Il2CppScheduleOne.AvatarFramework; using Il2CppScheduleOne.Building.Doors; using Il2CppScheduleOne.Economy; using Il2CppScheduleOne.Employees; using Il2CppScheduleOne.GameTime; using Il2CppScheduleOne.Instancing; using Il2CppScheduleOne.Management; using Il2CppScheduleOne.Map; using Il2CppScheduleOne.NPCs; using Il2CppScheduleOne.NPCs.Behaviour; using Il2CppScheduleOne.NPCs.Framework; using Il2CppScheduleOne.NPCs.Schedules; using Il2CppScheduleOne.Police; using Il2CppScheduleOne.Product; using Il2CppScheduleOne.Property; using Il2CppScheduleOne.Tools; using Il2CppScheduleOne.UI; using Il2CppScheduleOne.UI.Handover; using Il2CppScheduleOne.Weather; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using Il2CppSystem.Reflection; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using Polyfill.Boot; using Polyfill.Contract; using Polyfill.Core; using Polyfill.ModFixes; using Polyfill.Report; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Core), "Polyfill", "0.11.16", "DooDesch", "https://github.com/DooDesch-Mods/ScheduleOne-Polyfill")] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("DooDesch")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © DooDesch")] [assembly: AssemblyFileVersion("0.11.16.0")] [assembly: AssemblyInformationalVersion("0.11.16+a991f1874ad4639ca4259d22a7773c7b8489caa5")] [assembly: AssemblyProduct("Polyfill")] [assembly: AssemblyTitle("Polyfill")] [assembly: AssemblyVersion("0.11.16.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 DooDesch { internal static class ModVersion { internal const string Current = "0.11.16"; } } namespace Hash.Api { public static class HashCommands { private const string BridgeTypeName = "Hash.Bridge.HashBridge, Hash"; private static readonly List _pending = new List(); private static Action _declare; private static bool _bound; public static bool Available { get { Bind(); return _bound; } } private static string Owner { get { try { return Assembly.GetExecutingAssembly().GetName().Name ?? ""; } catch { return ""; } } } public static void Add(string word, string description, string example = null) { if (!string.IsNullOrEmpty(word)) { Bind(); if (_declare != null) { Safely(word, description, example); return; } _pending.Add(new string[3] { word, description ?? "", example ?? "" }); } } private static void Safely(string word, string description, string example) { try { _declare(word, description ?? "", example ?? "", Owner); } catch { } } private static void Bind() { if (_bound) { return; } try { Type type = Type.GetType("Hash.Bridge.HashBridge, Hash", throwOnError: false); if (type == null) { return; } _declare = type.GetField("Declare", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) as Action; if (_declare == null) { return; } _bound = true; foreach (string[] item in _pending) { Safely(item[0], item[1], item[2]); } _pending.Clear(); } catch { } } } } namespace Polyfill.Boot { internal sealed class MelonConsentStore : IConsentStore { private const string Category = "Polyfill"; internal static void Install() { Consent.Use(new MelonConsentStore()); } public bool TryReadBool(string key, out bool value) { return Read(key, fallback: false, out value); } public bool TryReadInt(string key, out int value) { return Read(key, 0, out value); } private static bool Read(string key, T fallback, out T value) { value = fallback; try { MelonPreferences_Category val = MelonPreferences.GetCategory("Polyfill") ?? MelonPreferences.CreateCategory("Polyfill"); if (val == null) { return false; } MelonPreferences_Entry val2 = val.GetEntry(key) ?? val.CreateEntry(key, fallback, key, Describe(key), false, false, (ValueValidator)null, (string)null); value = val2.Value; return true; } catch { return false; } } private static string Describe(string key) { return key switch { "ShareFindings" => "Send anonymous findings - which mod, which symbol, repaired or not. Never your name, your paths or your save.", "ShareFindingsAnswered" => "Whether the question has been answered. Clear this to be asked again.", "ShareFindingsAsked" => "How many launches have asked. After three, the question stops.", _ => key, }; } public void Write(string key, T value, string description) { try { MelonPreferences_Category val = MelonPreferences.GetCategory("Polyfill") ?? MelonPreferences.CreateCategory("Polyfill"); if (val != null) { MelonPreferences_Entry entry = val.GetEntry(key); if (entry == null) { val.CreateEntry(key, value, key, description, false, false, (ValueValidator)null, (string)null); } else { entry.Value = value; } } } catch { } } public void Flush() { try { MelonPreferences.Save(); } catch { } } } } namespace Polyfill.Core { internal sealed class GeneratorIdentity { internal string GameAssemblyHash; internal string UnityVersion; internal string DumperVersion; internal string DumperScrsVersion; internal string Loader; internal bool IsKnown => !string.IsNullOrEmpty(GameAssemblyHash); internal string Digest() { string text = GameAssemblyHash ?? ""; if (text.Length > 16) { text = text.Substring(0, 16); } return $"{text}/{UnityVersion}/{DumperVersion}/{DumperScrsVersion}/{Loader}"; } internal static GeneratorIdentity Read() { GeneratorIdentity generatorIdentity = new GeneratorIdentity { Loader = MelonLoaderVersion() }; string text = ConfigPath(); if (text == null || !File.Exists(text)) { return generatorIdentity; } try { string[] array = File.ReadAllLines(text); foreach (string text2 in array) { int num = text2.IndexOf('='); if (num > 0) { string text3 = text2.Substring(0, num).Trim(); string text4 = text2.Substring(num + 1).Trim().Trim('"'); switch (text3) { case "GameAssemblyHash": generatorIdentity.GameAssemblyHash = text4; break; case "UnityVersion": generatorIdentity.UnityVersion = text4; break; case "DumperVersion": generatorIdentity.DumperVersion = text4; break; case "DumperSCRSVersion": generatorIdentity.DumperScrsVersion = text4; break; } } } } catch { } return generatorIdentity; } internal static string ConfigPath() { try { string melonLoaderDirectory = MelonEnvironment.MelonLoaderDirectory; if (string.IsNullOrEmpty(melonLoaderDirectory)) { return null; } return Path.Combine(melonLoaderDirectory, "Dependencies", "Il2CppAssemblyGenerator", "Config.cfg"); } catch { return null; } } private static string MelonLoaderVersion() { try { return typeof(MelonPlugin).Assembly.GetName().Version?.ToString() ?? "?"; } catch { return "?"; } } } } namespace Polyfill.Contract { internal static class CommandTable { internal sealed class Command { internal string Name; internal string Help; internal string Example; } internal static readonly Command[] All = new Command[12] { C("polyfill", "what Polyfill found in your mods at startup", "polyfill"), C("polyfilllist", "every mod, with its verdict", "polyfilllist"), C("polyfillshow", "everything one mod asks for that is missing", "polyfillshow hitman"), C("polyfillunfixed", "only what cannot be pointed at anything", "polyfillunfixed hitman"), C("polyfillexport", "write one file with everything, ready to send", "polyfillexport"), C("polyfillprobe", "can the runtime resolve this type, and does Type::Member really run?", "polyfillprobe Il2CppScheduleOne.UI.InputPromptsCanvas::LoadModule objectselector"), C("polyfillprefab", "does the game still have this prefab, and what is near it", "polyfillprefab Basic Metal Glass Door"), C("polyfillfixes", "the per-mod fixes, and switch one off", "polyfillfixes off s1mapi-prefabs"), C("polyfillrestore", "undo every repair, restart to take effect", "polyfillrestore"), C("polyfillregen", "have MelonLoader rebuild the game's generated assemblies", "polyfillregen"), C("polyfillshare", "share anonymous findings, or see what would be sent", "polyfillshare show"), C("polyfillhelp", "list the polyfill commands", "polyfillhelp") }; internal static bool Owns(string command) { if (string.IsNullOrEmpty(command)) { return false; } Command[] all = All; for (int i = 0; i < all.Length; i++) { if (string.Equals(all[i].Name, command, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static Command C(string name, string help, string example) { return new Command { Name = name, Help = help, Example = example }; } } internal interface IConsentStore { bool TryReadBool(string key, out bool value); bool TryReadInt(string key, out int value); void Write(string key, T value, string description); void Flush(); } internal static class Consent { internal sealed class State { internal bool Sharing; internal bool Answered; internal int Asked; } internal const string SharingKey = "ShareFindings"; internal const string AnsweredKey = "ShareFindingsAnswered"; internal const string AskedKey = "ShareFindingsAsked"; private static IConsentStore _store; internal static bool Sharing => Read().Sharing; internal static void Use(IConsentStore store) { _store = store; } internal static State Read() { State state = new State(); IConsentStore store = _store; if (store == null) { return state; } try { if (store.TryReadBool("ShareFindings", out var value)) { state.Sharing = value; } if (store.TryReadBool("ShareFindingsAnswered", out var value2)) { state.Answered = value2; } if (store.TryReadInt("ShareFindingsAsked", out var value3)) { state.Asked = value3; } } catch { } return state; } internal static void Write(bool sharing, bool answered) { IConsentStore store = _store; if (store == null) { return; } try { store.Write("ShareFindings", sharing, "Send anonymous findings - which mod, which symbol, repaired or not. Never your name, your paths or your save."); store.Write("ShareFindingsAnswered", answered, "Whether the question has been answered. Clear this to be asked again."); store.Flush(); } catch { } } internal static void CountOneAsk(State state) { IConsentStore store = _store; if (store == null) { return; } try { store.Write("ShareFindingsAsked", state.Asked + 1, "How many launches have asked. After three, the question stops."); store.Flush(); } catch { } } } internal static class CoveredElsewhere { internal sealed class Entry { internal string Type; internal string Member; internal string FixId; internal string Because; } internal static readonly Entry[] All = new Entry[3] { new Entry { Type = "Il2CppScheduleOne.UI.AmountSelector", Member = "get_onPriceChanged", FixId = "amount-changed-after-override", Because = "the UnityEvent this returned cannot be handed back - interop wrappers are pooled by weak reference, so it could not be the same object twice - but the change it announced is raised again when a mod replaces the setter that used to raise it" }, new Entry { Type = "Il2CppScheduleOne.UI.Handover.HandoverScreen", Member = "get_OriginalItemLocations", FixId = "otc-smart-fill-tracking", Because = "the dictionary and the nested enum it was keyed by are both gone, and nothing of that shape can be handed back - but it was write-only bookkeeping even in 0.4.5f2, so the fix drops the call instead of answering it" }, new Entry { Type = "Il2CppScheduleOne.UI.Handover.HandoverScreen/EItemSource", Member = null, FixId = "otc-smart-fill-tracking", Because = "the enum only ever typed HandoverScreen.OriginalItemLocations, and 0.4.6 deleted both - a copy would have its own identity and could not satisfy the signature. The one method that names it is OverTheCounter's TrackItemAsPlayer, and the fix takes the call to it out, so nothing reaches the name" } }; internal static Entry For(string type, string member) { if (type == null || member == null) { return null; } Entry[] all = All; foreach (Entry entry in all) { if (string.Equals(entry.Type, type, StringComparison.Ordinal) && string.Equals(entry.Member, member, StringComparison.Ordinal)) { return entry; } } return null; } internal static Entry ForType(string type) { if (type == null) { return null; } Entry[] all = All; foreach (Entry entry in all) { if (entry.Member == null && string.Equals(entry.Type, type, StringComparison.Ordinal)) { return entry; } } return null; } } internal readonly struct GameVersion : IComparable, IEquatable { internal readonly int[] Parts; internal readonly string Raw; internal static readonly GameVersion Unknown = new GameVersion(null, ""); internal bool IsKnown { get { if (Parts != null) { return Parts.Length != 0; } return false; } } private GameVersion(int[] parts, string raw) { Parts = parts; Raw = raw; } internal static bool TryParse(string text, out GameVersion version) { version = Unknown; if (string.IsNullOrEmpty(text)) { return false; } List list = new List(4); long num = 0L; bool flag = false; foreach (char c in text) { if (c >= '0' && c <= '9') { if (num < 214748364) { num = num * 10 + (c - 48); } flag = true; } else if (flag) { list.Add((int)num); num = 0L; flag = false; } } if (flag) { list.Add((int)num); } if (list.Count == 0) { return false; } version = new GameVersion(list.ToArray(), text); return true; } internal static GameVersion Parse(string text) { if (!TryParse(text, out var version)) { return Unknown; } return version; } public int CompareTo(GameVersion other) { int[] array = Parts ?? Array.Empty(); int[] array2 = other.Parts ?? Array.Empty(); int num = Math.Min(array.Length, array2.Length); for (int i = 0; i < num; i++) { if (array[i] != array2[i]) { if (array[i] >= array2[i]) { return 1; } return -1; } } return array.Length.CompareTo(array2.Length); } internal bool StartsWith(GameVersion prefix, int count) { if (Parts == null || prefix.Parts == null) { return false; } if (count > prefix.Parts.Length || count > Parts.Length) { return false; } for (int i = 0; i < count; i++) { if (Parts[i] != prefix.Parts[i]) { return false; } } return true; } public bool Equals(GameVersion other) { if (CompareTo(other) == 0) { return IsKnown == other.IsKnown; } return false; } public override bool Equals(object obj) { if (obj is GameVersion other) { return Equals(other); } return false; } public override int GetHashCode() { int num = 17; if (Parts != null) { int[] parts = Parts; foreach (int num2 in parts) { num = num * 31 + num2; } } return num; } public override string ToString() { if (!IsKnown) { return "unknown"; } return Raw; } public static bool operator <(GameVersion a, GameVersion b) { return a.CompareTo(b) < 0; } public static bool operator >(GameVersion a, GameVersion b) { return a.CompareTo(b) > 0; } public static bool operator <=(GameVersion a, GameVersion b) { return a.CompareTo(b) <= 0; } public static bool operator >=(GameVersion a, GameVersion b) { return a.CompareTo(b) >= 0; } public static bool operator ==(GameVersion a, GameVersion b) { return a.Equals(b); } public static bool operator !=(GameVersion a, GameVersion b) { return !a.Equals(b); } } internal static class GameVersionSource { private static string _raw; internal static string Raw => _raw ?? (_raw = Read()); internal static GameVersion Current => GameVersion.Parse(Raw); private static string Read() { string[] array = new string[2] { "MelonLoader.InternalUtils.UnityInformationHandler, MelonLoader", "MelonLoader.MelonUtils, MelonLoader" }; foreach (string typeName in array) { try { if ((Type.GetType(typeName, throwOnError: false)?.GetProperty("GameVersion", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))?.GetValue(null) is string { Length: >0 } text) { return text; } } catch { } } return "unknown"; } internal static string Disagreement(string other) { if (string.IsNullOrEmpty(other)) { return null; } if (GameVersion.Parse(other) == Current) { return null; } return $"MelonLoader says the game is '{Raw}' and Unity says '{other}'. Polyfill went with " + "MelonLoader's, which is the one every version decision was made against."; } } internal static class SplitScreens { internal sealed class Entry { internal string Type; internal string Station; internal string StationName; internal bool HasRemoveUi; } private const string Stations = "Il2CppScheduleOne.UI.Stations."; private const string Objects = "Il2CppScheduleOne.ObjectScripts."; internal static readonly Entry[] All = new Entry[5] { new Entry { Type = "Il2CppScheduleOne.UI.Stations.PackagingStationCanvas", Station = "Il2CppScheduleOne.ObjectScripts.PackagingStation", StationName = "station", HasRemoveUi = true }, new Entry { Type = "Il2CppScheduleOne.UI.Stations.BrickPressCanvas", Station = "Il2CppScheduleOne.ObjectScripts.BrickPress", StationName = "press", HasRemoveUi = true }, new Entry { Type = "Il2CppScheduleOne.UI.Stations.LabOvenCanvas", Station = "Il2CppScheduleOne.ObjectScripts.LabOven", StationName = "oven", HasRemoveUi = true }, new Entry { Type = "Il2CppScheduleOne.UI.Stations.CauldronCanvas", Station = "Il2CppScheduleOne.ObjectScripts.Cauldron", StationName = "cauldron", HasRemoveUi = true }, new Entry { Type = "Il2CppScheduleOne.UI.Stations.DryingRackCanvas", Station = "Il2CppScheduleOne.ObjectScripts.DryingRack", StationName = "rack", HasRemoveUi = false } }; } internal static class GrownOverloads { internal sealed class Entry { internal string Type; internal string Name; internal string[] OldParameters; internal string Because; } internal static readonly Entry[] All = new Entry[4] { new Entry { Type = "Il2CppScheduleOne.UI.StorageMenu", Name = "Open", OldParameters = new string[3] { "System.String", "System.String", "Il2CppScheduleOne.ItemFramework.IItemSlotOwner" }, Because = "0.4.6 gave every StorageMenu.Open a closing callback" }, new Entry { Type = "Il2CppScheduleOne.UI.StorageMenu", Name = "Open", OldParameters = new string[3] { "Il2CppScheduleOne.ItemFramework.IItemSlotOwner", "System.String", "System.String" }, Because = "0.4.6 gave every StorageMenu.Open a closing callback" }, new Entry { Type = "Il2CppScheduleOne.UI.StorageMenu", Name = "Open", OldParameters = new string[1] { "Il2CppScheduleOne.Storage.StorageEntity" }, Because = "the third Open took the same trailing callback as its two siblings and was missed when they were listed (0.4.5f2 Open_Public_Virtual_New_Void_StorageEntity_0 against 0.4.6f13 ..._StorageEntity_Action_0)" }, new Entry { Type = "Il2CppScheduleOne.Economy.CustomerData", Name = "GetOrderDays", OldParameters = new string[2] { "System.Single", "System.Single" }, Because = "GetOrderDays stopped returning the list and started filling one it is handed" } }; internal static bool Doubled(string type, string name) { Entry[] all = All; foreach (Entry entry in all) { if (entry.Name == name && string.Equals(entry.Type, type, StringComparison.Ordinal)) { return true; } } return false; } internal static bool IsStandIn(string type, string name, int parameterCount) { Entry[] all = All; foreach (Entry entry in all) { if (entry.Name == name && entry.OldParameters.Length == parameterCount && string.Equals(entry.Type, type, StringComparison.Ordinal)) { return true; } } return false; } } internal static class Headless { private static bool _asked; private static bool _answer; private static string _why; internal static bool Yes(out string why) { if (!_asked) { _asked = true; _answer = Look(out _why); } why = _why; return _answer; } internal static bool Yes() { string why; return Yes(out why); } private static bool Look(out string why) { why = null; try { string[] commandLineArgs = Environment.GetCommandLineArgs(); for (int i = 0; i < commandLineArgs.Length; i++) { switch (commandLineArgs[i].TrimStart('-').ToLowerInvariant()) { case "batchmode": why = "the game is running in batch mode"; return true; case "nographics": why = "the game is running without graphics"; return true; case "dedicated-server": case "dedicatedserver": why = "this is a dedicated server"; return true; } } } catch (Exception) { } return false; } } internal interface ILog { void Msg(string message); void Warning(string message); void Error(string message); } internal static class NarrowedOverloads { internal sealed class Entry { internal string Type; internal string Name; internal string[] RealParameters; internal string ParameterName; internal string Because; } internal static readonly Entry[] All = new Entry[1] { new Entry { Type = "Il2CppScheduleOne.UI.Phone.CounterofferInterface", Name = "ChangeQuantity", RealParameters = new string[1] { "System.Single" }, ParameterName = "change", Because = "the quantity step took an int until 0.4.5f2 and takes a float now, and a second ChangeQuantity(string) arrived beside it for the text box (CounterofferInterface.cs:191-207)" } }; } internal static class PolyfillPaths { internal const string BackupSuffix = ".polyfill-orig"; internal const string TempSuffix = ".polyfill-tmp"; internal const string FolderName = "Polyfill"; internal const string LastRunFile = "last-run.txt"; internal const string ReportFile = "polyfill-report.txt"; internal const string StampFileName = "interop.stamp"; internal const string RestorePendingFile = "restore-pending"; internal static string Folder(string userDataDirectory) { return Path.Combine(userDataDirectory ?? ".", "Polyfill"); } internal static string LastRun(string userDataDirectory) { return Path.Combine(Folder(userDataDirectory), "last-run.txt"); } internal static string Report(string userDataDirectory) { return Path.Combine(Folder(userDataDirectory), "polyfill-report.txt"); } internal static string Stamp(string userDataDirectory) { return Path.Combine(Folder(userDataDirectory), "interop.stamp"); } internal static string RestorePending(string userDataDirectory) { return Path.Combine(Folder(userDataDirectory), "restore-pending"); } internal static string Backup(string assemblyPath) { return assemblyPath + ".polyfill-orig"; } } internal static class RenamedMethods { internal sealed class Entry { internal string Type; internal string OldName; internal string NewName; internal string Because; } internal static readonly Entry[] All = new Entry[2] { new Entry { Type = "Il2CppScheduleOne.ObjectScripts.PackagingStation", OldName = "Open", NewName = "Use", Because = "0.4.5f2 PackagingStation.Open() set the camera up and opened the canvas; 0.4.6f13 Use() pushes a state and opens the canvas, on the same type (PackagingStation.cs:405)" }, new Entry { Type = "Il2CppScheduleOne.UI.Handover.HandoverScreenPriceSelector", OldName = "SetPrice", NewName = "SetAmount", Because = "the price control became the game's general amount box in 0.4.6, and SetPrice(float) became SetAmount(float) on it (AmountSelector.cs:61)" } }; internal static string Successor(string type, string oldName) { if (type == null || oldName == null) { return null; } Entry[] all = All; foreach (Entry entry in all) { if (string.Equals(entry.Type, type, StringComparison.Ordinal) && string.Equals(entry.OldName, oldName, StringComparison.Ordinal)) { return entry.NewName; } } return null; } internal static string Because(string type, string oldName) { if (type == null || oldName == null) { return null; } Entry[] all = All; foreach (Entry entry in all) { if (string.Equals(entry.Type, type, StringComparison.Ordinal) && string.Equals(entry.OldName, oldName, StringComparison.Ordinal)) { return entry.Because; } } return null; } } internal static class RenamedParameters { internal sealed class Entry { internal string Type; internal string Method; internal int ParameterCount; internal int Index; internal string OldName; internal string NewName; internal string Because; } internal static readonly Entry[] All = new Entry[2] { new Entry { Type = "Il2CppScheduleOne.UI.Shop.ShopInterface", Method = "SetIsOpen", ParameterCount = 1, Index = 0, OldName = "isOpen", NewName = "open", Because = "the flag kept its type and its meaning and lost its name in 0.4.6" }, new Entry { Type = "Il2CppScheduleOne.UI.AmountSelector", Method = "SetAmount", ParameterCount = 1, Index = 0, OldName = "price", NewName = "amount", Because = "SetPrice(float price) became SetAmount(float amount) when the handover price control turned into the game's general amount box (AmountSelector.cs:61)" } }; internal static IEnumerable For(string type, string method, int parameterCount) { Entry[] all = All; foreach (Entry entry in all) { if (entry.Method == method && entry.ParameterCount == parameterCount && string.Equals(entry.Type, type, StringComparison.Ordinal)) { yield return entry; } } } } internal static class RenamedTypes { internal static readonly string[] StandIns = new string[10] { "Il2CppScheduleOne.UI.Stations.MixingStationCanvas", "Il2CppScheduleOne.UI.Stations.ChemistryStationCanvas", "Il2CppScheduleOne.UI.Stations.CauldronCanvas", "Il2CppScheduleOne.UI.Stations.DryingRackCanvas", "Il2CppScheduleOne.UI.Handover.HandoverScreenPriceSelector", "Il2CppScheduleOne.Weather.WeatherConditions", "Il2CppScheduleOne.DevUtilities.ExitAction", "Il2CppScheduleOne.UI.ATM.ATMInterface", "Il2CppScheduleOne.UI.Stations.Drying_rack.DryingOperationUI", "Il2CppScheduleOne.UI.MainMenu.MainMenuScreen" }; internal static bool IsStandIn(string fullName) { if (fullName == null) { return false; } string[] standIns = StandIns; for (int i = 0; i < standIns.Length; i++) { if (string.Equals(standIns[i], fullName, StringComparison.Ordinal)) { return true; } } return false; } } internal static class ReplacedMethods { internal sealed class Replacement { internal string Name; internal string[] Parameters = new string[0]; internal object[] Arguments = new object[0]; } internal sealed class Entry { internal string Type; internal string OldName; internal string[] Parameters; internal string[] ParameterNames; internal Replacement[] Replacements; internal string Because; } private const string Bool = "System.Boolean"; private const string Management = "Il2CppScheduleOne.UI.Management."; internal static readonly Entry[] All = new Entry[2] { Selector("ObjectSelector"), Selector("TransitEntitySelector") }; private static Entry Selector(string type) { Entry entry = new Entry(); entry.Type = "Il2CppScheduleOne.UI.Management." + type; entry.OldName = "Close"; entry.Parameters = new string[2] { "System.Boolean", "System.Boolean" }; entry.ParameterNames = new string[2] { "returnToClipboard", "pushChanges" }; entry.Replacements = new Replacement[2] { new Replacement { Name = "CloseAndSubmit", Arguments = new object[2] { true, true } }, new Replacement { Name = "CloseAndCancel", Arguments = new object[2] { true, false } } }; entry.Because = "0.4.5f2 " + type + ".Close(bool returnToClipboard, bool pushChanges); 0.4.6f13 has CloseAndSubmit and CloseAndCancel over a shared OnClose that always returns to the clipboard (ObjectSelector.cs:120-143)"; return entry; } } internal static class Outcome { internal const string Applied = "applied"; internal const string Refused = "refused"; internal const string StoodDown = "stood-down"; internal const string None = "none"; } internal sealed class Finding { internal string Kind; internal bool Note; internal string Scope; internal string Symbol; internal string Reason; internal string Hint; internal string Site; internal string Outcome = "none"; internal string OutcomeDetail; internal bool Covered; internal string RepairKey; internal bool Fixable => !string.IsNullOrEmpty(Hint); } internal sealed class ModReport { internal string Path; internal string AssemblyName; internal string Name; internal string Version; internal string Author; internal int TypeRefs; internal int MemberRefs; internal int HarmonyTargetsChecked; internal readonly List Findings = new List(); internal readonly List Namespaces = new List(); internal string Display { get { if (string.IsNullOrEmpty(Name)) { if (string.IsNullOrEmpty(AssemblyName)) { return System.IO.Path.GetFileName(Path ?? ""); } return AssemblyName; } return Name; } } internal string Verdict { get { bool flag = false; foreach (Finding finding in Findings) { if (!finding.Note) { flag = true; if (!(finding.Outcome == "applied") && !finding.Covered) { return "blocked"; } } } if (!flag) { return "clean"; } return "adaptable"; } } } internal sealed class RunReport { internal const int Format = 2; internal const string HeaderPrefix = "# polyfill-report "; private const char CarriageReturn = '\r'; internal string Generated = ""; internal string Game = "?"; internal string Interop = ""; internal int AssemblyCount; internal readonly List Mods = new List(); internal readonly List Dropped = new List(); internal string Problem; internal string Text() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# polyfill-report " + 2); stringBuilder.AppendLine("# generated=" + Escape(Generated)); stringBuilder.AppendLine("# game=" + Escape(Game)); stringBuilder.AppendLine("# interop=" + Escape(Interop)); stringBuilder.AppendLine("# assemblies=" + AssemblyCount); stringBuilder.AppendLine("# mods=" + Mods.Count); foreach (ModReport mod in Mods) { stringBuilder.AppendLine(string.Join("|", "M", Escape(mod.Path), Escape(mod.AssemblyName), Escape(mod.Name), Escape(mod.Version), Escape(mod.Author), mod.Verdict, mod.TypeRefs.ToString(), mod.MemberRefs.ToString(), mod.HarmonyTargetsChecked.ToString(), mod.Findings.Count.ToString())); foreach (Finding finding in mod.Findings) { stringBuilder.AppendLine(string.Join("|", "F", Escape(mod.Path), Escape(finding.Kind), Escape(finding.Scope), Escape(finding.Symbol), Escape(finding.Reason), Escape(finding.Hint), Escape(finding.Site), Escape(finding.Outcome), Escape(finding.OutcomeDetail))); } } foreach (ModReport mod2 in Mods) { if (mod2.Namespaces.Count > 0) { stringBuilder.AppendLine(string.Join("|", "N", Escape(mod2.Path), Escape(string.Join(",", mod2.Namespaces)))); } } foreach (string item in Dropped) { stringBuilder.AppendLine("D|" + Escape(item)); } return stringBuilder.ToString(); } internal static RunReport Read(IEnumerable lines) { RunReport runReport = new RunReport(); Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); bool flag = false; foreach (string line in lines) { if (line == null) { continue; } string text = line.TrimEnd('\r'); if (!flag) { if (!text.StartsWith("# polyfill-report ", StringComparison.Ordinal)) { runReport.Problem = "this is not a Polyfill report"; return runReport; } string text2 = text.Substring("# polyfill-report ".Length).Trim(); if (!int.TryParse(text2, out var result)) { runReport.Problem = "the format is written as '" + text2 + "', which is not a number"; return runReport; } if (result > 2) { runReport.Problem = $"it is format {result} and this build reads {2}. " + "Polyfill.dll in Mods/ and Polyfill.Boot.dll in Plugins/ are from different releases - update both."; return runReport; } flag = true; } else if (text.StartsWith("# game=", StringComparison.Ordinal)) { runReport.Game = text.Substring(7); } else if (text.StartsWith("# interop=", StringComparison.Ordinal)) { runReport.Interop = text.Substring(10); } else if (text.StartsWith("# generated=", StringComparison.Ordinal)) { runReport.Generated = text.Substring(12); } else if (text.StartsWith("# assemblies=", StringComparison.Ordinal)) { runReport.AssemblyCount = Int(text.Substring(13)); } else { if (text.Length == 0 || text[0] == '#') { continue; } string[] array = text.Split('|'); switch (array[0]) { case "M": if (array.Length >= 10) { ModReport modReport = new ModReport { Path = array[1], AssemblyName = array[2], Name = array[3], Version = array[4], Author = array[5], TypeRefs = Int(array[7]), MemberRefs = Int(array[8]), HarmonyTargetsChecked = Int(array[9]) }; runReport.Mods.Add(modReport); dictionary[modReport.Path] = modReport; } break; case "F": { if (array.Length >= 8 && dictionary.TryGetValue(array[1], out var value)) { value.Findings.Add(new Finding { Kind = array[2], Scope = array[3], Symbol = array[4], Reason = array[5], Hint = array[6], Site = array[7], Outcome = ((array.Length > 8) ? array[8] : "none"), OutcomeDetail = ((array.Length > 9) ? array[9] : "") }); } break; } case "N": { if (array.Length < 3 || !dictionary.TryGetValue(array[1], out var value2)) { break; } string[] array2 = array[2].Split(','); foreach (string text3 in array2) { if (!string.IsNullOrEmpty(text3)) { value2.Namespaces.Add(text3); } } break; } case "D": if (array.Length >= 2) { runReport.Dropped.Add(array[1]); } break; } } } if (!flag) { runReport.Problem = "the file is empty"; } return runReport; } private static int Int(string s) { if (!int.TryParse(s, out var result)) { return 0; } return result; } internal static string Escape(string value) { if (!string.IsNullOrEmpty(value)) { return value.Replace('|', '/').Replace('\r', ' ').Replace('\n', ' '); } return ""; } } internal static class SplitMethods { internal sealed class Entry { internal string Type; internal string Name; internal string[] StandInParameters; internal string[] RealParameters; internal string Because; } internal static readonly Entry[] All = new Entry[1] { new Entry { Type = "Il2CppScheduleOne.Tools.ManagementClipboard", Name = "Close", StandInParameters = new string[1] { "System.Boolean" }, RealParameters = new string[0], Because = "the player's own exit calls Close() and never the flagged one, so a mod's postfix on the old signature stopped seeing ordinary closes" } }; } internal sealed class VersionDb { internal static class Op { internal const string Rename = "R"; internal const string Removed = "-"; internal const string Ambiguous = "?"; internal const string Merge = "M"; } internal static class Origin { internal const string Derived = "derived"; internal const string Curated = "curated"; internal const string Override = "override"; } internal sealed class Row { internal string Op; internal string Kind; internal string Type; internal string From; internal string To; internal string RuleName; internal string Confidence; internal string Note; internal string Origin; internal int Arity; internal string Key => Kind + "|" + Type + "|" + From + "|" + Arity; internal string NameKey => Kind + "|" + Type + "|" + From; } internal sealed class Step { internal GameVersion From; internal GameVersion To; internal string Source = ""; internal readonly Dictionary Renames = new Dictionary(StringComparer.Ordinal); internal readonly Dictionary Removed = new Dictionary(StringComparer.Ordinal); internal readonly HashSet Refused = new HashSet(StringComparer.Ordinal); } internal const string HeaderPrefix = "# polyfill-versiondb "; internal const int Format = 1; private readonly List _steps = new List(); private readonly Dictionary _overrides = new Dictionary(StringComparer.Ordinal); private readonly List _overrideRanges = new List(); internal readonly List Notes = new List(); internal int RenameCount { get; private set; } internal int StepCount => _steps.Count; internal GameVersion Newest { get { if (_steps.Count != 0) { List steps = _steps; return steps[steps.Count - 1].To; } return GameVersion.Unknown; } } internal static VersionDb Load(IEnumerable<(string Name, IEnumerable Lines)> files) { VersionDb versionDb = new VersionDb(); Dictionary dictionary = new Dictionary(StringComparer.Ordinal); foreach (var file in files) { try { versionDb.ReadOne(file.Name, file.Lines, dictionary); } catch (Exception ex) { versionDb.Notes.Add(file.Name + " could not be read (" + ex.Message + ")"); } } versionDb._steps.AddRange(dictionary.Values); versionDb._steps.Sort((Step a, Step b) => a.From.CompareTo(b.From)); foreach (Step step in versionDb._steps) { versionDb.RenameCount += step.Renames.Count; } return versionDb; } private void ReadOne(string name, IEnumerable lines, Dictionary steps) { Step step = null; VersionRange range = null; bool flag = false; List list = new List(); foreach (string line in lines) { if (line == null) { continue; } string text = line.TrimEnd('\r'); if (!flag) { if (!text.StartsWith("# polyfill-versiondb ", StringComparison.Ordinal)) { Notes.Add(name + " is not a version database"); return; } if (!int.TryParse(text.Substring("# polyfill-versiondb ".Length).Trim(), out var result) || result > 1) { Notes.Add(name + " is a newer format than this build reads; skipped"); return; } flag = true; } else if (text.StartsWith("# from=", StringComparison.Ordinal)) { (step ?? (step = new Step())).From = GameVersion.Parse(text.Substring(7)); } else if (text.StartsWith("# to=", StringComparison.Ordinal)) { (step ?? (step = new Step())).To = GameVersion.Parse(text.Substring(5)); } else if (text.StartsWith("# source=", StringComparison.Ordinal)) { (step ?? (step = new Step())).Source = text.Substring(9); } else if (text.StartsWith("# applies=", StringComparison.Ordinal)) { if (!VersionRange.TryParse(text.Substring(10), out range, out var problem)) { Notes.Add($"{name} applies to '{text.Substring(10)}', which is not a range ({problem})"); return; } } else { if (text.Length == 0 || text[0] == '#') { continue; } Row row = Parse(text); if (row == null) { continue; } if (range != null) { TakeOverride(name, row, range); continue; } if (step == null) { Notes.Add(name + " has rows but no step"); return; } switch (row.Op) { case "R": list.Add(row); break; case "-": step.Removed[row.Key] = row; break; case "?": case "M": step.Refused.Add(row.NameKey); break; } } } if (range != null) { return; } if (step == null || !step.From.IsKnown || !step.To.IsKnown) { Notes.Add(name + " does not say which two builds it is between"); return; } Dictionary dictionary = new Dictionary(StringComparer.Ordinal); foreach (Row item in list) { if (step.Renames.TryGetValue(item.Key, out var value)) { step.Refused.Add(item.NameKey); step.Renames.Remove(item.Key); Notes.Add($"{name}: {item.Type}.{item.From} is renamed twice in one step ({value.To} and {item.To}); neither is used"); continue; } string key = item.Kind + "|" + item.Type + "|" + item.To + "|" + item.Arity; if (dictionary.TryGetValue(key, out var value2)) { step.Refused.Add(item.NameKey); step.Refused.Add(value2.NameKey); step.Renames.Remove(value2.Key); Notes.Add($"{name}: {value2.From} and {item.From} both became {item.To} on {item.Type}; " + "neither is used"); } else { dictionary[key] = item; step.Renames[item.Key] = item; } } string text2 = step.From.ToString() + "->" + step.To; if (steps.ContainsKey(text2)) { Notes.Add(name + " replaces the step " + text2 + " that was loaded before it"); } steps[text2] = step; } private void TakeOverride(string name, Row row, VersionRange applies) { if (row.Origin == "override" && string.IsNullOrEmpty(row.Note)) { Notes.Add($"{name}: {row.Type}.{row.From} overrides the history with no reason given; skipped"); } else if (_overrides.ContainsKey(row.Key)) { Notes.Add($"{name}: {row.Type}.{row.From} is decided twice by hand; neither is used"); _overrides.Remove(row.Key); } else { _overrides[row.Key] = row; _overrideRanges.Add(applies); } } private static Row Parse(string line) { string[] array = line.Split('|'); if (array.Length < 10) { return null; } int result; return new Row { Op = array[0], Kind = array[1], Type = array[2], From = array[3], To = array[4], Arity = (int.TryParse(array[5], out result) ? result : 0), Origin = array[6], RuleName = array[7], Confidence = array[8], Note = array[9] }; } internal string Successor(string kind, string type, string name, int arity, GameVersion game) { string key = kind + "|" + type + "|" + name + "|" + arity; if (_overrides.TryGetValue(key, out var value)) { return value.To; } string text = name; foreach (Step step in _steps) { if (game.IsKnown && step.To > game) { break; } string text2 = kind + "|" + type + "|" + text; if (step.Refused.Contains(text2)) { return null; } if (step.Renames.TryGetValue(text2 + "|" + arity, out var value2)) { text = value2.To; } } if (!(text == name)) { return text; } return null; } internal string RemovedIn(string kind, string type, string name, int arity) { string key = kind + "|" + type + "|" + name + "|" + arity; foreach (Step step in _steps) { if (step.Removed.ContainsKey(key)) { return step.To.ToString(); } } return null; } internal bool WasRefused(string kind, string type, string name) { string item = kind + "|" + type + "|" + name; foreach (Step step in _steps) { if (step.Refused.Contains(item)) { return true; } } return false; } internal IEnumerable Versions() { foreach (Step step in _steps) { yield return step.From.ToString(); } if (_steps.Count > 0) { List steps = _steps; yield return steps[steps.Count - 1].To.ToString(); } } internal string Gap() { for (int i = 1; i < _steps.Count; i++) { if (_steps[i - 1].To != _steps[i].From) { return $"{_steps[i - 1].To} is followed by a step that starts at {_steps[i].From}"; } } return null; } } internal sealed class VersionRange { private enum Kind { Any, Prefix, AtLeast, Above, AtMost, Below, Between, Exact } private readonly struct Term { internal readonly Kind Kind; internal readonly GameVersion Low; internal readonly GameVersion High; internal readonly int PrefixParts; internal Term(Kind kind, GameVersion low, GameVersion high = default(GameVersion), int prefixParts = 0) { Kind = kind; Low = low; High = high; PrefixParts = prefixParts; } } private readonly Term[] _terms; internal static readonly VersionRange Any = new VersionRange(new Term[1] { new Term(Kind.Any, GameVersion.Unknown) }, "*"); internal static readonly VersionRange None = new VersionRange(Array.Empty(), "nothing"); internal string Text { get; } private VersionRange(Term[] terms, string text) { _terms = terms; Text = text; } internal static VersionRange Parse(string text) { if (!TryParse(text, out var range, out var problem)) { throw new FormatException("'" + text + "' is not a version range: " + problem); } return range; } internal static bool TryParse(string text, out VersionRange range, out string problem) { range = null; problem = null; if (string.IsNullOrWhiteSpace(text)) { range = Any; return true; } List list = new List(); string[] array = text.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0) { continue; } if (text2 == "*") { list.Add(new Term(Kind.Any, GameVersion.Unknown)); continue; } if (text2.EndsWith("*", StringComparison.Ordinal)) { if (!GameVersion.TryParse(text2.Substring(0, text2.Length - 1).TrimEnd('.', 'f', ' '), out var version)) { problem = "'" + text2 + "' has no version in front of the star"; return false; } list.Add(new Term(Kind.Prefix, version, default(GameVersion), version.Parts.Length)); continue; } int num = text2.IndexOf("..", StringComparison.Ordinal); if (num > 0) { if (!GameVersion.TryParse(text2.Substring(0, num), out var version2) || !GameVersion.TryParse(text2.Substring(num + 2), out var version3)) { problem = "'" + text2 + "' is not two versions with .. between them"; return false; } if (version2 > version3) { problem = "'" + text2 + "' starts after it ends"; return false; } list.Add(new Term(Kind.Between, version2, version3)); continue; } Kind kind = Kind.Exact; string text3 = text2; if (text3.StartsWith(">=", StringComparison.Ordinal)) { kind = Kind.AtLeast; text3 = text3.Substring(2); } else if (text3.StartsWith("<=", StringComparison.Ordinal)) { kind = Kind.AtMost; text3 = text3.Substring(2); } else if (text3.StartsWith(">", StringComparison.Ordinal)) { kind = Kind.Above; text3 = text3.Substring(1); } else if (text3.StartsWith("<", StringComparison.Ordinal)) { kind = Kind.Below; text3 = text3.Substring(1); } if (!GameVersion.TryParse(text3.Trim(), out var version4)) { problem = "'" + text2 + "' has no version in it"; return false; } list.Add(new Term(kind, version4)); } if (list.Count == 0) { problem = "it is empty"; return false; } range = new VersionRange(list.ToArray(), text); return true; } internal bool Allows(GameVersion version) { return Matches(version, unknownAnswer: false); } internal bool AllowsOrUnknown(GameVersion version) { return Matches(version, unknownAnswer: true); } internal bool Allows(string version) { return Allows(GameVersion.Parse(version)); } internal bool AllowsOrUnknown(string version) { return AllowsOrUnknown(GameVersion.Parse(version)); } private bool Matches(GameVersion version, bool unknownAnswer) { Term[] terms = _terms; for (int i = 0; i < terms.Length; i++) { Term term = terms[i]; if (term.Kind == Kind.Any) { return true; } if (!version.IsKnown) { return unknownAnswer; } if (term.Kind switch { Kind.Prefix => version.StartsWith(term.Low, term.PrefixParts), Kind.AtLeast => version >= term.Low, Kind.Above => version > term.Low, Kind.AtMost => version <= term.Low, Kind.Below => version < term.Low, Kind.Between => version >= term.Low && version <= term.High, _ => version == term.Low, }) { return true; } } return false; } public override string ToString() { return Text; } internal IEnumerable Bounds() { Term[] terms = _terms; for (int i = 0; i < terms.Length; i++) { Term term = terms[i]; if (term.Kind != Kind.Any) { if (term.Low.IsKnown) { yield return term.Low; } if (term.Kind == Kind.Between && term.High.IsKnown) { yield return term.High; } } } } internal string Describe() { StringBuilder stringBuilder = new StringBuilder(); Term[] terms = _terms; for (int i = 0; i < terms.Length; i++) { Term term = terms[i]; if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } StringBuilder stringBuilder2 = stringBuilder; stringBuilder2.Append(term.Kind switch { Kind.Any => "any build", Kind.Prefix => term.Low.ToString() + " and its builds", Kind.AtLeast => term.Low.ToString() + " and newer", Kind.Above => "newer than " + term.Low, Kind.AtMost => term.Low.ToString() + " and older", Kind.Below => "older than " + term.Low, Kind.Between => term.Low.ToString() + " to " + term.High, _ => term.Low.ToString(), }); } return stringBuilder.ToString(); } } } namespace Polyfill.ModFixes { internal sealed class AmountChangedAfterOverride : Fix { private static Instance _log; private static bool _said; private static readonly HashSet Complained = new HashSet(); internal override string Id => "amount-changed-after-override"; internal override string Mod => "Tweakables"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "raising the deal cap tells the rest of the screen the price moved, so it does not show a stale number"; internal override string StandsDownBecause => "the field this mod used to announce a price change with does not exist in 0.4.6, and the event that replaced it is only raised by the method the mod replaces."; internal override bool Apply(Instance log) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown _log = log; Type type = AccessTools.TypeByName("Il2CppScheduleOne.UI.AmountSelector"); if (type == null) { log.Warning("[fix] amount-changed-after-override: Il2CppScheduleOne.UI.AmountSelector is not on this build, so there is nothing to listen to."); return false; } MethodInfo methodInfo = AccessTools.Method(type, "SetAmount", new Type[1] { typeof(float) }, (Type[])null); if (methodInfo == null) { log.Warning("[fix] amount-changed-after-override: AmountSelector.SetAmount(float) is not here, so the moment the notification goes missing cannot be found."); return false; } new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(AmountChangedAfterOverride), "After", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } private static void After(object __instance, bool __runOriginal) { if (__runOriginal || __instance == null) { return; } try { Type type = __instance.GetType(); object obj = AccessTools.PropertyGetter(type, "OnAmountChanged")?.Invoke(__instance, null); if (obj == null) { return; } object obj2 = AccessTools.PropertyGetter(type, "SelectedAmount")?.Invoke(__instance, null); if (obj2 == null) { Complain("the selector has no SelectedAmount to announce"); return; } MethodInfo methodInfo = AccessTools.Method(obj.GetType(), "Invoke", new Type[1] { typeof(float) }, (Type[])null); if (methodInfo == null) { Complain("the event on this build takes something other than a single float, so what to pass it cannot be worked out without guessing"); return; } methodInfo.Invoke(obj, new object[1] { obj2 }); if (!_said) { _said = true; Instance log = _log; if (log != null) { log.Msg("[fix] amount-changed-after-override: a mod replaced the amount box's own setter, so Polyfill raises the change event it would have raised."); } } } catch (Exception ex) { Complain("could not raise the change event: " + ex.GetType().Name + ": " + ex.Message); } } private static void Complain(string why) { if (Complained.Add(why)) { Instance log = _log; if (log != null) { log.Warning("[fix] amount-changed-after-override: " + why + ". Whatever listens for a price change will not hear this one."); } Fixes.Record("amount-changed-after-override", "did nothing: " + why); } } } internal sealed class BiggerTreesScale : Fix { private const string TerrainPath = "Hyland Point/Main Terrain"; private const int WaitSeconds = 75; private static Instance _log; private static MelonPreferences_Entry _factor; private static readonly Dictionary Original = new Dictionary(); private static float _largest; internal override string Id => "biggertrees-instance-scale"; internal override string Mod => "BiggerTrees"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6f5"; internal override bool NeedsAScreen => true; internal override string What => "the trees actually get bigger"; internal override string StandsDownBecause => "Bigger Trees will apply its setting and nothing will change on screen, because the terrain has drawn no trees since 0.4.6f5."; internal override bool Apply(Instance log) { _log = log; ReadPreference(); float num = _factor?.Value ?? 2f; if (num <= 1.0001f) { log.Msg($"[fix] biggertrees-instance-scale: the size is set to {num:0.##}, so the trees " + "are left as they are. `TreeScale` in MelonPreferences changes it."); return false; } MelonCoroutines.Start(Mirror()); return true; } private static IEnumerator Mirror() { int done = 0; int waitingFor = 0; int waited = 0; while (true) { yield return (object)new WaitForSecondsRealtime(1f); Terrain val = FindTerrain(); if ((Object)(object)val == (Object)null) { continue; } int instanceID = ((Object)val).GetInstanceID(); if (instanceID == done) { continue; } if (instanceID != waitingFor) { waitingFor = instanceID; waited = 0; } if (ModHasApplied(val)) { done = instanceID; if (!Resize(_factor?.Value ?? 2f)) { Fixes.Record("biggertrees-instance-scale", "did nothing"); } continue; } int num = waited + 1; waited = num; if (num >= 75) { done = instanceID; Fixes.Record("biggertrees-instance-scale", "did nothing"); Instance log = _log; if (log != null) { log.Msg("[fix] biggertrees-instance-scale: the terrain's tree LOD bias never moved off 1, so Bigger Trees did not apply and nothing was resized."); } } } } private static bool ModHasApplied(Terrain terrain) { try { return terrain.treeLODBiasMultiplier > 1.0001f; } catch { return false; } } private static bool Resize(float factor) { InstancingManager val = null; try { val = Object.FindObjectOfType(); } catch (Exception ex) { Instance log = _log; if (log != null) { log.Warning("[fix] biggertrees-instance-scale: " + ex.Message); } return false; } if ((Object)(object)val == (Object)null) { Instance log2 = _log; if (log2 != null) { log2.Warning("[fix] biggertrees-instance-scale: this build has no instanced renderer, so the mod's own setting works as it always did."); } return false; } List backedInstanceObjects = val.BackedInstanceObjects; if (backedInstanceObjects == null || backedInstanceObjects.Count == 0) { return false; } int num = 0; int num2 = 0; _largest = 0f; for (int i = 0; i < backedInstanceObjects.Count; i++) { int num3 = ScaleOne(backedInstanceObjects[i], factor); if (num3 > 0) { num++; num2 = Mathf.Max(num2, num3); } } if (num == 0) { Instance log3 = _log; if (log3 != null) { log3.Warning("[fix] biggertrees-instance-scale: nothing carried a size to change."); } return false; } Instance log4 = _log; if (log4 != null) { log4.Msg($"[fix] biggertrees-instance-scale: {num2} tree(s) resized to {factor:0.##}x across {num} level(s) of detail, largest now {_largest:0.###}. `TreeScale` in " + "MelonPreferences changes it."); } return true; } private static int ScaleOne(InstanceObjectData data, float factor) { //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) Texture2D val = ((data != null) ? data.PositionData : null); if ((Object)(object)val == (Object)null) { return 0; } int width = ((Texture)val).width; int height = ((Texture)val).height; if (width <= 0 || height <= 0) { return 0; } try { Texture2D val2 = Readable(val, width, height); Il2CppStructArray pixels = val2.GetPixels(); int instanceID = ((Object)data).GetInstanceID(); if (!Original.TryGetValue(instanceID, out var value) || value.Length != ((Il2CppArrayBase)(object)pixels).Length) { value = (Color[])(object)new Color[((Il2CppArrayBase)(object)pixels).Length]; for (int i = 0; i < ((Il2CppArrayBase)(object)pixels).Length; i++) { value[i] = ((Il2CppArrayBase)(object)pixels)[i]; } Original[instanceID] = value; } int num = 0; for (int j = 0; j < ((Il2CppArrayBase)(object)pixels).Length; j++) { Color val3 = value[j]; if (val3.a <= 0f) { ((Il2CppArrayBase)(object)pixels)[j] = val3; continue; } num++; float num2 = val3.a * factor; if (num2 > _largest) { _largest = num2; } ((Il2CppArrayBase)(object)pixels)[j] = new Color(val3.r, val3.g, val3.b, num2); } if (num == 0) { return 0; } val2.SetPixels(pixels); val2.Apply(false, false); data.PositionData = val2; return num; } catch (Exception ex) { Instance log = _log; if (log != null) { log.Warning("[fix] biggertrees-instance-scale: could not resize a level of detail: " + ex.Message); } return 0; } } private static Texture2D Readable(Texture2D source, int width, int height) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0038: 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) //IL_004d: Expected O, but got Unknown RenderTexture temporary = RenderTexture.GetTemporary(width, height, 0, (RenderTextureFormat)11, (RenderTextureReadWrite)1); RenderTexture active = RenderTexture.active; try { Graphics.Blit((Texture)(object)source, temporary); RenderTexture.active = temporary; Texture2D val = new Texture2D(width, height, (TextureFormat)20, false); val.ReadPixels(new Rect(0f, 0f, (float)width, (float)height), 0, 0); val.Apply(false, false); return val; } finally { RenderTexture.active = active; RenderTexture.ReleaseTemporary(temporary); } } private static Terrain FindTerrain() { try { GameObject obj = GameObject.Find("Map"); Transform val = ((obj != null) ? obj.transform.Find("Hyland Point/Main Terrain") : null); return ((Object)(object)val == (Object)null) ? null : ((Component)val).GetComponent(); } catch { return null; } } private static void ReadPreference() { if (_factor != null) { return; } try { MelonPreferences_Category val = MelonPreferences.GetCategory("Polyfill") ?? MelonPreferences.CreateCategory("Polyfill"); _factor = val.GetEntry("TreeScale") ?? val.CreateEntry("TreeScale", 2f, "How much bigger the trees get", "Only with the Bigger Trees mod installed. 1 leaves them alone.", false, false, (ValueValidator)null, (string)null); } catch { } } } internal sealed class BorrowedAppLayout : Fix { private static readonly (string Type, string Method, string Field)[] Hooks = new(string, string, string)[3] { ("MediaPlayer.PhoneIntegration", "ClearContainer", null), ("Tweakables.TweakablesApp", "ClonePanel", "_appPanel"), ("ElDiablo59WagesManager.WagesApp", "BuildAppRoot", "_appRoot") }; private static Instance _log; private static readonly HashSet Done = new HashSet(); private static readonly Dictionary Owners = new Dictionary(); private static readonly HashSet Complained = new HashSet(); internal override string Id => "borrowed-app-layout"; internal override string Mod => "*"; internal override string ModVersions => "*"; internal override string GameVersions => "0.4.6*"; internal override bool NeedsAScreen => true; internal override string What => "phone apps built inside a borrowed vanilla one fill the screen again instead of being squeezed into a strip at the top"; internal override string StandsDownBecause => "0.4.6 put a vertical layout group on the app container these mods clone, which overrules where they put their own panels."; internal override bool Apply(Instance log) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Expected O, but got Unknown _log = log; int num = 0; Harmony val = new Harmony("doodesch.polyfill.fixes"); (string, string, string)[] hooks = Hooks; for (int i = 0; i < hooks.Length; i++) { (string, string, string) tuple = hooks[i]; string item = tuple.Item1; string item2 = tuple.Item2; string item3 = tuple.Item3; Type type = AccessTools.TypeByName(item); if (type == null) { continue; } MethodInfo methodInfo = AccessTools.Method(type, item2, (Type[])null, (Type[])null); if (methodInfo == null) { log.Warning($"[fix] borrowed-app-layout: {item} is here but {item2} is not, " + "so this version of the mod builds its app some other way and the moment to act cannot be found."); continue; } FieldInfo fieldInfo = ((item3 == null) ? null : AccessTools.Field(type, item3)); if (item3 != null && fieldInfo == null) { log.Warning($"[fix] borrowed-app-layout: {item} has no {item3}, so the app it " + "clones cannot be identified - and guessing which one is its would risk the game's own screens."); continue; } try { Owners[methodInfo] = fieldInfo; val.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(BorrowedAppLayout), "Loosen", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } catch (Exception ex) { log.Warning($"[fix] borrowed-app-layout: could not hook {item}.{item2}: " + ex.Message); } } return num > 0; } private static string Path(Transform transform) { try { List list = new List(); Transform val = transform; while ((Object)(object)val != (Object)null) { list.Add(((Object)val).name); if (list.Count > 12) { break; } val = val.parent; } list.Reverse(); return string.Join("/", list); } catch { return ((Object)(object)transform == (Object)null) ? "(nothing)" : ((Object)transform).name; } } private static void Loosen(MethodBase __originalMethod, object[] __args) { try { Owners.TryGetValue(__originalMethod, out var value); GameObject val = null; if (value != null) { object? value2 = value.GetValue(null); val = (GameObject)((value2 is GameObject) ? value2 : null); } else if (__args != null && __args.Length != 0) { object obj = __args[0]; val = (GameObject)((obj is GameObject) ? obj : null); } if ((Object)(object)val == (Object)null) { Complain("the mod's own app object was not there to read after it built, so the container cannot be identified"); return; } Transform val2 = ((((Object)val.transform).name == "Container") ? val.transform : val.transform.Find("Container")); if ((Object)(object)val2 == (Object)null) { Complain("the cloned app has no Container child, so the shape it was written against is not the shape it got"); } else { if (!Done.Add(((Object)val2).GetInstanceID())) { return; } VerticalLayoutGroup component = ((Component)val2).GetComponent(); if ((Object)(object)component == (Object)null) { Complain("the app container has no vertical layout group any more, so there is nothing to loosen and the mod's own anchors already decide"); } else if (((Behaviour)component).enabled) { ((Behaviour)component).enabled = false; Instance log = _log; if (log != null) { log.Msg("[fix] borrowed-app-layout: switched off the vertical layout group on " + Path(val2) + ", inherited from the game's product manager, so the app's own panels decide where they go again."); } } } } catch (Exception ex) { Complain("could not read the cloned app: " + ex.GetType().Name + ": " + ex.Message); } } private static void Complain(string why) { if (Complained.Add(why)) { Instance log = _log; if (log != null) { log.Warning("[fix] borrowed-app-layout: " + why + ". The app is left exactly as the mod built it."); } Fixes.Record("borrowed-app-layout", "did nothing: " + why); } } } internal static class ButtonCodeShift { private static readonly string[] OldOrder = new string[34] { "PrimaryClick", "SecondaryClick", "TertiaryClick", "Forward", "Backward", "Left", "Right", "Jump", "Crouch", "Sprint", "Escape", "Back", "Interact", "Submit", "TogglePhone", "VehicleToggleLights", "VehicleHandbrake", "RotateLeft", "RotateRight", "ManagementMode", "OpenMap", "OpenJournal", "OpenTexts", "QuickMove", "ToggleFlashlight", "ViewAvatar", "Reload", "InventoryLeft", "InventoryRight", "Holster", "VehicleResetCamera", "SkateboardDismount", "SkateboardMount", "TogglePauseMenu" }; private const int Nowhere = -1; private static readonly Dictionary> Instead = new Dictionary>(StringComparer.OrdinalIgnoreCase) { ["otc-button-codes"] = new Dictionary(StringComparer.Ordinal) { ["Escape"] = "SecondaryClick", ["Back"] = "SecondaryClick" } }; private static int[] _map; private static Instance _log; private static readonly List<(int Old, int Now)> _pointed = new List<(int, int)>(); private static string Current = "?"; internal static int[] Map(Instance log) { if (_map != null) { return _map; } _log = log; string[] array = InstalledOrder(); if (array == null || array.Length == 0) { return null; } bool flag = array.Length != OldOrder.Length; int[] array2 = new int[OldOrder.Length]; for (int i = 0; i < OldOrder.Length; i++) { array2[i] = -1; for (int j = 0; j < array.Length; j++) { if (string.Equals(array[j], OldOrder[i], StringComparison.Ordinal)) { array2[i] = j; break; } } if (array2[i] != i) { flag = true; } } return _map = (flag ? array2 : null); } private static string[] InstalledOrder() { try { Type nestedType = typeof(GameInput).GetNestedType("ButtonCode"); return (nestedType == null) ? null : Enum.GetNames(nestedType); } catch { return null; } } private static int InsteadOf(int old) { if (!Instead.TryGetValue(Current, out var value)) { return -1; } if (!value.TryGetValue(OldName(old), out var value2)) { return -1; } string[] array = InstalledOrder(); if (array == null) { return -1; } for (int i = 0; i < array.Length; i++) { if (string.Equals(array[i], value2, StringComparison.Ordinal)) { return i; } } return -1; } internal static string OldName(int value) { if (value < 0 || value >= OldOrder.Length) { return value.ToString(); } return OldOrder[value]; } internal static IEnumerable Transpile(IEnumerable instructions, string who) { List list = new List(instructions); int[] map = _map; if (map == null) { return list; } for (int i = 0; i + 1 < list.Count; i++) { if (TakesAButton(list[i + 1]) && Constant(list[i], out var value) && value >= 0 && value < map.Length) { int num = map[value]; if (num == -1) { num = InsteadOf(value); } if (num != value) { CodeInstruction val = Load(num); list[i].opcode = val.opcode; list[i].operand = val.operand; _pointed.Add((value, num)); } } } return list; } private static bool TakesAButton(CodeInstruction instruction) { if (instruction.opcode != OpCodes.Call && instruction.opcode != OpCodes.Callvirt) { return false; } if (instruction.operand is MethodInfo methodInfo && methodInfo.DeclaringType == typeof(GameInput)) { return methodInfo.Name.StartsWith("GetButton", StringComparison.Ordinal); } return false; } private static bool Constant(CodeInstruction instruction, out int value) { value = 0; OpCode opcode = instruction.opcode; if (opcode == OpCodes.Ldc_I4) { value = (int)instruction.operand; return true; } if (opcode == OpCodes.Ldc_I4_S) { value = Convert.ToInt32(instruction.operand); return true; } if (opcode == OpCodes.Ldc_I4_M1) { value = -1; return true; } for (int i = 0; i <= 8; i++) { if (opcode == Short(i)) { value = i; return true; } } return false; } private static CodeInstruction Load(int value) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown if (value < 0 || value > 8) { if (value >= -128 && value <= 127) { return new CodeInstruction(OpCodes.Ldc_I4_S, (object)(sbyte)value); } return new CodeInstruction(OpCodes.Ldc_I4, (object)value); } return new CodeInstruction(Short(value), (object)null); } private static OpCode Short(int value) { return value switch { 0 => OpCodes.Ldc_I4_0, 1 => OpCodes.Ldc_I4_1, 2 => OpCodes.Ldc_I4_2, 3 => OpCodes.Ldc_I4_3, 4 => OpCodes.Ldc_I4_4, 5 => OpCodes.Ldc_I4_5, 6 => OpCodes.Ldc_I4_6, 7 => OpCodes.Ldc_I4_7, _ => OpCodes.Ldc_I4_8, }; } internal static int Apply(Instance log, string who, string assembly, params (string Type, string Method)[] targets) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Expected O, but got Unknown //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Expected O, but got Unknown if (Map(log) == null) { log.Msg("[fix] " + who + ": this game orders the buttons the way the mod expects, so nothing needed pointing anywhere."); return 0; } Assembly assembly2 = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly3 in assemblies) { if (string.Equals(assembly3.GetName()?.Name, assembly, StringComparison.OrdinalIgnoreCase)) { assembly2 = assembly3; break; } } if (assembly2 == null) { return 0; } Harmony val = new Harmony("doodesch.polyfill.fixes"); int num = 0; for (int i = 0; i < targets.Length; i++) { var (text, text2) = targets[i]; try { Type type = assembly2.GetType(text, throwOnError: false); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, text2, (Type[])null, (Type[])null)); if (methodInfo == null) { log.Warning($"[fix] {who}: {text}.{text2} is not where it was."); continue; } Current = who; _pointed.Clear(); val.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(ButtonCodeShift), "Rewrite", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); foreach (var (value, num2) in _pointed) { Dictionary value2; if (num2 == -1) { log.Warning($"[fix] {who}: {text} asks for the {OldName(value)} button, which " + "this build of the game does not have any more, so that key does nothing."); } else if (Instead.TryGetValue(who, out value2) && value2.ContainsKey(OldName(value))) { log.Msg($"[fix] {who}: {text}.{text2} asked for the {OldName(value)} button, which this build does not have. Pointed at {value2[OldName(value)]}, which is what the mod's own maintainers " + "chose when they hit this."); } else { log.Msg($"[fix] {who}: {text}.{text2} asked for button {value}, which was {OldName(value)} and is now {num2}. Pointed at {num2}."); } } if (_pointed.Count > 0) { num++; } } catch (Exception ex) { log.Warning($"[fix] {who}: {text}.{text2} could not be pointed: {ex.Message}"); } } return num; } private static IEnumerable Rewrite(IEnumerable instructions) { return Transpile(instructions, Current); } } internal sealed class DeepPocketsEarlyBroadcast : Fix { private static Instance _log; internal override bool Early => true; internal override string Id => "deeppockets-early-broadcast"; internal override string Mod => "Deep Pockets"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "Deep Pockets answers every other mod's settings save by looking for players who are not there yet, before the game is loaded."; internal override bool Apply(Instance log) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown _log = log; Type type = AccessTools.TypeByName("DeepPockets.Config"); if (type == null) { log.Msg("[fix] deeppockets-early-broadcast: Deep Pockets is not loaded, so there is nothing to guard."); return false; } MethodInfo methodInfo = AccessTools.Method(type, "BroadcastHostConfigLive", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo.GetParameters().Length != 0) { log.Warning("[fix] deeppockets-early-broadcast: DeepPockets.Config has no no-argument BroadcastHostConfigLive on this build, so it was left alone."); return false; } try { new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(DeepPocketsEarlyBroadcast), "NotBeforeTheGameIsUp", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { log.Warning("[fix] deeppockets-early-broadcast: could not guard DeepPockets.Config.BroadcastHostConfigLive: " + ex.Message); return false; } log.Msg("[fix] deeppockets-early-broadcast: Deep Pockets waits for the game before it looks for players to send its settings to."); return true; } private static bool NotBeforeTheGameIsUp() { return MainSceneLatch.Reached; } } internal sealed class EmptyDeadDropSearch : Fix { private static Instance _log; private static bool _said; private static readonly HashSet Complained = new HashSet(); internal override string Id => "empty-dead-drop-search"; internal override string Mod => "*"; internal override string ModVersions => "*"; internal override string GameVersions => "*"; internal override string What => "asking for a free dead drop when every one is full answers 'none' instead of throwing, so a mod that puts something in one carries on"; internal override string StandsDownBecause => "DeadDrop.GetRandomEmptyDrop drops the nearest candidate before checking whether it had any, so a world with no free dead drop throws out of the middle of whatever called it."; internal override bool Apply(Instance log) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Expected O, but got Unknown _log = log; Type type = AccessTools.TypeByName("Il2CppScheduleOne.Economy.DeadDrop"); if (type == null) { log.Warning("[fix] empty-dead-drop-search: Il2CppScheduleOne.Economy.DeadDrop is not on this build, so there is nothing to guard."); return false; } MethodInfo methodInfo = AccessTools.Method(type, "GetRandomEmptyDrop", (Type[])null, (Type[])null); if (methodInfo == null) { log.Warning("[fix] empty-dead-drop-search: DeadDrop.GetRandomEmptyDrop is not here. If the game renamed it, a mod calling the old name has a bigger problem than this guard."); return false; } new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(EmptyDeadDropSearch), "Before", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } private static bool Before(ref object __result) { try { Type type = AccessTools.TypeByName("Il2CppScheduleOne.Economy.DeadDrop"); object obj = AccessTools.Property(type, "DeadDrops")?.GetValue(null) ?? AccessTools.Field(type, "DeadDrops")?.GetValue(null); if (obj == null) { Complain("neither a property nor a field called DeadDrops"); return true; } int num = 0; foreach (object item in Enumerate(obj)) { if (item == null) { continue; } object obj2 = AccessTools.Property(item.GetType(), "Storage")?.GetValue(item) ?? AccessTools.Field(item.GetType(), "Storage")?.GetValue(item); if (obj2 != null) { object obj3 = AccessTools.Property(obj2.GetType(), "ItemCount")?.GetValue(obj2); if (obj3 is int && (int)obj3 == 0) { num++; } } } if (num > 0) { return true; } __result = null; if (!_said) { _said = true; Instance log = _log; if (log != null) { log.Msg("[fix] empty-dead-drop-search: every dead drop in the world is full, so the game was asked for one and answered instead of throwing."); } } return false; } catch (Exception ex) { Complain(ex.GetType().Name + ": " + ex.Message); return true; } } private static IEnumerable Enumerate(object list) { int count = (AccessTools.Property(list.GetType(), "Count")?.GetValue(list) as int?).GetValueOrDefault(); PropertyInfo item = AccessTools.Property(list.GetType(), "Item"); if (item == null) { yield break; } for (int i = 0; i < count; i++) { object obj = null; try { obj = item.GetValue(list, new object[1] { i }); } catch { } if (obj != null) { yield return obj; } } } private static void Complain(string why) { if (Complained.Add(why)) { Instance log = _log; if (log != null) { log.Warning("[fix] empty-dead-drop-search: could not read the dead drop list (" + why + "), so the game answers this one itself."); } Fixes.Record("empty-dead-drop-search", "stood aside: " + why); } } } internal abstract class Fix { private VersionRange _forMod; private VersionRange _forGame; internal abstract string Id { get; } internal abstract string Mod { get; } internal abstract string ModVersions { get; } internal abstract string GameVersions { get; } internal virtual string StandsDownBecause => null; internal virtual bool Early => false; internal virtual bool NeedsAScreen => false; internal abstract string What { get; } internal VersionRange ForMod => _forMod ?? (_forMod = Range(ModVersions, "ModVersions")); internal VersionRange ForGame => _forGame ?? (_forGame = Range(GameVersions, "GameVersions")); internal string RangeProblem { get; private set; } internal abstract bool Apply(Instance log); internal bool AppliesTo(string modVersion, string gameVersion) { if (ForMod.Allows(modVersion)) { return ForGame.Allows(gameVersion); } return false; } internal bool GameIsNewerThanKnown(string gameVersion) { GameVersion gameVersion2 = GameVersion.Parse(gameVersion); if (!gameVersion2.IsKnown) { return false; } bool result = false; foreach (GameVersion item in ForGame.Bounds()) { result = true; if (gameVersion2 <= item) { return false; } } return result; } private VersionRange Range(string text, string which) { if (VersionRange.TryParse(text, out var range, out var problem)) { return range; } RangeProblem = $"{which} is '{text}', which is not a version range ({problem})"; return VersionRange.None; } } internal static class Fixes { internal sealed class Outcome { internal Fix Fix; internal string Mod; internal string State; } internal static readonly HashSet Repaired = new HashSet(StringComparer.Ordinal); internal static readonly List Results = new List(); private static readonly List All = new List { new S1MapiPrefabLookup(), new S1MapiClonedDoors(), new S1MapiPrefabs(), new S1MapiInstancedTrees(), new OverTheCounterNetworkLib(), new OverTheCounterDrifterPrefab(), new OverTheCounterAdoptsOnlyClones(), new OverTheCounterButtonCodes(), new OverTheCounterClipboard(), new OverTheCounterStalePanel(), new OverTheCounterSmartFill(), new OverTheCounterHandover(), new MeetPointsLobbyChat(), new SmartEmployeesLobbyChat(), new MoreRealisticSleepingPhoneFonts(), new DeepPocketsEarlyBroadcast(), new MulesPrefsReloadOnAWorker(), new SupplierMeetingNeverStarts(), new PhoneAppIconWithoutFile(), new ThmButtonCodes(), new BiggerTreesScale(), new GraphicsModSunToggle(), new ScheduleActionsSurviveDeath(), new MoreFootPatrolsOfficerPool(), new GuiDrawTexture(), new BorrowedAppLayout(), new EmptyDeadDropSearch(), new AmountChangedAfterOverride(), new StorageMenuClosedEvent(), new PatchesOnGrownOverloads(), new PatchesOnSplitMethods(), new PatchesOnNarrowedOverloads(), new PatchesOnResultTurnedArgument(), new SplitScreenPatches(), new PatchesOnReplacedMethods() }; private static MelonPreferences_Entry _disabled; internal static void RunEarly(Instance log) { Run(log, early: true); } internal static void Run(Instance log) { Run(log, early: false); } private static void Run(Instance log, bool early) { ReadPreference(); string text = GameVersion(); foreach (Fix item in All) { if (item.Early != early) { continue; } Outcome outcome = new Outcome { Fix = item, Mod = InstalledVersion(item.Mod) }; Results.Add(outcome); string why; if (outcome.Mod == null) { outcome.State = "not installed"; } else if (item.RangeProblem != null) { outcome.State = "broken: " + item.RangeProblem; log.Error($"[fix] {item.Id}: {item.RangeProblem}. It will not run. This is a bug in " + "Polyfill, not in your setup."); } else if (IsOff(item.Id)) { outcome.State = "off"; } else if (!item.AppliesTo(outcome.Mod, text)) { outcome.State = "wrong version"; if (item.GameIsNewerThanKnown(text)) { outcome.State = "needs checking on " + text; log.Warning($"[fix] {item.Id} was written for {item.ForGame.Describe()} and this game is {text}, so it did not run. {item.StandsDownBecause ?? item.What} ({item.Mod} {outcome.Mod} is installed.)"); } } else if (item.NeedsAScreen && Headless.Yes(out why)) { outcome.State = "not needed without a screen"; log.Msg($"[fix] {item.Id} did not run: {why}, and this repair only changes what a player sees. {item.What}"); } else { bool flag; try { flag = item.Apply(log); } catch (Exception ex) { outcome.State = "failed: " + ex.Message; log.Warning("[fix] " + item.Id + " failed and changed nothing: " + ex.Message); continue; } outcome.State = (flag ? "applied" : "did nothing"); if (flag) { log.Msg("[fix] " + item.Id + ": " + item.What); } } } } internal static void Record(string id, string state) { foreach (Outcome result in Results) { if (string.Equals(result.Fix.Id, id, StringComparison.OrdinalIgnoreCase)) { result.State = state; } } } private static string InstalledVersion(string name) { if (name == "*") { return "*"; } try { foreach (MelonBase registeredMelon in MelonBase.RegisteredMelons) { if (((registeredMelon != null) ? registeredMelon.Info : null) != null && string.Equals(registeredMelon.Info.Name, name, StringComparison.OrdinalIgnoreCase)) { return registeredMelon.Info.Version ?? ""; } } } catch { } try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { string text = assembly.GetName()?.Name; if (text != null && (string.Equals(text, name, StringComparison.OrdinalIgnoreCase) || string.Equals(text, name + "_Il2Cpp", StringComparison.OrdinalIgnoreCase))) { return assembly.GetName().Version?.ToString() ?? ""; } } } catch { } return null; } private static string GameVersion() { try { return Application.version; } catch { return ""; } } internal static bool IsOff(string id) { string text = _disabled?.Value; if (string.IsNullOrEmpty(text)) { return false; } string[] array = text.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { if (string.Equals(array[i].Trim(), id, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } internal static void Set(string id, bool on) { ReadPreference(); List list = new List(); string[] array = (_disabled.Value ?? "").Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0 && !string.Equals(text, id, StringComparison.OrdinalIgnoreCase)) { list.Add(text); } } if (!on) { list.Add(id); } _disabled.Value = string.Join(",", list); try { MelonPreferences.Save(); } catch { } } internal static bool Known(string id) { foreach (Fix item in All) { if (string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static void ReadPreference() { if (_disabled != null) { return; } try { MelonPreferences_Category val = MelonPreferences.GetCategory("Polyfill") ?? MelonPreferences.CreateCategory("Polyfill"); _disabled = val.GetEntry("DisabledFixes") ?? val.CreateEntry("DisabledFixes", "", "Mod fixes to leave alone", "Comma separated ids of per-mod fixes that should not run. Type `polyfillfixes` in the console to see the ids.", false, false, (ValueValidator)null, (string)null); } catch { } } } internal sealed class GraphicsModSunToggle : Fix { private const string OldPath = "Managers/@EnvironmentFX/SkySystemController"; private static Instance _log; private static MethodInfo _sunLight; private static bool _said; internal override string Id => "graphicsmod-sun-toggle"; internal override string Mod => "GraphicsMOD"; internal override string ModVersions => "2.0.0"; internal override string GameVersions => "0.4.6f13"; internal override bool NeedsAScreen => true; internal override string What => "GraphicsMOD's lighting toggle reaches the sun the game holds now"; internal override string StandsDownBecause => "GraphicsMOD's lighting option does nothing at all - it looks for the sun under a path 0.4.6 no longer has, and says so in the log."; internal override bool Apply(Instance log) { //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Expected O, but got Unknown _log = log; Type type = AccessTools.TypeByName("GraphicsSettings"); if (type == null) { return false; } MethodInfo methodInfo = AccessTools.Method(type, "OptimitationLights", (Type[])null, (Type[])null); ParameterInfo[] array = methodInfo?.GetParameters(); if (methodInfo == null || methodInfo.ReturnType != typeof(void) || array.Length != 1 || array[0].ParameterType != typeof(bool)) { log.Warning("[fix] " + Id + ": GraphicsSettings.OptimitationLights is not the one-argument method this knows, so the lighting toggle stays as it is."); return false; } _sunLight = AccessTools.PropertyGetter(typeof(DayNightController), "_sunLight"); if (_sunLight == null) { log.Warning("[fix] " + Id + ": DayNightController has no _sunLight on this build, so there is no sun to point the toggle at."); return false; } new Harmony("doodesch.polyfill.graphicsmodsun").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(GraphicsModSunToggle), "ToggleTheSunTheGameHolds", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } private static bool ToggleTheSunTheGameHolds(bool enabled) { try { if ((Object)(object)GameObject.Find("Managers/@EnvironmentFX/SkySystemController") != (Object)null) { return true; } } catch { return true; } try { DayNightController val = Object.FindObjectOfType(); if ((Object)(object)val == (Object)null) { return Complain("no DayNightController is loaded"); } object? obj2 = _sunLight.Invoke(val, null); Light val2 = (Light)((obj2 is Light) ? obj2 : null); if (val2 == null || (Object)(object)val2 == (Object)null) { return Complain("the controller has no sun light"); } ((Component)val2).gameObject.SetActive(enabled); if (!_said) { _said = true; Instance log = _log; if (log != null) { log.Msg("[fix] graphicsmod-sun-toggle: the lighting option now switches the sun the game holds, which is where 0.4.6 moved it."); } } return false; } catch (Exception ex) { return Complain(ex.Message); } } private static bool Complain(string why) { if (!_said) { _said = true; Instance log = _log; if (log != null) { log.Warning("[fix] graphicsmod-sun-toggle: " + why + ", so the lighting option was left alone and changed nothing."); } } return true; } } internal sealed class GuiDrawTexture : Fix { private const string StubMessage = "Method unstripping failed"; private static Instance _log; private static bool _saidBorders; private static string _gaveUp; internal override string Id => "gui-drawtexture"; internal override string Mod => "*"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override bool NeedsAScreen => true; internal override string What => "mods that draw an image over the screen with GUI.DrawTexture draw it instead of throwing"; internal override string StandsDownBecause => "GUI.DrawTexture is a stub that throws in this build, and a mod calling it from OnGUI throws once per frame until the game dies."; internal override bool Apply(Instance log) { //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Expected O, but got Unknown _log = log; MethodInfo methodInfo = AccessTools.Method(typeof(GUI), "DrawTexture", new Type[12] { typeof(Rect), typeof(Texture), typeof(ScaleMode), typeof(bool), typeof(float), typeof(Color), typeof(Color), typeof(Color), typeof(Color), typeof(Vector4), typeof(Vector4), typeof(bool) }, (Type[])null); if (methodInfo == null) { log.Warning("[fix] gui-drawtexture: the overload every other one calls is not where it was, so the family cannot be repaired in one place."); return false; } if (!IsStub(methodInfo)) { log.Msg("[fix] gui-drawtexture: GUI.DrawTexture works in this build. Nothing to repair."); return false; } new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(GuiDrawTexture), "Draw", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } private static bool IsStub(MethodBase method) { try { byte[] array = method.GetMethodBody()?.GetILAsByteArray(); if (array == null || array.Length < 5) { return false; } Module module = method.Module; for (int i = 0; i + 4 < array.Length; i++) { if (array[i] == 114) { int metadataToken = array[i + 1] | (array[i + 2] << 8) | (array[i + 3] << 16) | (array[i + 4] << 24); if (module.ResolveString(metadataToken) == "Method unstripping failed") { return true; } } } } catch (Exception ex) { Instance log = _log; if (log != null) { log.Warning("[fix] gui-drawtexture: could not read GUI.DrawTexture to see whether it is a stub, so it was left alone: " + ex.Message); } } return false; } private static bool Draw(Rect position, Texture image, ScaleMode scaleMode, bool alphaBlend, float imageAspect, Color leftColor, Vector4 borderWidths) { //IL_013e: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Expected O, but got Unknown //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Expected O, but got Unknown //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Expected O, but got Unknown //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Expected O, but got Unknown //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Expected O, but got Unknown //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0128: 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) if (_gaveUp != null) { return true; } if ((Object)(object)image == (Object)null) { return false; } try { Texture2D val = ((Il2CppObjectBase)image).TryCast(); if ((Object)(object)val == (Object)null) { GiveUp("a mod drew a " + ((MemberInfo)((Object)image).GetIl2CppType()).Name + " rather than a Texture2D, and the only drawing call left in this build takes a Texture2D"); return true; } if (borderWidths != Vector4.zero && !_saidBorders) { _saidBorders = true; Instance log = _log; if (log != null) { log.Msg("[fix] gui-drawtexture: a mod asked for a border around its texture. The border is not drawn - nothing left in this build can draw one - but the texture is."); } } float num = imageAspect; if (num <= 0f) { int height = image.height; num = ((height > 0) ? ((float)image.width / (float)height) : 1f); } Rect val2 = default(Rect); Rect val3 = default(Rect); if (!GUI.CalculateScaledTextureRects(position, scaleMode, num, ref val2, ref val3)) { return false; } GUIStyle val4 = new GUIStyle(); val4.normal.background = val; val4.border = new RectOffset(0, 0, 0, 0); val4.padding = new RectOffset(0, 0, 0, 0); val4.margin = new RectOffset(0, 0, 0, 0); val4.overflow = new RectOffset(0, 0, 0, 0); Color color = GUI.color; GUI.color = leftColor; try { GUI.Box(val2, GUIContent.none, val4); } finally { GUI.color = color; } } catch (Exception ex) { GiveUp("drawing through GUI.Box failed: " + ex.GetType().Name + ": " + ex.Message); return true; } return false; } private static void GiveUp(string reason) { if (_gaveUp == null) { _gaveUp = reason; Instance log = _log; if (log != null) { log.Error("[fix] gui-drawtexture: standing down - " + reason + ". GUI.DrawTexture will throw again from here on, which is what it did before Polyfill. An overlay that is simply missing is harder to report than one that crashes, so this says it rather than hiding it."); } Fixes.Record("gui-drawtexture", "failed: " + reason); } } } internal static class MainSceneLatch { internal const string GameScene = "Main"; private static int _reached; internal static bool Reached => Volatile.Read(in _reached) != 0; internal static void Note(string sceneName) { if (sceneName == "Main") { Volatile.Write(ref _reached, 1); } } } internal sealed class MeetPointsLobbyChat : Fix { internal override string Id => "meetpoints-lobby-chat"; internal override string Mod => "HUB - MeetPoints"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "a client's chosen meeting place reaches the host again, over the lobby chat the game moved to another type"; internal override string StandsDownBecause => "MeetPoints listens for lobby chat on Lobby.OnLobbyChatMessage, which 0.4.6 moved to SteamLobbyService - so the host never hears a client's meeting place and the customer goes to the vanilla one."; internal override bool Apply(Instance log) { //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Expected O, but got Unknown Type type = AccessTools.TypeByName("Il2CppScheduleOne.Networking.Lobby"); Type type2 = AccessTools.TypeByName("Il2CppScheduleOne.Networking.SteamLobbyService"); if (type2 == null) { log.Warning("[fix] meetpoints-lobby-chat: Il2CppScheduleOne.Networking.SteamLobbyService is not on this build, so there is nothing to listen on."); return false; } if (type != null && AccessTools.Method(type, "OnLobbyChatMessage", (Type[])null, (Type[])null) != null) { return false; } MethodInfo methodInfo = Only(type2, "OnLobbyChatMessage"); if (methodInfo == null) { log.Warning("[fix] meetpoints-lobby-chat: SteamLobbyService has no single OnLobbyChatMessage to attach to, so the mod's listener was left alone."); return false; } MethodInfo methodInfo2 = AccessTools.Method("HUB.MeetPoints.Network.MeetPointsLobbyChatPatch:Postfix", (Type[])null, (Type[])null); if (methodInfo2 == null) { log.Msg("[fix] meetpoints-lobby-chat: HUB - MeetPoints is not loaded, so there is no listener to attach. If the file is installed, something is keeping MelonLoader from loading it."); return false; } ParameterInfo[] parameters = methodInfo2.GetParameters(); if (parameters.Length != 1 || methodInfo.GetParameters().Length != 1 || parameters[0].ParameterType != methodInfo.GetParameters()[0].ParameterType) { log.Warning("[fix] meetpoints-lobby-chat: the mod's listener takes " + Describe(parameters) + " and the callback hands out " + Describe(methodInfo.GetParameters()) + ", so it was left alone."); return false; } try { new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { log.Warning("[fix] meetpoints-lobby-chat: could not attach the mod's listener to SteamLobbyService.OnLobbyChatMessage: " + ex.Message); return false; } Fixes.Repaired.Add("HUB.MeetPoints|Il2CppScheduleOne.Networking.Lobby::OnLobbyChatMessage"); log.Msg("[fix] meetpoints-lobby-chat: MeetPoints listens on SteamLobbyService.OnLobbyChatMessage, where 0.4.6 moved the lobby chat callback."); return true; } private static MethodInfo Only(Type type, string name) { MethodInfo methodInfo = null; MethodInfo[] methods = type.GetMethods(AccessTools.all); foreach (MethodInfo methodInfo2 in methods) { if (!(methodInfo2.Name != name) && !(methodInfo2.DeclaringType != type)) { if (methodInfo != null) { return null; } methodInfo = methodInfo2; } } return methodInfo; } private static string Describe(ParameterInfo[] parameters) { if (parameters.Length == 0) { return "nothing"; } List list = new List(parameters.Length); foreach (ParameterInfo parameterInfo in parameters) { list.Add(parameterInfo.ParameterType.Name); } return string.Join(", ", list); } } internal sealed class MoreFootPatrolsOfficerPool : Fix { private sealed class Request { internal IntPtr Route; internal PatrolGroup Group; internal float Speed; internal bool Warp; } private const int ReservedForVanilla = 2; private const float WaitForPoolSeconds = 30f; private static Instance _log; private static readonly List Pending = new List(); private static bool _draining; internal override string Id => "morefootpatrols-officer-pool"; internal override string Mod => "MoreFootPatrols"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "the extra patrol routes are staffed from the police station"; internal override string StandsDownBecause => "More Foot Patrols clones a prefab called PoliceNPC that this build does not offer mods, so every route it tries to staff ends in a NullReferenceException and no patrol is created."; internal override bool Apply(Instance log) { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: 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_008c: Expected O, but got Unknown //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown _log = log; Type type = AccessTools.TypeByName("BogsMod.Core"); if (type == null) { log.Warning("[fix] morefootpatrols-officer-pool: BogsMod.Core is not where it was."); return false; } MethodInfo methodInfo = AccessTools.Method(type, "spawnOfficer", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(type, "LoadPatrolRoutesFromJson", (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { log.Warning("[fix] morefootpatrols-officer-pool: the mod no longer has both spawnOfficer and LoadPatrolRoutesFromJson, so it was left alone."); return false; } Harmony val = new Harmony("doodesch.polyfill.fixes"); val.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(MoreFootPatrolsOfficerPool), "Park", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); val.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(MoreFootPatrolsOfficerPool), "HandOut", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } private static bool Park(PatrolGroup group, float movementSpeedMult, bool warpToStart) { try { if ((Object)(object)((group != null) ? group.Route : null) == (Object)null) { return false; } Pending.Add(new Request { Route = ((Il2CppObjectBase)group.Route).Pointer, Group = group, Speed = movementSpeedMult, Warp = warpToStart }); } catch (Exception ex) { Instance log = _log; if (log != null) { log.Warning("[fix] morefootpatrols-officer-pool: " + ex.Message); } } return false; } private static void HandOut() { if (!_draining && Pending.Count != 0) { _draining = true; MelonCoroutines.Start(WhenTheStationHasOfficers()); } } private static IEnumerator WhenTheStationHasOfficers() { float deadline = Time.realtimeSinceStartup + 30f; int previous = -1; while (Time.realtimeSinceStartup < deadline) { yield return (object)new WaitForSeconds(0.5f); int num = Available(); if (num > 2 && num == previous) { break; } previous = num; } Distribute(); } private static int Available() { try { PoliceStation obj = Station(); return ((obj == null) ? ((int?)null) : obj.OfficerPool?.Count).GetValueOrDefault(); } catch { return 0; } } private static PoliceStation Station() { List policeStations = PoliceStation.PoliceStations; if (policeStations != null && policeStations.Count != 0) { return policeStations[0]; } return null; } private static void Distribute() { _draining = false; try { PoliceStation val = Station(); if ((Object)(object)val == (Object)null) { Instance log = _log; if (log != null) { log.Warning("[fix] morefootpatrols-officer-pool: there is no police station in this scene, so the routes stay empty."); } Pending.Clear(); return; } List> list = new List>(); Dictionary dictionary = new Dictionary(); foreach (Request item in Pending) { if (!dictionary.TryGetValue(item.Route, out var value)) { value = list.Count; dictionary[item.Route] = value; list.Add(new List()); } list[value].Add(item); } Pending.Clear(); int num = 0; HashSet hashSet = new HashSet(); bool flag = true; while (flag) { flag = false; foreach (List item2 in list) { if (item2.Count != 0) { if (val.OfficerPool.Count <= 2) { goto end_IL_0187; } Request request = item2[0]; item2.RemoveAt(0); PoliceOfficer val2 = val.PullOfficer(); if ((Object)(object)val2 == (Object)null) { goto end_IL_0187; } ((NPC)val2).Movement.MoveSpeedMultiplier = request.Speed; val2.StartFootPatrol(request.Group, request.Warp); hashSet.Add(request.Route); num++; flag = true; } } continue; end_IL_0187: break; } int count = hashSet.Count; Instance log2 = _log; if (log2 != null) { log2.Msg($"[fix] morefootpatrols-officer-pool: 'PoliceNPC' is not among the prefabs this build lets a mod spawn, so no officer can be cloned. Staffed {num} patrol slot(s) across {count} route(s) from the police station instead; {val.OfficerPool.Count} officer(s) left for callouts."); } } catch (Exception ex) { Pending.Clear(); Instance log3 = _log; if (log3 != null) { log3.Warning("[fix] morefootpatrols-officer-pool: " + ex.Message); } } } } internal sealed class MoreRealisticSleepingPhoneFonts : Fix { private static readonly Dictionary Fonts = new Dictionary(StringComparer.Ordinal) { ["openSansBold"] = "OpenSans-Bold", ["openSansSemiBold"] = "OpenSans-SemiBold" }; private static Instance _log; private static Type _fontType; internal override string Id => "mrs-phone-fonts"; internal override string Mod => "MoreRealisticSleeping"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "the sleeping app gets the two phone fonts it waits for, instead of waiting for the rest of the session"; internal override string StandsDownBecause => "More Realistic Sleeping reads a font off a delivery-shop row that 0.4.6 moved and leaves switched off - so its font loader throws and the app waits for a font that never loads."; internal override bool Apply(Instance log) { //IL_0058: 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_007c: Expected O, but got Unknown _log = log; MethodInfo methodInfo = AccessTools.Method("MoreRealisticSleeping.Util.FontLoader:FindFontFromOtherApp", (Type[])null, (Type[])null); if (methodInfo == null) { log.Msg("[fix] mrs-phone-fonts: More Realistic Sleeping is not loaded, or does not have FontLoader.FindFontFromOtherApp - nothing was changed."); return false; } _fontType = AccessTools.TypeByName("UnityEngine.Font"); if (_fontType == null) { log.Warning("[fix] mrs-phone-fonts: UnityEngine.Font is not resolvable here, so the mod's fonts were left alone."); return false; } try { new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(MoreRealisticSleepingPhoneFonts), "Instead", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { log.Warning("[fix] mrs-phone-fonts: could not replace FontLoader.FindFontFromOtherApp, so the app still waits for its fonts: " + ex.Message); return false; } log.Msg("[fix] mrs-phone-fonts: More Realistic Sleeping asks the game for OpenSans-Bold and OpenSans-SemiBold by name, not by walking a screen that has been rearranged."); if ((Object)(object)Named("OpenSans-Bold") == (Object)null) { return true; } Prime(log); return true; } private static void Prime(Instance log) { MethodInfo methodInfo = AccessTools.Method("MoreRealisticSleeping.Util.FontLoader:FindFontFromOtherApp", (Type[])null, (Type[])null); FieldInfo fieldInfo = AccessTools.Field("MoreRealisticSleeping.Util.FontLoader:openSansBold"); FieldInfo fieldInfo2 = AccessTools.Field("MoreRealisticSleeping.Util.FontLoader:openSansSemiBold"); FieldInfo fieldInfo3 = AccessTools.Field("MoreRealisticSleeping.Util.FontLoader:openSansBoldIsInitialized"); FieldInfo fieldInfo4 = AccessTools.Field("MoreRealisticSleeping.Util.FontLoader:openSansSemiBoldIsInitialized"); if (methodInfo == null || fieldInfo == null || fieldInfo2 == null || fieldInfo3 == null || fieldInfo4 == null) { log.Warning("[fix] mrs-phone-fonts: FontLoader is not the shape this reads, so the fonts were not loaded again - the app will only open after a restart."); return; } try { fieldInfo.SetValue(null, methodInfo.Invoke(null, new object[1] { "openSansBold" })); fieldInfo2.SetValue(null, methodInfo.Invoke(null, new object[1] { "openSansSemiBold" })); } catch (Exception ex) { log.Warning("[fix] mrs-phone-fonts: loading the fonts again failed, so the app keeps waiting: " + (ex.InnerException ?? ex).Message); return; } object value = fieldInfo3.GetValue(null); if (value is bool && (bool)value) { value = fieldInfo4.GetValue(null); if (value is bool && (bool)value) { log.Msg("[fix] mrs-phone-fonts: both phone fonts are loaded; the app is no longer waiting."); return; } } log.Warning("[fix] mrs-phone-fonts: the phone fonts did not load, so the app keeps waiting for them."); } private static bool Instead(string fontName, ref Font __result) { if (fontName == null || !Fonts.TryGetValue(fontName, out var value)) { return true; } __result = Named(value); if ((Object)(object)__result == (Object)null) { _log.Warning("[fix] mrs-phone-fonts: " + value + " is not loaded, so " + fontName + " stays unset and the app keeps waiting."); return false; } FieldInfo fieldInfo = AccessTools.Field("MoreRealisticSleeping.Util.FontLoader:" + fontName + "IsInitialized"); if (fieldInfo == null) { _log.Warning("[fix] mrs-phone-fonts: FontLoader has no " + fontName + "IsInitialized, so the font was handed back but the app will keep waiting."); return false; } fieldInfo.SetValue(null, true); return false; } private static Font Named(string name) { Il2CppReferenceArray val = Resources.FindObjectsOfTypeAll(Il2CppType.From(_fontType)); if (val == null) { return null; } foreach (Object item in (Il2CppArrayBase)(object)val) { if (!(item == (Object)null) && !(item.name != name)) { Font val2 = ((Il2CppObjectBase)item).TryCast(); if ((Object)(object)val2 != (Object)null) { return val2; } } } return null; } } internal sealed class MulesPrefsReloadOnAWorker : Fix { private static Instance _log; internal override bool Early => true; internal override string Id => "mules-prefs-reload-on-a-worker"; internal override string Mod => "Mules"; internal override string ModVersions => "0.2.1"; internal override string GameVersions => ">=0.4.6"; internal override string What => "Mules re-reads the settings file on a background thread the moment anything writes it, and during startup that runs other mods' settings code off the main thread, which takes the game down with no error at all."; internal override string StandsDownBecause => "It guards one exact version of Mules, because the guard is only complete while every one of its file-watcher callbacks still goes through the same method."; internal override bool Apply(Instance log) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown _log = log; Type type = AccessTools.TypeByName("Mules.Plugin"); if (type == null) { log.Msg("[fix] mules-prefs-reload-on-a-worker: Mules is not loaded, so there is nothing to guard."); return false; } MethodInfo methodInfo = AccessTools.Method(type, "SchedulePrefsReload", Type.EmptyTypes, (Type[])null); if (methodInfo == null) { log.Warning("[fix] mules-prefs-reload-on-a-worker: Mules.Plugin has no no-argument SchedulePrefsReload on this build, so it was left alone."); return false; } try { new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(MulesPrefsReloadOnAWorker), "NotBeforeTheGameIsUp", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { log.Warning("[fix] mules-prefs-reload-on-a-worker: could not guard Mules.Plugin.SchedulePrefsReload: " + ex.Message); return false; } log.Msg("[fix] mules-prefs-reload-on-a-worker: Mules waits for the game before it starts re-reading the settings file, which took the game down during startup."); return true; } private static bool NotBeforeTheGameIsUp() { return MainSceneLatch.Reached; } } internal sealed class OverTheCounterAdoptsOnlyClones : Fix { private static Instance _log; private static int _refused; internal override string Id => "otc-adopts-only-clones"; internal override string Mod => "OverTheCounter"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "a manager slot can no longer turn a townsperson into the manager"; internal override string StandsDownBecause => "an OverTheCounter manager can become a named character - their face, their name and their inventory - and a save later there is no way back."; internal override bool Apply(Instance log) { _log = log; if (0 + Guard("OverTheCounter.Logic.ManagerInstance", "FindNetworkNpc", log) + Guard("OverTheCounter.Logic.DrifterManager", "FindNetworkDrifter", log) == 0) { log.Warning("[fix] " + Id + ": neither lookup is where this build of OverTheCounter keeps it, so a stale slot can still adopt a townsperson."); return false; } return true; } private static int Guard(string typeName, string methodName, Instance log) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown Type type = AccessTools.TypeByName(typeName); MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, methodName, (Type[])null, (Type[])null)); if (methodInfo == null || !typeof(NPC).IsAssignableFrom(methodInfo.ReturnType)) { return 0; } new Harmony("doodesch.polyfill.otcadoption").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(OverTheCounterAdoptsOnlyClones), "NotAPlacedNpc", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); log.Msg($"[fix] otc-adopts-only-clones: {typeName}.{methodName} now refuses a placed NPC."); return 1; } private static void NotAPlacedNpc(ref NPC __result) { if ((Object)(object)__result == (Object)null) { return; } try { NetworkObject component = ((Component)__result).gameObject.GetComponent(); if ((Object)(object)component == (Object)null || !component.IsSceneObject) { return; } string text = null; try { NPCData nPCData = __result.NPCData; object obj; if (nPCData == null) { obj = null; } else { BasicInfo basicInfo = nPCData.BasicInfo; obj = ((basicInfo != null) ? basicInfo.ID : null); } text = (string)obj; } catch { } if (_refused++ == 0) { Instance log = _log; if (log != null) { log.Warning("[fix] otc-adopts-only-clones: a manager slot pointed at " + (string.IsNullOrEmpty(text) ? "a character placed in the world" : text) + ", who was placed in the world rather than spawned as a manager. The adoption was refused - the manager stays missing until the right slot arrives, which is the recoverable half of this."); } } __result = null; } catch (Exception ex) { if (_refused++ == 0) { Instance log2 = _log; if (log2 != null) { log2.Warning("[fix] otc-adopts-only-clones: could not tell a placed NPC from a spawned one: " + ex.Message); } } } } } internal sealed class OverTheCounterButtonCodes : Fix { internal override string Id => "otc-button-codes"; internal override string Mod => "OverTheCounter"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "the clipboard answers the interact key, and the route picker stops closing itself on it"; internal override string StandsDownBecause => "The manager clipboard answers a key nobody presses, and the route picker closes without choosing on the interact key - which is the key meant to choose with."; internal override bool Apply(Instance log) { return ButtonCodeShift.Apply(log, Id, "OverTheCounter.Il2Cpp", ("OverTheCounter.UI.RouteEntitySelector", "Tick"), ("OverTheCounter.Patches.ManagerClipboardPatch", "UpdatePrefix")) > 0; } } internal sealed class OverTheCounterClipboard : Fix { internal override string Id => "otc-clipboard"; internal override string Mod => "OverTheCounter"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "the management clipboard stops being dead in your hands"; internal override string StandsDownBecause => "The management clipboard will not react at all while OverTheCounter is installed."; internal override bool Apply(Instance log) { //IL_00d9: Unknown result type (might be due to invalid IL or missing references) if (AccessTools.Method(typeof(ManagementInterface), "get_NPCSelector", (Type[])null, (Type[])null) != null) { log.Msg("[fix] otc-clipboard: not needed - the NPC selector screen answers null now, so the mod's own clipboard patch runs and keeps its manager panel."); return false; } Type type = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetType("OverTheCounter.Patches.ManagerClipboardPatch", throwOnError: false); } catch { } if (type != null) { break; } } if (type == null) { log.Warning("[fix] otc-clipboard: ManagerClipboardPatch is not where it was."); return false; } MethodInfo methodInfo = AccessTools.Method(type, "UpdatePrefix", (Type[])null, (Type[])null); if (methodInfo == null) { log.Warning("[fix] otc-clipboard: UpdatePrefix is gone."); return false; } MethodInfo methodInfo2 = AccessTools.Method(typeof(ManagementClipboard_Equippable), "Update", (Type[])null, (Type[])null); if (methodInfo2 == null) { log.Warning("[fix] otc-clipboard: the clipboard's Update is not where it was."); return false; } try { new Harmony("doodesch.polyfill.fixes").Unpatch((MethodBase)methodInfo2, methodInfo); } catch (Exception ex) { log.Warning("[fix] otc-clipboard: could not take the patch off: " + ex.Message); return false; } log.Warning("[fix] otc-clipboard: OverTheCounter's clipboard patch asks for the NPC selector screen, which 0.4.6 removed and did not replace. The patch cannot run at all, so it has been taken off: the clipboard works as the game's own again, and OverTheCounter's manager selection stays gone."); return true; } } internal sealed class OverTheCounterDrifterPrefab : Fix { private static Instance _log; private static readonly List _pool = new List(); private static readonly List _baked = new List(); private static int _next; private static bool _searched; private static bool _said; private static int _handed; internal override string Id => "otc-drifter-prefab"; internal override string Mod => "OverTheCounter"; internal override string ModVersions => "2.0.10"; internal override string GameVersions => "*"; internal override string What => "the drifters and the managers stop being cloned out of an employee and throwing every tick"; internal override string StandsDownBecause => "OverTheCounter's drifters and hired managers may be cloned from an employee prefab, which throws a NullReferenceException in Employee.UpdateBehaviour on every tick."; internal override bool Apply(Instance log) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown _log = log; int num = 0; string[] array = new string[2] { "OverTheCounter.Logic.NpcSpawner", "OverTheCounter.Logic.ManagerSpawner" }; for (int i = 0; i < array.Length; i++) { Type type = Find(array[i]); if (!(type == null)) { MethodInfo methodInfo = AccessTools.Method(type, "GetBasePrefab", (Type[])null, (Type[])null); if (!(methodInfo == null)) { new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(OverTheCounterDrifterPrefab), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); num++; } } } if (num == 0) { log.Warning("[fix] otc-drifter-prefab: neither spawner has a GetBasePrefab here."); return false; } log.Msg($"[fix] otc-drifter-prefab: watching {num} prefab search(es) - the drifters and, " + "when you hire one, the manager."); return true; } private static void Postfix(ref NetworkObject __result) { if ((Object)(object)__result == (Object)null) { return; } GameObject val = null; try { val = ((Component)__result).gameObject; } catch { } if ((Object)(object)val == (Object)null) { return; } Employee val2 = null; try { val2 = val.GetComponent(); } catch { } if ((Object)(object)val2 == (Object)null) { return; } NetworkObject val3 = Replacement(); if ((Object)(object)val3 == (Object)null) { Say("[fix] otc-drifter-prefab: OverTheCounter fell back to the employee prefab '" + ((Object)val).name + "', and this build has no spawnable NPC that is not an employee. Left alone; the drifters will keep throwing in Employee.UpdateBehaviour."); return; } Say($"[fix] otc-drifter-prefab: OverTheCounter asked for 'CivilianNPC', which this build does not have, and fell back to '{((Object)val).name}' - an employee. Handed it one of {_pool.Count} spawnable NPC prefab(s) instead, a different one each time: {Names()}."); if (_handed < 8) { _handed++; try { Instance log = _log; if (log != null) { log.Msg($"[fix] otc-drifter-prefab: handover {_handed} -> {((Object)((Component)val3).gameObject).name}" + ((_handed == 8) ? " (further handovers are not logged)" : "")); } } catch { } } __result = val3; } private static NetworkObject Replacement() { if (!_searched) { _searched = true; Gather(); } if (_pool.Count == 0) { return null; } return _pool[_next++ % _pool.Count]; } private static void Gather() { try { NetworkManager networkManager = InstanceFinder.NetworkManager; PrefabObjects val = ((networkManager != null) ? networkManager.SpawnablePrefabs : null); if ((Object)(object)val == (Object)null) { return; } int objectCount = val.GetObjectCount(); for (int i = 0; i < objectCount; i++) { NetworkObject val2 = val.GetObject(true, i); GameObject val3 = null; try { val3 = ((val2 != null) ? ((Component)val2).gameObject : null); } catch { } if ((Object)(object)val3 == (Object)null) { continue; } NPC val4 = null; Employee val5 = null; try { val4 = val3.GetComponent(); val5 = val3.GetComponent(); } catch { } if ((Object)(object)val4 == (Object)null || (Object)(object)val5 != (Object)null) { continue; } try { string bakedGUID = val4.BakedGUID; if (!string.IsNullOrEmpty(bakedGUID)) { Instance log = _log; if (log != null) { log.Warning($"[fix] otc-drifter-prefab: '{((Object)val3).name}' carries a baked GUID ({bakedGUID}); every copy of it claims that id and displaces " + "whoever held it."); } } } catch { } if (Baked(val4)) { _baked.Add(val2); } else { _pool.Add(val2); } } if (_pool.Count == 0) { _pool.AddRange(_baked); } else if (_baked.Count > 0) { Instance log2 = _log; if (log2 != null) { log2.Msg($"[fix] otc-drifter-prefab: skipped {_baked.Count} prefab(s) whose body is a " + "single baked layer - a customer cloned from one keeps that body whatever OverTheCounter randomises on top."); } } } catch (Exception ex) { Instance log3 = _log; if (log3 != null) { log3.Warning("[fix] otc-drifter-prefab: " + ex.Message); } } } private static bool Baked(NPC npc) { try { Avatar avatar = npc.Avatar; AvatarSettings val = ((avatar != null) ? avatar.CurrentSettings : null); return (Object)(object)val != (Object)null && val.UseCombinedLayer && (Object)(object)val.CombinedLayer != (Object)null; } catch { return false; } } private static string Names() { List list = new List(); foreach (NetworkObject item in _pool) { try { list.Add(((Object)((Component)item).gameObject).name); } catch { } if (list.Count == 8) { list.Add("..."); break; } } return string.Join(", ", list); } private static void Say(string line) { if (!_said) { _said = true; Instance log = _log; if (log != null) { log.Msg(line); } } } private static Type Find(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type type = null; try { type = assembly.GetType(fullName, throwOnError: false); } catch { } if (type != null) { return type; } } return null; } } internal sealed class OverTheCounterHandover : Fix { private static Instance _log; private static bool _said; internal override string Id => "otc-handover-customer"; internal override string Mod => "OverTheCounter"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "handing goods to the mod's own customer opens the offer screen again"; internal override string StandsDownBecause => "Bella's handover screen will open with no options in it, whatever you are carrying."; internal override bool Apply(Instance log) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown _log = log; MethodInfo methodInfo = AccessTools.Method(typeof(HandoverScreen), "Open", (Type[])null, (Type[])null); if (methodInfo == null) { log.Warning("[fix] otc-handover-customer: HandoverScreen.Open is not where it was."); return false; } new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(OverTheCounterHandover), "OpenPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } private static void OpenPrefix(ref Customer customer) { try { if (CanDraw(customer)) { return; } Customer val = WithData(customer); if ((Object)(object)val == (Object)null) { if (!_said) { _said = true; Instance log = _log; if (log != null) { log.Warning("[fix] otc-handover-customer: this save has no customer with data to show the offer screen with, so it stays empty."); } } return; } customer = val; if (!_said) { _said = true; Instance log2 = _log; if (log2 != null) { log2.Msg("[fix] otc-handover-customer: the offer screen was handed a customer with no data and could not draw itself. Showing another customer's preferences instead - what you hand over is still judged by the mod."); } } } catch (Exception ex) { Instance log3 = _log; if (log3 != null) { log3.Warning("[fix] otc-handover-customer: " + ex.Message); } } } private static Customer WithData(Customer avoid) { List[] array = new List[2] { Customer.UnlockedCustomers, Customer.LockedCustomers }; foreach (List val in array) { if (val == null) { continue; } for (int j = 0; j < val.Count; j++) { Customer val2 = val[j]; try { if ((Object)(object)val2 == (Object)null || (Object)(object)val2 == (Object)(object)avoid || !CanDraw(val2)) { continue; } return val2; } catch { } } } return null; } private static bool CanDraw(Customer customer) { try { if ((Object)(object)customer == (Object)null) { return false; } if ((Object)(object)customer.NPC == (Object)null || customer.NPC.RelationData == null) { return false; } if ((Object)(object)customer.CustomerData == (Object)null) { return false; } if (customer.CustomerData.PreferredProperties == null) { return false; } List orderedDrugTypes = customer.GetOrderedDrugTypes(); return orderedDrugTypes != null && orderedDrugTypes.Count > 0; } catch { return false; } } } internal sealed class OverTheCounterNetworkLib : Fix { private static readonly (string Type, string[] Members)[] Needed = new(string, string[])[2] { ("SteamNetworkLib.Sync.HostSyncVar`1", new string[5] { "set_Value", "Refresh", "add_OnValueChanged", "add_OnSyncError", "add_OnWriteIgnored" }), ("SteamNetworkLib.Sync.ClientSyncVar`1", new string[5] { "set_Value", "Refresh", "GetAllValues", "add_OnValueChanged", "add_OnSyncError" }) }; internal override string Id => "otc-networklib-version"; internal override string Mod => "OverTheCounter"; internal override string ModVersions => "2.0.10"; internal override string GameVersions => "*"; internal override string What => "multiplayer sync stops being switched off by a version number alone"; internal override bool Apply(Instance log) { Type type = Find("OverTheCounter.SaveData.ConfigSyncData"); if (type == null) { log.Warning("[fix] otc-networklib-version: ConfigSyncData is not where it was."); return false; } FieldInfo fieldInfo = AccessTools.Field(type, "_networkLibAvailable"); FieldInfo fieldInfo2 = AccessTools.Field(type, "RequiredSteamNetworkLibVersion"); if (fieldInfo == null || fieldInfo2 == null) { log.Warning("[fix] otc-networklib-version: the version gate is not where it was."); return false; } Assembly assembly = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly2 in assemblies) { string text = null; try { text = assembly2.GetName().Name; } catch { } if (text != null && text.Contains("SteamNetworkLib")) { assembly = assembly2; break; } } if (assembly == null) { return false; } Version version = assembly.GetName().Version; Version version2 = fieldInfo2.GetValue(null) as Version; if (version == null || version2 == null) { return false; } if (version == version2) { return false; } if (version < version2) { log.Warning($"[fix] otc-networklib-version: SteamNetworkLib {version} is OLDER than the {version2} the mod was built against. Left switched off."); return false; } string text2 = FirstMissing(assembly); if (text2 != null) { log.Warning($"[fix] otc-networklib-version: SteamNetworkLib {version} has no {text2}, " + "which the mod calls. Left switched off."); return false; } try { fieldInfo.SetValue(null, true); } catch (Exception ex) { log.Warning("[fix] otc-networklib-version: could not open the gate: " + ex.Message); return false; } log.Msg($"[fix] otc-networklib-version: SteamNetworkLib {version} carries everything the mod asks of {version2}, so its sync is on again. The mod's own \"multiplayer sync " + "disabled\" line further up was written before this and no longer holds."); return true; } private static string FirstMissing(Assembly library) { (string, string[])[] needed = Needed; for (int i = 0; i < needed.Length; i++) { (string, string[]) tuple = needed[i]; string item = tuple.Item1; string[] item2 = tuple.Item2; Type type = null; try { type = library.GetType(item, throwOnError: false); } catch { } if (type == null) { return item; } string[] array = item2; foreach (string text in array) { bool flag = false; try { flag = type.GetMethod(text, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) != null; } catch { } if (!flag) { return item + "::" + text; } } } return null; } private static Type Find(string fullName) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type type = null; try { type = assembly.GetType(fullName, throwOnError: false); } catch { } if (type != null) { return type; } } return null; } } internal sealed class OverTheCounterSmartFill : Fix { private static Instance _log; private static MethodInfo _obsolete; private static int _removed; internal override string Id => "otc-smart-fill-tracking"; internal override string Mod => "OverTheCounter"; internal override string ModVersions => "*"; internal override string GameVersions => ">=0.4.6"; internal override string What => "Smart Fill counts the items it puts in a handover, so the same stack is not left behind in your hotbar as well"; internal override string StandsDownBecause => "Smart Fill tells the handover screen where a copied item came from, and 0.4.6 removed the list it wrote to - so the placing throws half-way, the count stays at zero and the source stack is never taken."; internal override bool Apply(Instance log) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Expected O, but got Unknown _log = log; _obsolete = AccessTools.Method("OverTheCounter.PrivateAccess:TrackItemAsPlayer", (Type[])null, (Type[])null); MethodInfo methodInfo = AccessTools.Method("OverTheCounter.UI.HandoverFillUI:TryPlaceInCustomerSlots", (Type[])null, (Type[])null); if (_obsolete == null || methodInfo == null) { log.Msg("[fix] otc-smart-fill-tracking: OverTheCounter's Smart Fill is not here in the shape this reads, so nothing was changed."); return false; } _removed = 0; try { new Harmony("doodesch.polyfill.fixes").Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(OverTheCounterSmartFill), "Without", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception ex) { log.Warning("[fix] otc-smart-fill-tracking: could not rewrite Smart Fill, so it still stops half-way: " + ex.Message); return false; } if (_removed != 1) { log.Warning($"[fix] otc-smart-fill-tracking: TryPlaceInCustomerSlots calls TrackItemAsPlayer {_removed} time(s) where this expected exactly one, so " + "Smart Fill was left as it was."); return false; } log.Msg("[fix] otc-smart-fill-tracking: Smart Fill no longer writes to the handover list 0.4.6 removed, so it counts what it placed and takes it off your stack."); return true; } private static IEnumerable Without(IEnumerable instructions) { foreach (CodeInstruction instruction in instructions) { if (_obsolete == null || !CodeInstructionExtensions.Calls(instruction, _obsolete)) { yield return instruction; continue; } _removed++; yield return CodeInstructionExtensions.WithBlocks(CodeInstructionExtensions.WithLabels(new CodeInstruction(OpCodes.Pop, (object)null), (IEnumerable