using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; using Archipelago.MultiClient.Net; using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.Models; using HarmonyLib; using InputSystem; using MelonLoader; using MelonLoader.Preferences; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SuperhotArchipelago.Core; using SuperhotArchipelago.Patches; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Mod), "SuperhotArchipelago", "0.2.2", "My-Key-Morris", null)] [assembly: MelonGame("SUPERHOT_Team", "SUPERHOT")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("SuperhotArchipelago")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+ac2738bb885f50d160aa9e05b060e49b13323e18")] [assembly: AssemblyProduct("SuperhotArchipelago")] [assembly: AssemblyTitle("SuperhotArchipelago")] [assembly: AssemblyVersion("1.0.0.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 SuperhotArchipelago.Patches { [HarmonyPatch(typeof(piOsMenu), "CreateViewFromNode", new Type[] { typeof(XElement), typeof(List), typeof(List), typeof(float), typeof(bool) })] public static class ArchipelagoFolderButtonPatch { private const string ButtonLabel = "ARCHIPELAGO"; private const string SettingsButtonPrefix = "SETTINGS"; public static void Postfix(SHGUIcommanderview ___createdView) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) if (___createdView != null && ___createdView.isRoot) { SHGUIcommanderbutton val = new SHGUIcommanderbutton("ARCHIPELAGO".PadRight(12, ' ') + "│>FOLDER<", 'w', (Action)delegate { OpenFolder(); }).SetListLink(___createdView).SetData("Connect to a server, toggle Archipelago mode, view your check/item history, or configure notification settings."); ___createdView.AddButtonView(val); InsertAboveSettings(___createdView); } } private static void InsertAboveSettings(SHGUIcommanderview view) { List buttons = view.buttons; int num = buttons.Count - 1; int num2 = buttons.FindIndex((SHGUIcommanderbutton b) => b.ButtonText.StartsWith("SETTINGS", StringComparison.Ordinal)); if (num2 >= 0 && num2 < num) { SHGUIcommanderbutton item = buttons[num]; buttons.RemoveAt(num); buttons.Insert(num2, item); for (int num3 = 0; num3 < buttons.Count; num3++) { ((SHGUIview)buttons[num3]).y = num3 + 1; } } } private static void OpenFolder() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown //IL_003e: Unknown result type (might be due to invalid IL or missing references) SHGUIcommanderview val = new SHGUIcommanderview { isRoot = false, path = "C:\\ARCHIPELAGO\\" }; SHGUIcommanderbutton val2 = new SHGUIcommanderbutton("/.. │", 'w', (Action)delegate { SHGUI.current.PopView(); }).SetListLink(val).SetData("Go back."); val.AddButtonView(val2); ConnectionButtonPatch.AddTo(val); ArchipelagoModeTogglePatch.AddTo(val); ArchipelagoLogButtonPatch.AddTo(val); NotificationSettingsButtonPatch.AddTo(val); SHGUI.current.AddViewOnTop((SHGUIview)(object)val); } } public static class ArchipelagoLogButtonPatch { private const string ButtonLabel = "AP LOG"; internal static void AddTo(SHGUIcommanderview view) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) SHGUIcommanderbutton val = new SHGUIcommanderbutton("AP LOG".PadRight(12, ' ') + "│VIEW", 'w', (Action)delegate { SHGUI.current.AddViewOnTop((SHGUIview)(object)new ArchipelagoLogApp()); }).SetListLink(view).SetData("See a history of items you've received and checks you've sent this run."); view.AddButtonView(val); } } public static class ArchipelagoModeTogglePatch { private const string ButtonLabel = "AP MODE"; private static bool? _lastEnabled; internal static SHGUIcommanderbutton? Button { get; private set; } internal static void AddTo(SHGUIcommanderview view) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) SHGUIcommanderbutton val = new SHGUIcommanderbutton(BuildLabel(), 'w', (Action)delegate { Mod.SetEnabled(!Config.Enabled.Value); }).SetListLink(view).SetData("Turn Archipelago mode on or off. Off plays SUPERHOT exactly like vanilla -- no level gating, no hub overlay -- and drops any active connection. Turning back on reconnects and picks up right where you left off."); view.AddButtonView(val); Button = val; _lastEnabled = null; RefreshLabel(); } internal static void RefreshLabel() { if (Button != null) { bool value = Config.Enabled.Value; if (value != _lastEnabled) { _lastEnabled = value; Button.ButtonText = BuildLabel(); Button.RefreshText(); } } } private static string BuildLabel() { string text = (Config.Enabled.Value ? "ON" : "OFF"); return "AP MODE".PadRight(12, ' ') + "│" + text; } } internal static class AutoTransitionCheck { public static void ReportCompletionIfFirstCall(bool alreadyStarted) { if (!alreadyStarted && Mod.IsEnabled) { LevelInfo currentLevelInfo = LevelSetup.CurrentLevelInfo; if (currentLevelInfo != null) { Mod.Locations?.CheckLocation(currentLevelInfo.ID); } } } public static bool TryBlockAndRedirect() { LevelInfo nextLevelInfo = LevelSetup.GetNextLevelInfo(); if (!LevelAccessGuard.ShouldBlock(nextLevelInfo, out string blockMessage)) { return false; } Instance? log = Mod.Log; if (log != null) { log.Msg("Blocked auto-transition to '" + nextLevelInfo.SceneFileName + "' before any static/camera transition started -- " + blockMessage + ". Returning to hub."); } if ((Object)(object)SHGUI.current != (Object)null) { SHGUI.current.LaunchLevelAppTunnels("SHMenu", false, 1f); } return true; } } [HarmonyPatch(typeof(LevelFlowControl), "LoadNextLevel")] public static class AutoTransitionCheckPatch_LoadNextLevel { public static bool Prefix(ref bool ___loadedNextLevel, ref Status __result) { AutoTransitionCheck.ReportCompletionIfFirstCall(___loadedNextLevel); if (___loadedNextLevel || !AutoTransitionCheck.TryBlockAndRedirect()) { return true; } ___loadedNextLevel = true; __result = (Status)1; return false; } } [HarmonyPatch(typeof(LevelFlowControl), "LoadNextLevelWithTunnels")] public static class AutoTransitionCheckPatch_LoadNextLevelWithTunnels { public static bool Prefix(ref bool ___loadedNextLevel, ref Status __result) { AutoTransitionCheck.ReportCompletionIfFirstCall(___loadedNextLevel); if (___loadedNextLevel || !AutoTransitionCheck.TryBlockAndRedirect()) { return true; } ___loadedNextLevel = true; __result = (Status)1; return false; } } [HarmonyPatch(typeof(LevelFlowControl), "LoadNextLevelWithTunnelsWithoutScramble")] public static class AutoTransitionCheckPatch_LoadNextLevelWithTunnelsWithoutScramble { public static bool Prefix(ref bool ___loadedNextLevel, ref Status __result) { AutoTransitionCheck.ReportCompletionIfFirstCall(___loadedNextLevel); if (___loadedNextLevel || !AutoTransitionCheck.TryBlockAndRedirect()) { return true; } ___loadedNextLevel = true; __result = (Status)1; return false; } } [HarmonyPatch(typeof(LevelFlowControl), "LoadNextLevelWithTunnelsWithScrambleOnEnd")] public static class AutoTransitionCheckPatch_LoadNextLevelWithTunnelsWithScrambleOnEnd { public static bool Prefix(ref bool ___loadedNextLevel, ref Status __result) { AutoTransitionCheck.ReportCompletionIfFirstCall(___loadedNextLevel); if (___loadedNextLevel || !AutoTransitionCheck.TryBlockAndRedirect()) { return true; } ___loadedNextLevel = true; __result = (Status)1; return false; } } [HarmonyPatch(typeof(LevelFlowControl), "LoadNextLevelInstant")] public static class AutoTransitionCheckPatch_LoadNextLevelInstant { public static bool Prefix() { AutoTransitionCheck.ReportCompletionIfFirstCall(alreadyStarted: false); return !AutoTransitionCheck.TryBlockAndRedirect(); } } public static class ConnectionButtonPatch { private const string ButtonLabel = "CONNECT"; private static string? _lastStatus; internal static SHGUIcommanderbutton? Button { get; private set; } internal static void AddTo(SHGUIcommanderview view) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) SHGUIcommanderbutton val = new SHGUIcommanderbutton("CONNECT".PadRight(12, ' ') + "│OFFLINE", 'w', (Action)delegate { SHGUI.current.AddViewOnTop((SHGUIview)(object)new ArchipelagoConnectApp()); }).SetListLink(view).SetData("Set up or check your Archipelago server connection."); view.AddButtonView(val); Button = val; _lastStatus = null; RefreshLabel(); } internal static void RefreshLabel() { if (Button != null) { ArchipelagoConnection? connection = Mod.Connection; string text = ((connection != null && connection.IsConnected) ? "ONLINE" : "OFFLINE"); if (!(text == _lastStatus)) { _lastStatus = text; Button.ButtonText = "CONNECT".PadRight(12, ' ') + "│" + text; Button.RefreshText(); } } } } [HarmonyPatch(typeof(LevelSetup), "LoadNextLevel")] public static class DirectLevelSkipPatch { public static bool Prefix() { LevelInfo nextLevelInfo = LevelSetup.GetNextLevelInfo(); if (!LevelAccessGuard.ShouldBlock(nextLevelInfo, out string blockMessage)) { return true; } Instance? log = Mod.Log; if (log != null) { log.Msg("Blocked direct-continue to '" + nextLevelInfo.SceneFileName + "' -- " + blockMessage + ". Returning to hub instead of leaving the player stuck."); } if ((Object)(object)SHGUI.current != (Object)null) { SHGUI.current.LaunchLevelAppTunnels("SHMenu", false, 1f); } return false; } } internal static class EndingClickBuffer { public static void SuppressIfWithinBuffer(ref InputData inputData, bool endingStarted, float endingStartedTime) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (endingStarted && Time.realtimeSinceStartup - endingStartedTime < Config.EndingClickBufferSeconds.Value) { inputData.skipButton = (ButtonState)1; } } } [HarmonyPatch(typeof(LevelFlowControl), "SuperHotSuperHotEnding")] public static class EndingClickBufferPatch_Ending { public static void Prefix(ref InputData ___inputData, bool ___SuperHotEndingStarted, float ___SuperHotEndingStartedTime) { EndingClickBuffer.SuppressIfWithinBuffer(ref ___inputData, ___SuperHotEndingStarted, ___SuperHotEndingStartedTime); } } [HarmonyPatch(typeof(LevelFlowControl), "SuperHotSuperHotEndingClickThrough")] public static class EndingClickBufferPatch_EndingClickThrough { public static void Prefix(ref InputData ___inputData, bool ___SuperHotEndingStarted, float ___SuperHotEndingStartedTime) { EndingClickBuffer.SuppressIfWithinBuffer(ref ___inputData, ___SuperHotEndingStarted, ___SuperHotEndingStartedTime); } } [HarmonyPatch(typeof(piOsMenu), "LockUnfinishedLevels")] public static class HubUnlockPatch { private static readonly FieldInfo RecursiveColorSetField = AccessTools.Field(typeof(SHGUIcommanderbutton), "recursiveColorSet"); public static void Postfix() { try { RunPostfixBody(); } catch (Exception arg) { Instance? log = Mod.Log; if (log != null) { log.Error($"HubUnlockPatch.Postfix threw: {arg}"); } } } private static void RunPostfixBody() { if (!Mod.IsEnabled || (Object)(object)SHGUI.current == (Object)null) { return; } SHGUIview interactableView = SHGUI.current.GetInteractableView(); SHGUIcommanderview val = (SHGUIcommanderview)(object)((interactableView is SHGUIcommanderview) ? interactableView : null); if (val == null || val.buttons == null) { return; } foreach (SHGUIcommanderbutton button in val.buttons) { if (button == null || button.LevelToBeLoaded == "") { continue; } LevelInfo levelInfoByUniqueSHName = LevelSetup.GetLevelInfoByUniqueSHName(button.LevelToBeLoadedName); if (string.IsNullOrEmpty(levelInfoByUniqueSHName.SceneFileName) || !LevelCatalog.LevelIdToLevel.TryGetValue(levelInfoByUniqueSHName.ID, out LevelEntry value)) { continue; } bool flag = Mod.Connection != null && Mod.Connection.IsLevelExcluded(value.Order); if (value.HasSecret) { if (flag) { button.AddScrollingNotification("INACTIVE".PadRight(8), false); string text = LocalizationAccessHelperExtensions.T("MENU_SECRETNOTCRACKED", Array.Empty()); string text2 = LocalizationAccessHelperExtensions.T("MENU_SECRETCRACKED", Array.Empty()); if (button.data.Contains(text2)) { button.data = button.data.Replace(text2, "INACTIVE"); } else if (button.data.Contains(text)) { button.data = button.data.Replace(text, "INACTIVE"); } } else { bool flag2 = Mod.Locations?.IsSecretCompleted(levelInfoByUniqueSHName.ID) ?? false; if (flag2) { button.AddScrollingNotification(LocalizationAccessHelperExtensions.T("MENU_CRACKED8CHARS", Array.Empty()).PadRight(8), false); } else { button.RemoveScrollingNotification(); } string text3 = LocalizationAccessHelperExtensions.T("MENU_SECRETNOTCRACKED", Array.Empty()); string text4 = LocalizationAccessHelperExtensions.T("MENU_SECRETCRACKED", Array.Empty()); if (flag2 && button.data.Contains(text3)) { button.data = button.data.Replace(text3, text4); } else if (!flag2 && button.data.Contains(text4)) { button.data = button.data.Replace(text4, text3); } } } bool flag3 = value.Order == LevelCatalog.Levels.Count; int num = 0; int num2 = 0; bool flag4 = false; if (flag3) { num = Mod.Locations?.GetLevelsRequiredForFree() ?? 0; num2 = Mod.Locations?.CountOtherLevelsCompleted() ?? 0; flag4 = num2 < num; if (ButtonTextCache.TryGetData(levelInfoByUniqueSHName.ID, out string cleanData)) { button.data = $"\n\n{num2}/{num} LEVELS COMPLETED\n\n" + cleanData; } } string text5 = (flag3 ? $"{num2}/{num}".PadRight(8) : null); bool flag5 = value.Order == 1 || flag || UnlockState.IsUnlocked(levelInfoByUniqueSHName.ID); if (!ButtonTextCache.TryGet(levelInfoByUniqueSHName.ID, out string cleanName)) { button.SetLocked(!flag5); continue; } if (!flag5) { string scrambledString = StringScrambler.GetScrambledString(cleanName.ToUpper(), 0.9f, "▀▄█▌▐░▒▓■▪01 "); button.ButtonText = scrambledString + "│" + (text5 ?? LocalizationAccessHelperExtensions.T("MENU_LOCKED8CHARS", Array.Empty())); button.RefreshText(); button.SetLocked(true); continue; } bool flag6 = Mod.Locations?.IsLevelCompleted(levelInfoByUniqueSHName.ID) ?? false; if (flag3 && flag4) { button.ButtonText = cleanName + "│" + text5; button.RefreshText(); button.IsLocked = true; ((SHGUIview)button).color = 'z'; ((SHGUIview)button).SetColorRecursive('z'); continue; } string text6 = (flag ? StringScrambler.GetScrambledString(cleanName.ToUpper(), 0.9f, "▀▄█▌▐░▒▓■▪01 ") : cleanName); string text7 = (flag3 ? text5 : (flag ? "INACTIVE" : LocalizationAccessHelperExtensions.T("MENU_LEVEL8CHARS", Array.Empty()))); button.ButtonText = text6 + "│" + text7; button.RefreshText(); button.IsLocked = flag; ((SHGUIview)button).SetColorRecursive(((SHGUIview)button).color = (flag ? 'b' : (flag6 ? 'w' : 'z'))); if (flag) { RecursiveColorSetField.SetValue(button, true); } } } } [HarmonyPatch(typeof(piOsMenu), "PrepareLevelCommanderButtonForLevel")] public static class LevelButtonCapturePatch { public static void Postfix(ref SHGUIcommanderbutton b, LevelInfo element, string customName) { try { if (!(customName != "") && b != null && element != null && LevelCatalog.LevelIdToLevel.ContainsKey(element.ID)) { int num = b.ButtonText.IndexOf('│'); string cleanName = ((num >= 0) ? b.ButtonText.Substring(0, num) : b.ButtonText); ButtonTextCache.Remember(element.ID, cleanName); ButtonTextCache.RememberData(element.ID, b.data); } } catch (Exception arg) { Instance? log = Mod.Log; if (log != null) { log.Error("LevelButtonCapturePatch.Postfix threw for element.ID=" + (element?.ID.ToString() ?? "(null)") + ", customName='" + customName + "', " + string.Format("b.ButtonText='{0}': {1}", b?.ButtonText ?? "(null)", arg)); } } } } [HarmonyPatch(typeof(LevelSetup), "UnlockNextLevel")] public static class LevelCompletePatch { public static void Postfix() { if (Mod.IsEnabled) { LevelInfo currentLevelInfo = LevelSetup.CurrentLevelInfo; if (currentLevelInfo != null) { Mod.Locations?.CheckLocation(currentLevelInfo.ID); } } } } [HarmonyPatch(typeof(SHGUI), "LaunchLevelAppTunnels", new Type[] { typeof(LevelInfo), typeof(bool), typeof(bool) })] public static class LevelGatePatch { public static bool Prefix(LevelInfo level) { if (!LevelAccessGuard.ShouldBlock(level, out string blockMessage)) { return true; } Instance? log = Mod.Log; if (log != null) { log.Msg("Blocked launch of '" + level.SceneFileName + "' via LaunchLevelAppTunnels -- " + blockMessage); } return false; } } [HarmonyPatch(typeof(piOsMenu), "ShouldBeShown")] public static class MenuVisibilityPatch { public static bool Prefix(ref bool __result) { if (!Mod.IsEnabled) { return true; } __result = true; return false; } } public static class NotificationSettingsButtonPatch { private const string ButtonLabel = "SETTINGS"; internal static void AddTo(SHGUIcommanderview parentView) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) SHGUIcommanderbutton val = new SHGUIcommanderbutton("SETTINGS".PadRight(12, ' ') + "│>FOLDER<", 'w', (Action)delegate { OpenFolder(); }).SetListLink(parentView).SetData("Choose which item classes pop up during play. Everything still shows up in AP LOG regardless of these settings."); parentView.AddButtonView(val); } private static void OpenFolder() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown //IL_003e: Unknown result type (might be due to invalid IL or missing references) SHGUIcommanderview val = new SHGUIcommanderview { isRoot = false, path = "C:\\ARCHIPELAGO\\SETTINGS\\" }; SHGUIcommanderbutton val2 = new SHGUIcommanderbutton("/.. │", 'w', (Action)delegate { SHGUI.current.PopView(); }).SetListLink(val).SetData("Go back."); val.AddButtonView(val2); AddToggle(val, "PROGRESSION", Config.NotifyProgression, "Popup when a progression item is sent or received."); AddToggle(val, "USEFUL", Config.NotifyUseful, "Popup when a useful item is sent or received."); AddToggle(val, "NORMAL", Config.NotifyFiller, "Popup for normal (filler) items -- most checks."); AddToggle(val, "TRAP", Config.NotifyTrap, "Popup when a trap item is sent or received."); SHGUI.current.AddViewOnTop((SHGUIview)(object)val); } private static void AddToggle(SHGUIcommanderview view, string label, MelonPreferences_Entry pref, string description) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) SHGUIcommanderbutton button = null; button = new SHGUIcommanderbutton(BuildLabel(label, pref.Value), 'w', (Action)delegate { pref.Value = !pref.Value; Config.Save(); button.ButtonText = BuildLabel(label, pref.Value); button.RefreshText(); }).SetListLink(view).SetData(description); view.AddButtonView(button); } private static string BuildLabel(string label, bool value) { return label.PadRight(12, ' ') + "│" + (value ? "ON" : "OFF"); } } [HarmonyPatch(typeof(TerminalActivator), "OnActivate")] public static class SecretFoundPatch { public static void Prefix(bool ___secretFound, out bool __state) { __state = ___secretFound; } public static void Postfix(bool ___secretFound, bool __state) { if (Mod.IsEnabled && !__state && ___secretFound) { LevelInfo currentLevelInfo = LevelSetup.CurrentLevelInfo; if (currentLevelInfo != null) { Mod.Locations?.CheckSecretLocation(currentLevelInfo.ID); } } } } [HarmonyPatch(typeof(piOsMenu), "CreateViewFromNode")] public static class StoryLevelsUnlockPatch { public static void Postfix(piOsMenu __instance, XElement e) { try { if (Mod.IsEnabled && e != null && ((XContainer)e).Elements().Any(delegate(XElement child) { if (((object)child.Name).ToString() == "item") { XAttribute obj = child.Attribute(XName.op_Implicit("type")); return ((obj != null) ? obj.Value : null) == "storylevels"; } return false; })) { __instance.LockUnfinishedLevels(); } } catch (Exception arg) { Instance? log = Mod.Log; if (log != null) { log.Error($"StoryLevelsUnlockPatch.Postfix threw: {arg}"); } } } } [HarmonyPatch(typeof(SHGUIcommanderview), "AddButtonView")] public static class SuperhotExeButtonPatch { private const string SuperhotExeCustomName = "superhot.exe"; public static bool Prefix(SHGUIcommanderbutton button) { if (button == null || button.ButtonText == null) { return true; } if (button.ButtonText.StartsWith("superhot.exe")) { return false; } return true; } } internal static class TitleCardGate { public static void SuppressAdvanceIfBlocked(ref InputData inputData) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 //IL_002c: Unknown result type (might be due to invalid IL or missing references) if ((inputData.skipButton & 4) == 0 && (int)inputData.skipButton != 3) { return; } LevelInfo nextLevelInfo = LevelSetup.GetNextLevelInfo(); if (LevelAccessGuard.ShouldBlock(nextLevelInfo, out string blockMessage)) { inputData.skipButton = (ButtonState)1; Instance? log = Mod.Log; if (log != null) { log.Msg("Suppressed SUPERHOT title-card click-through to '" + nextLevelInfo.SceneFileName + "' -- " + blockMessage + ". Returning to hub."); } if ((Object)(object)SHGUI.current != (Object)null) { SHGUI.current.LaunchLevelAppTunnels("SHMenu", false, 1f); } } } } [HarmonyPatch(typeof(LevelFlowControl), "SuperHotSuperHotEnding")] public static class TitleCardGatePatch_Ending { public static void Prefix(ref InputData ___inputData) { TitleCardGate.SuppressAdvanceIfBlocked(ref ___inputData); } } [HarmonyPatch(typeof(LevelFlowControl), "SuperHotSuperHotEndingClickThrough")] public static class TitleCardGatePatch_EndingClickThrough { public static void Prefix(ref InputData ___inputData) { TitleCardGate.SuppressAdvanceIfBlocked(ref ___inputData); } } [HarmonyPatch(typeof(SHGUI), "LaunchLevelViaApp", new Type[] { typeof(LevelInfo), typeof(float) })] public static class ViaAppGatePatch { public static bool Prefix(LevelInfo level) { if (!LevelAccessGuard.ShouldBlock(level, out string blockMessage)) { return true; } Instance? log = Mod.Log; if (log != null) { log.Msg("Blocked launch of '" + level.SceneFileName + "' via LaunchLevelViaApp -- " + blockMessage + ". Returning to hub."); } if ((Object)(object)SHGUI.current != (Object)null) { SHGUI.current.LaunchLevelAppTunnels("SHMenu", false, 1f); } return false; } } } namespace SuperhotArchipelago.Core { public class ArchipelagoConnectApp : SHGUIappbase { private const int FieldCount = 3; private const int StatusLineWidth = 50; private readonly SHGUItext[] _labels = (SHGUItext[])(object)new SHGUItext[3]; private readonly SHGUItext[] _values = (SHGUItext[])(object)new SHGUItext[3]; private readonly string[] _labelText = new string[3] { "SERVER", "SLOT", "PASSWORD" }; private readonly string[] _buffers = new string[3]; private readonly bool[] _masked = new bool[3] { false, false, true }; private SHGUItext _statusField; private int _focusedField; public ArchipelagoConnectApp() : base("ARCHIPELAGO", true) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected O, but got Unknown //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Expected O, but got Unknown //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Expected O, but got Unknown //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Expected O, but got Unknown _buffers[0] = Config.Server.Value; _buffers[1] = Config.Slot.Value; _buffers[2] = Config.Password.Value; int num = 3; for (int i = 0; i < 3; i++) { ref SHGUItext reference = ref _labels[i]; SHGUIview obj = ((SHGUIview)this).AddSubView((SHGUIview)new SHGUItext(_labelText[i], 3, num, 'z', false)); reference = (SHGUItext)(object)((obj is SHGUItext) ? obj : null); ref SHGUItext reference2 = ref _values[i]; SHGUIview obj2 = ((SHGUIview)this).AddSubView((SHGUIview)new SHGUItext("", 3, num + 1, 'w', false)); reference2 = (SHGUItext)(object)((obj2 is SHGUItext) ? obj2 : null); num += 3; } ref SHGUItext statusField = ref _statusField; SHGUIview obj3 = ((SHGUIview)this).AddSubView((SHGUIview)new SHGUItext("", 3, num, 'z', false)); statusField = (SHGUItext)(object)((obj3 is SHGUItext) ? obj3 : null); num += 5; ((SHGUIview)this).AddSubView((SHGUIview)new SHGUItext("[TAB] switch field [ENTER] next / connect", 3, num, 'z', false)); RefreshDisplay(); } public override void Update() { ((SHGUIview)this).Update(); if (Input.GetKeyDown((KeyCode)27)) { SHGUI.current.PopView(); Input.ResetInputAxes(); return; } if (Input.GetKeyDown((KeyCode)9)) { _focusedField = (_focusedField + 1) % 3; } string text = _buffers[_focusedField]; if (Input.inputString.Length > 0) { text += Input.inputString; } for (int i = 0; i < text.Length; i++) { if (text[i] == '\b') { text = text.Remove(i--, 1); if (i >= 0) { text = text.Remove(i--, 1); } } } bool flag = false; for (int j = 0; j < text.Length; j++) { if (text[j] == '\r' || text[j] == '\n') { text = text.Remove(j, text.Length - j); flag = true; break; } } _buffers[_focusedField] = text; if (flag) { if (_focusedField < 2) { _focusedField++; } else { Connect(); } } RefreshDisplay(); Input.ResetInputAxes(); } public override void ReactToInputKeyboard(SHGUIinput key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) if ((int)key == 6) { ((SHGUIappbase)this).ReactToInputKeyboard(key); } } private void Connect() { Mod.ApplyConnectionSettingsAndConnect(_buffers[0], _buffers[1], _buffers[2]); RefreshDisplay(); } private void RefreshDisplay() { string text = ((Mathf.Sin(Time.realtimeSinceStartup * 10f) > 0f) ? "_" : ""); for (int i = 0; i < 3; i++) { bool flag = i == _focusedField; ((SHGUIview)_labels[i]).color = (flag ? 'w' : 'z'); string text2 = (_masked[i] ? new string('*', _buffers[i].Length) : _buffers[i]); _values[i].text = text2 + (flag ? text : ""); } _statusField.text = "STATUS: " + StatusText(); _statusField.BreakTextForLineLength(50); } private string StatusText() { ArchipelagoConnection connection = Mod.Connection; if (connection == null) { return "NOT INITIALIZED"; } if (connection.IsConnected) { return "CONNECTED AS '" + Config.Slot.Value + "'"; } if (!string.IsNullOrEmpty(connection.LastError)) { return "ERROR -- " + connection.LastError; } return "NOT CONNECTED"; } } public class ArchipelagoConnection { private readonly Instance _log; public ArchipelagoSession? Session { get; private set; } public bool IsConnected { get; private set; } public string? LastError { get; private set; } public int LevelsRequiredForFree { get; private set; } = 25; public DateTime ConnectedAtUtc { get; private set; } public HashSet ExcludedLevelOrders { get; private set; } = new HashSet(); public event Action? Connected; public ArchipelagoConnection(Instance log) { _log = log; } public void Connect(string server, string slotName, string? password = null) { LastError = null; IsConnected = false; if (string.IsNullOrEmpty(server) || string.IsNullOrEmpty(slotName)) { LastError = "Server and Slot are both required."; _log.Warning("Not connecting: " + LastError); return; } Session = ArchipelagoSessionFactory.CreateSession(server, 38281); try { LoginResult val = Session.TryConnectAndLogin("SUPERHOT", slotName, (ItemsHandlingFlags)7, (Version)null, (string[])null, (string)null, password, true); if (!val.Successful) { LastError = "Login failed -- check server address, slot name, and password."; _log.Error("Failed to connect to Archipelago server: " + server); return; } LoginSuccessful val2 = (LoginSuccessful)(object)((val is LoginSuccessful) ? val : null); if (val2 != null && val2.SlotData.TryGetValue("levels_required_for_free", out var value)) { LevelsRequiredForFree = Convert.ToInt32(value); _log.Msg($"Slot data: {LevelsRequiredForFree} other levels required to enter '34 - Free'."); } else { _log.Warning("Slot data had no 'levels_required_for_free' -- falling back to " + $"{LevelsRequiredForFree}. Expected if this room was generated from an " + "apworld version older than this feature."); } LoginSuccessful val3 = (LoginSuccessful)(object)((val is LoginSuccessful) ? val : null); if (val3 == null || !val3.SlotData.TryGetValue("excluded_level_orders", out var value2)) { goto IL_019c; } JArray val4 = (JArray)((value2 is JArray) ? value2 : null); if (val4 == null) { goto IL_019c; } ExcludedLevelOrders = new HashSet(((JToken)val4).ToObject>() ?? new List()); if (ExcludedLevelOrders.Count > 0) { _log.Msg($"Slot data: {ExcludedLevelOrders.Count} level(s) excluded from " + "tracking (ExcludeSlowLevels) -- always unlocked, no checks sent for them."); } goto end_IL_0056; IL_019c: ExcludedLevelOrders = new HashSet(); end_IL_0056:; } catch (Exception ex) { LastError = ex.Message; _log.Error("Failed to connect to Archipelago server '" + server + "': " + ex.Message); return; } IsConnected = true; ConnectedAtUtc = DateTime.UtcNow; NotificationLog.Clear(); UnlockState.Clear(); _log.Msg("Connected to Archipelago as '" + slotName + "'."); if (this.Connected == null) { return; } Delegate[] invocationList = this.Connected.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(); } catch (Exception arg) { _log.Error($"A Connected event handler threw and was skipped: {arg}"); } } } public bool IsLevelExcluded(int order) { return ExcludedLevelOrders.Contains(order); } public void Disconnect() { if (Session == null) { IsConnected = false; return; } try { Session.Socket.DisconnectAsync(); } catch (Exception ex) { _log.Warning("Error while disconnecting from the Archipelago server: " + ex.Message); } Session = null; IsConnected = false; _log.Msg("Disconnected from the Archipelago server."); } } public class ArchipelagoLogApp : SHGUIappbase { private const int VisibleLines = 14; private const int LineWidth = 58; private const int MaxSegmentsPerLine = 6; private const int BaseX = 3; private readonly SHGUItext[][] _lineSegments = new SHGUItext[14][]; private readonly SHGUItext _footer; private int _scrollOffset; public ArchipelagoLogApp() : base("LOG", true) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown int num = 3; for (int i = 0; i < 14; i++) { _lineSegments[i] = (SHGUItext[])(object)new SHGUItext[6]; for (int j = 0; j < 6; j++) { ref SHGUItext reference = ref _lineSegments[i][j]; SHGUIview obj = ((SHGUIview)this).AddSubView((SHGUIview)new SHGUItext("", 3, num, 'w', false)); reference = (SHGUItext)(object)((obj is SHGUItext) ? obj : null); } num++; } num++; ref SHGUItext footer = ref _footer; SHGUIview obj2 = ((SHGUIview)this).AddSubView((SHGUIview)new SHGUItext("", 3, num, 'z', false)); footer = (SHGUItext)(object)((obj2 is SHGUItext) ? obj2 : null); RefreshDisplay(); } public override void Update() { ((SHGUIview)this).Update(); if (Input.GetKeyDown((KeyCode)27)) { SHGUI.current.PopView(); Input.ResetInputAxes(); return; } int val = Math.Max(0, NotificationLog.Entries.Count - 14); if (Input.GetKeyDown((KeyCode)274)) { _scrollOffset = Math.Min(val, _scrollOffset + 1); RefreshDisplay(); } else if (Input.GetKeyDown((KeyCode)273)) { _scrollOffset = Math.Max(0, _scrollOffset - 1); RefreshDisplay(); } else if (Input.GetKeyDown((KeyCode)281)) { _scrollOffset = Math.Min(val, _scrollOffset + 14); RefreshDisplay(); } else if (Input.GetKeyDown((KeyCode)280)) { _scrollOffset = Math.Max(0, _scrollOffset - 14); RefreshDisplay(); } } private void RefreshDisplay() { IReadOnlyList entries = NotificationLog.Entries; List list = new List(entries.Count); for (int num = entries.Count - 1; num >= 0; num--) { list.Add(entries[num]); } for (int i = 0; i < 14; i++) { int num2 = _scrollOffset + i; if (num2 < list.Count) { SetRowSegments(i, list[num2]); } else if (num2 == 0) { SetRowSegments(i, new LogSegment[1] { new LogSegment("No notifications yet.", 'w') }); } else { SetRowSegments(i, Array.Empty()); } } _footer.text = ((list.Count == 0) ? "[ESC] close" : ($"[UP/DOWN] scroll [ESC] close ({_scrollOffset + 1}-" + $"{Math.Min(_scrollOffset + 14, list.Count)} of {list.Count})")); } private void SetRowSegments(int rowIndex, LogSegment[] segments) { SHGUItext[] array = _lineSegments[rowIndex]; int num = 0; int i = 0; for (int j = 0; j < segments.Length; j++) { LogSegment logSegment = segments[j]; if (i >= array.Length) { break; } int num2 = 58 - num; string text = ((num2 > 0) ? ((logSegment.Text.Length > num2) ? ((num2 <= 3) ? logSegment.Text.Substring(0, num2) : (logSegment.Text.Substring(0, num2 - 3) + "...")) : logSegment.Text) : ""); SHGUItext obj = array[i]; ((SHGUIview)obj).x = 3 + num; ((SHGUIview)obj).color = logSegment.Color; obj.text = text; num += text.Length; i++; } for (; i < array.Length; i++) { array[i].text = ""; } } } public static class ButtonTextCache { private static readonly Dictionary _cleanNameByLevelId = new Dictionary(); private static readonly Dictionary _cleanDataByLevelId = new Dictionary(); public static void Remember(int levelId, string cleanName) { _cleanNameByLevelId[levelId] = cleanName; } public static bool TryGet(int levelId, out string cleanName) { return _cleanNameByLevelId.TryGetValue(levelId, out cleanName); } public static void RememberData(int levelId, string cleanData) { _cleanDataByLevelId[levelId] = cleanData; } public static bool TryGetData(int levelId, out string cleanData) { return _cleanDataByLevelId.TryGetValue(levelId, out cleanData); } } public static class Config { private static MelonPreferences_Category _category; public static MelonPreferences_Entry Server { get; private set; } public static MelonPreferences_Entry Slot { get; private set; } public static MelonPreferences_Entry Password { get; private set; } public static MelonPreferences_Entry Enabled { get; private set; } public static MelonPreferences_Entry NotifyProgression { get; private set; } public static MelonPreferences_Entry NotifyUseful { get; private set; } public static MelonPreferences_Entry NotifyFiller { get; private set; } public static MelonPreferences_Entry NotifyTrap { get; private set; } public static MelonPreferences_Entry EndingClickBufferSeconds { get; private set; } public static bool IsConfigured { get { if (Server.Value != "") { return Slot.Value != ""; } return false; } } public static void Load() { _category = MelonPreferences.CreateCategory("SuperhotArchipelago", "SUPERHOT Archipelago"); Server = _category.CreateEntry("Server", "", "Server", "Archipelago server address, e.g. archipelago.gg:38281 or localhost:38281", false, false, (ValueValidator)null, (string)null); Slot = _category.CreateEntry("Slot", "", "Slot", "Your player/slot name, matching the name in your player YAML.", false, false, (ValueValidator)null, (string)null); Password = _category.CreateEntry("Password", "", "Password", "Room password, if the server has one set. Leave blank otherwise.", false, false, (ValueValidator)null, (string)null); Enabled = _category.CreateEntry("Enabled", true, "Enabled", "Whether Archipelago mode is on. Turn off to play vanilla SUPERHOT (no level gating, no hub overlay) without uninstalling the mod.", false, false, (ValueValidator)null, (string)null); NotifyProgression = _category.CreateEntry("NotifyProgression", true, "NotifyProgression", "Popup for progression items.", false, false, (ValueValidator)null, (string)null); NotifyUseful = _category.CreateEntry("NotifyUseful", true, "NotifyUseful", "Popup for useful items.", false, false, (ValueValidator)null, (string)null); NotifyFiller = _category.CreateEntry("NotifyFiller", true, "NotifyFiller", "Popup for normal (filler) items -- most checks.", false, false, (ValueValidator)null, (string)null); NotifyTrap = _category.CreateEntry("NotifyTrap", true, "NotifyTrap", "Popup for trap items.", false, false, (ValueValidator)null, (string)null); EndingClickBufferSeconds = _category.CreateEntry("EndingClickBufferSeconds", 1.5f, "EndingClickBufferSeconds", "Seconds after a level's ending sequence starts before a click can skip through it. Higher values give more protection against finishing-blow clicks accidentally skipping ahead; 0 disables the buffer entirely.", false, false, (ValueValidator)null, (string)null); _category.SaveToFile(false); } public static bool ShouldNotify(NotificationColors.ItemClass itemClass) { return itemClass switch { NotificationColors.ItemClass.Progression => NotifyProgression.Value, NotificationColors.ItemClass.Useful => NotifyUseful.Value, NotificationColors.ItemClass.Trap => NotifyTrap.Value, _ => NotifyFiller.Value, }; } public static void Save() { _category.SaveToFile(false); } } public class ItemManager { private readonly struct QueuedItem { public readonly long ItemId; public readonly long LocationId; public readonly string SenderDisplayName; public readonly int SenderSlot; public readonly bool IsCatchUp; public readonly ItemFlags Flags; public QueuedItem(long itemId, long locationId, string senderDisplayName, int senderSlot, ItemFlags flags, bool isCatchUp) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) ItemId = itemId; LocationId = locationId; SenderDisplayName = senderDisplayName; SenderSlot = senderSlot; Flags = flags; IsCatchUp = isCatchUp; } } private static readonly TimeSpan CatchUpWindow = TimeSpan.FromSeconds(5.0); private readonly ArchipelagoConnection _connection; private readonly Instance _log; private readonly ConcurrentQueue _itemQueue = new ConcurrentQueue(); public ItemManager(ArchipelagoConnection connection, Instance log) { _connection = connection; _log = log; _connection.Connected += OnConnected; } private void OnConnected() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_0075: Unknown result type (might be due to invalid IL or missing references) try { if (_connection.Session == null) { return; } _connection.Session.Items.ItemReceived += (ItemReceivedHandler)delegate(ReceivedItemsHelper helper) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) try { ItemInfo val2 = helper.DequeueItem(); bool isCatchUp = DateTime.UtcNow - _connection.ConnectedAtUtc < CatchUpWindow; string alias = val2.Player.Alias; _itemQueue.Enqueue(new QueuedItem(val2.ItemId, val2.LocationId, alias, val2.Player.Slot, val2.Flags, isCatchUp)); } catch (Exception arg3) { _log.Error("ItemReceived handler threw for one item -- it may not unlock its " + $"level, but every other item is unaffected: {arg3}"); } }; while (_connection.Session.Items.Any()) { try { ItemInfo val = _connection.Session.Items.DequeueItem(); _itemQueue.Enqueue(new QueuedItem(val.ItemId, val.LocationId, val.Player.Alias, val.Player.Slot, val.Flags, isCatchUp: true)); } catch (Exception arg) { _log.Error("Draining one historical item from the received-items queue threw -- it " + $"may not unlock its level, but every other item is unaffected: {arg}"); } } } catch (Exception arg2) { _log.Error("ItemManager.OnConnected failed -- items received this connection " + $"may not unlock levels correctly: {arg2}"); } } public void ProcessQueue() { QueuedItem result; while (_itemQueue.TryDequeue(out result)) { ApplyItem(result); } } private void ApplyItem(QueuedItem queued) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) long itemId = queued.ItemId; bool isSelfSend = _connection.Session != null && queued.SenderSlot == _connection.Session.ConnectionInfo.Slot; switch (itemId) { case 0L: return; case 3901100L: _log.Msg("Received 'White Space' -- filler, nothing to apply."); Notify("White Space", queued.SenderDisplayName, queued.IsCatchUp, queued.LocationId, isSelfSend, queued.Flags); return; } if (!LevelCatalog.ItemIdToLevel.TryGetValue(itemId, out LevelEntry value)) { _log.Warning($"Received unknown item id {itemId} -- no matching level in LevelCatalog."); return; } UnlockState.Unlock(value.LevelId); _log.Msg($"Unlocked '{value.DisplayName}' (level id {value.LevelId}, scene '{value.SceneName}') from a received item."); Notify(value.DisplayName, queued.SenderDisplayName, queued.IsCatchUp, queued.LocationId, isSelfSend, queued.Flags); } private void Notify(string itemDisplayName, string senderDisplayName, bool isCatchUp, long locationId, bool isSelfSend, ItemFlags flags) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) if (!isSelfSend) { char color = NotificationColors.ForItemFlags(flags); LogSegment[] segments = new LogSegment[4] { new LogSegment("Received ", 'w'), new LogSegment(itemDisplayName, color), new LogSegment(" from ", 'w'), new LogSegment(senderDisplayName, 'D') }; string popupText = (Config.ShouldNotify(NotificationColors.Classify(flags)) ? ("Received " + itemDisplayName + " from " + senderDisplayName) : null); if (isCatchUp) { NotificationLog.Add(segments, null, locationId); } else { NotificationLog.Add(segments, popupText); } } } } public static class LevelAccessGuard { public static bool ShouldBlock(LevelInfo level, out string blockMessage) { blockMessage = ""; if (!Mod.IsEnabled) { return false; } if (level == null || string.IsNullOrEmpty(level.SceneFileName)) { return false; } LevelInfo val = ResolveToTrackedLevel(level); if (val == null) { return false; } if (!LevelCatalog.LevelIdToLevel.TryGetValue(val.ID, out LevelEntry value)) { return false; } if (!LevelCatalog.LevelIdToLevel.ContainsKey(level.ID)) { blockMessage = "Return to hub to continue"; return true; } if (value.Order == 1) { return false; } if (Mod.Connection != null && Mod.Connection.IsLevelExcluded(value.Order)) { return false; } if (!UnlockState.IsUnlocked(val.ID)) { blockMessage = "LOCKED: '" + value.DisplayName + "' needs an Archipelago item to unlock"; return true; } if (value.Order == LevelCatalog.Levels.Count) { int num = Mod.Locations?.GetLevelsRequiredForFree() ?? 0; int num2 = Mod.Locations?.CountOtherLevelsCompleted() ?? 0; if (num2 < num) { blockMessage = $"LOCKED: {num2}/{num} levels needed"; return true; } } return false; } private static LevelInfo? ResolveToTrackedLevel(LevelInfo level) { LevelInfo val = level; int num = 0; while (val != null && !string.IsNullOrEmpty(val.SceneFileName) && !LevelCatalog.LevelIdToLevel.ContainsKey(val.ID) && num < 64) { int num2 = LevelSetup.GetLevelIndexByID(val.ID) + 1; val = ((num2 < LevelSetup.Levels.Count) ? LevelSetup.Levels[num2] : null); num++; } return val; } } public class LevelEntry { public int Order; public string SceneName = ""; public string DisplayName = ""; public int LevelId; public bool HasSecret; } public static class LevelCatalog { public const long BaseId = 3891000L; public const long ItemIdOffset = 10000L; public const long SecretLocationIdOffset = 20000L; public const long WhiteSpaceItemId = 3901100L; public static List Levels { get; private set; } = new List(); public static Dictionary LocationIdToLevel { get; private set; } = new Dictionary(); public static Dictionary ItemIdToLevel { get; private set; } = new Dictionary(); public static Dictionary SecretLocationIdToLevel { get; private set; } = new Dictionary(); public static Dictionary LevelIdToLevel { get; private set; } = new Dictionary(); public static void Load(Instance log) { string text = Path.Combine(Path.GetDirectoryName(typeof(LevelCatalog).Assembly.Location), "data", "levels.json"); if (!File.Exists(text)) { log.Error("levels.json not found at '" + text + "' -- location/item id mapping will be empty."); return; } foreach (JToken item in (IEnumerable)JObject.Parse(File.ReadAllText(text))["levels"]) { LevelEntry obj = new LevelEntry { Order = Extensions.Value((IEnumerable)item[(object)"order"]), SceneName = Extensions.Value((IEnumerable)item[(object)"id"]), DisplayName = Extensions.Value((IEnumerable)item[(object)"name"]), LevelId = Extensions.Value((IEnumerable)item[(object)"gameId"]) }; JToken obj2 = item[(object)"hasSecret"]; obj.HasSecret = obj2 != null && Extensions.Value((IEnumerable)obj2); LevelEntry levelEntry = obj; Levels.Add(levelEntry); long key = 3891000L + (long)levelEntry.Order; long key2 = 3901000L + (long)levelEntry.Order; LocationIdToLevel[key] = levelEntry; ItemIdToLevel[key2] = levelEntry; if (levelEntry.HasSecret) { long key3 = 3911000L + (long)levelEntry.Order; SecretLocationIdToLevel[key3] = levelEntry; } LevelIdToLevel[levelEntry.LevelId] = levelEntry; } log.Msg($"LevelCatalog loaded {Levels.Count} levels from '{text}'."); } public static string? TryGetShortItemDisplayName(long itemId) { if (itemId == 3901100) { return "White Space"; } if (!ItemIdToLevel.TryGetValue(itemId, out LevelEntry value)) { return null; } return value.DisplayName; } } public class LocationManager { private readonly ArchipelagoConnection _connection; private readonly Instance _log; private readonly ConcurrentQueue<(LogSegment[] Segments, string? PopupText, long OrderKey)> _pendingNotifications = new ConcurrentQueue<(LogSegment[], string, long)>(); public LocationManager(ArchipelagoConnection connection, Instance log) { _connection = connection; _log = log; _connection.Connected += OnConnected; } private void OnConnected() { try { OnConnectedCore(); } catch (Exception arg) { _log.Error("LocationManager.OnConnected failed, notification history resync " + $"skipped for this connection: {arg}"); } } private void OnConnectedCore() { if (_connection.Session == null) { return; } long[] array = _connection.Session.Locations.AllLocationsChecked.Where((long id) => LevelCatalog.LocationIdToLevel.ContainsKey(id) || LevelCatalog.SecretLocationIdToLevel.ContainsKey(id)).ToArray(); if (array.Length == 0) { return; } _connection.Session.Locations.ScoutLocationsAsync(array).ContinueWith(delegate(Task> task) { //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (task.IsFaulted || task.IsCanceled || task.Result == null) { _log.Warning("Failed to scout this slot's already-checked locations -- the AP LOG screen won't show full history until the next successful reconnect."); return; } foreach (KeyValuePair item2 in task.Result) { try { string text = LevelCatalog.TryGetShortItemDisplayName(((ItemInfo)item2.Value).ItemId) ?? ((ItemInfo)item2.Value).ItemDisplayName; char color = NotificationColors.ForItemFlags(((ItemInfo)item2.Value).Flags); LogSegment[] item = ((_connection.Session == null || item2.Value.Player.Slot != _connection.Session.ConnectionInfo.Slot) ? new LogSegment[5] { new LogSegment("Sent ", 'w'), new LogSegment(text, color), new LogSegment(" to ", 'w'), new LogSegment(item2.Value.Player.Alias, 'D'), new LogSegment(" from " + ResolveLocationDisplayText(item2.Key), 'w') } : new LogSegment[3] { new LogSegment(item2.Value.Player.Alias, 'D'), new LogSegment(" found their ", 'w'), new LogSegment(text, color) }); _pendingNotifications.Enqueue((item, null, item2.Key)); } catch (Exception arg) { _log.Error("Processing one scouted historical location threw -- it may be " + $"missing from the AP LOG, but every other entry is unaffected: {arg}"); } } }); } public void ProcessPendingNotifications() { (LogSegment[], string, long) result; while (_pendingNotifications.TryDequeue(out result)) { NotificationLog.Add(result.Item1, result.Item2, result.Item3); } } public void CheckLocation(int levelId) { if (_connection.Session == null || !_connection.IsConnected) { _log.Warning($"CheckLocation({levelId}) called before connecting -- ignored."); return; } if (!LevelCatalog.LevelIdToLevel.TryGetValue(levelId, out LevelEntry value)) { _log.Warning($"Unknown level id {levelId} -- no matching entry in levels.json. " + "Either a level outside our catalog just finished, or levels.json's order/id list is out of sync with the real game."); return; } if (value.Order != LevelCatalog.Levels.Count && !_connection.IsLevelExcluded(value.Order)) { long num = 3891000L + (long)value.Order; if (_connection.Session.Locations.AllLocationsChecked.Contains(num)) { _log.Msg("'" + value.DisplayName + "' already checked -- skipping duplicate check/notification."); } else { _connection.Session.Locations.CompleteLocationChecks(new long[1] { num }); _log.Msg($"Sent check for '{value.DisplayName}' (level id {levelId}, location id {num})."); ScoutAndQueueSentNotification(num, value.DisplayName, isLive: true); } } if (value.Order == LevelCatalog.Levels.Count) { _connection.Session.SetGoalAchieved(); _log.Msg("'" + value.DisplayName + "' was the final level -- reported goal achieved."); SaveManager.Instance.SetValue("storyFinished", (object)true); } } public void CheckSecretLocation(int levelId) { if (_connection.Session == null || !_connection.IsConnected) { _log.Warning($"CheckSecretLocation({levelId}) called before connecting -- ignored."); return; } if (!LevelCatalog.LevelIdToLevel.TryGetValue(levelId, out LevelEntry value)) { _log.Warning($"Unknown level id {levelId} for a secret find -- no matching entry in " + "levels.json. Either a level outside our catalog has a secret we don't know about, or levels.json's order/id list is out of sync with the real game."); return; } if (!value.HasSecret) { _log.Warning("Secret found in '" + value.DisplayName + "', but levels.json says this level has none -- sending the check anyway, but this likely means levels.json's hasSecret is out of sync with the real game."); } if (_connection.IsLevelExcluded(value.Order)) { _log.Msg("'" + value.DisplayName + "' secret found, but this level is excluded from tracking (ExcludeSlowLevels) -- no check to send."); return; } long num = 3911000L + (long)value.Order; if (_connection.Session.Locations.AllLocationsChecked.Contains(num)) { _log.Msg("'" + value.DisplayName + "' secret already checked -- skipping duplicate check/notification."); return; } _connection.Session.Locations.CompleteLocationChecks(new long[1] { num }); _log.Msg($"Sent secret check for '{value.DisplayName}' (level id {levelId}, location id {num})."); ScoutAndQueueSentNotification(num, value.DisplayName + " Secret", isLive: true); } private void ScoutAndQueueSentNotification(long locationId, string locationDisplayText, bool isLive) { if (_connection.Session == null) { return; } _connection.Session.Locations.ScoutLocationsAsync(new long[1] { locationId }).ContinueWith(delegate(Task> task) { //IL_020c: 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_0066: Unknown result type (might be due to invalid IL or missing references) try { ItemFlags? val = null; string text2; LogSegment[] item; if (!task.IsFaulted && !task.IsCanceled && task.Result != null && task.Result.TryGetValue(locationId, out var value)) { val = ((ItemInfo)value).Flags; string text = LevelCatalog.TryGetShortItemDisplayName(((ItemInfo)value).ItemId) ?? ((ItemInfo)value).ItemDisplayName; char color = NotificationColors.ForItemFlags(((ItemInfo)value).Flags); if (_connection.Session != null && value.Player.Slot == _connection.Session.ConnectionInfo.Slot) { text2 = value.Player.Alias + " found their " + text; item = new LogSegment[3] { new LogSegment(value.Player.Alias, 'D'), new LogSegment(" found their ", 'w'), new LogSegment(text, color) }; } else { text2 = "Sent " + text + " to " + value.Player.Alias; item = new LogSegment[5] { new LogSegment("Sent ", 'w'), new LogSegment(text, color), new LogSegment(" to ", 'w'), new LogSegment(value.Player.Alias, 'D'), new LogSegment(" from " + locationDisplayText, 'w') }; } } else { _log.Warning($"Failed to scout location {locationId} for a notification -- " + "showing a generic message instead."); text2 = "Sent check for " + locationDisplayText; item = new LogSegment[1] { new LogSegment(text2, 'w') }; } bool flag = isLive && (!val.HasValue || Config.ShouldNotify(NotificationColors.Classify(val.Value))); _pendingNotifications.Enqueue((item, flag ? text2 : null, long.MaxValue)); } catch (Exception arg) { _log.Error($"Building a notification for location {locationId} threw -- it may be " + $"missing a popup/log entry, but nothing else is affected: {arg}"); } }); } private static string ResolveLocationDisplayText(long locationId) { if (LevelCatalog.LocationIdToLevel.TryGetValue(locationId, out LevelEntry value)) { return value.DisplayName; } if (LevelCatalog.SecretLocationIdToLevel.TryGetValue(locationId, out LevelEntry value2)) { return value2.DisplayName + " Secret"; } return "an unknown location"; } public bool IsLevelCompleted(int levelId) { if (_connection.Session == null || !_connection.IsConnected) { return false; } if (!LevelCatalog.LevelIdToLevel.TryGetValue(levelId, out LevelEntry value)) { return false; } if (value.Order == LevelCatalog.Levels.Count) { return UnlockState.IsUnlocked(levelId); } if (_connection.IsLevelExcluded(value.Order)) { return true; } long value2 = 3891000L + (long)value.Order; return _connection.Session.Locations.AllLocationsChecked.Contains(value2); } public bool IsSecretCompleted(int levelId) { if (_connection.Session == null || !_connection.IsConnected) { return false; } if (!LevelCatalog.LevelIdToLevel.TryGetValue(levelId, out LevelEntry value)) { return false; } if (_connection.IsLevelExcluded(value.Order)) { return true; } long value2 = 3911000L + (long)value.Order; return _connection.Session.Locations.AllLocationsChecked.Contains(value2); } private IEnumerable OtherTrackedLevels() { foreach (LevelEntry level in LevelCatalog.Levels) { if (level.Order != LevelCatalog.Levels.Count && !_connection.IsLevelExcluded(level.Order)) { yield return level; } } } public int CountOtherLevelsCompleted() { int num = 0; foreach (LevelEntry item in OtherTrackedLevels()) { if (IsLevelCompleted(item.LevelId)) { num++; } } return num; } public int GetLevelsRequiredForFree() { return Math.Min(_connection.LevelsRequiredForFree, OtherTrackedLevels().Count()); } } public class Mod : MelonMod { private static Mod? _instance; private static bool _suppressAutoReconnect; public static ArchipelagoConnection? Connection { get; private set; } public static LocationManager? Locations { get; private set; } public static ItemManager? Items { get; private set; } public static Instance? Log { get; private set; } public static bool IsEnabled => Config.Enabled.Value; private static bool IsFullyConnected { get { if (Connection != null) { return Connection.IsConnected; } return false; } } public override void OnInitializeMelon() { _instance = this; Log = ((MelonBase)this).LoggerInstance; ((MelonBase)this).LoggerInstance.Msg("SuperhotArchipelago loading."); Config.Load(); LevelCatalog.Load(((MelonBase)this).LoggerInstance); Connection = new ArchipelagoConnection(((MelonBase)this).LoggerInstance); Locations = new LocationManager(Connection, ((MelonBase)this).LoggerInstance); Items = new ItemManager(Connection, ((MelonBase)this).LoggerInstance); if (!Config.Enabled.Value) { ((MelonBase)this).LoggerInstance.Msg("Not connecting yet: Archipelago mode is turned off. Click the AP MODE icon on the hub's main screen to turn it back on."); } else if (Config.IsConfigured) { TryConnect(); } else { ((MelonBase)this).LoggerInstance.Msg("Not connecting yet: Server/Slot not set. Click the ARCHIPELAGO icon on the hub's main screen to open the connection menu and fill them in, or edit the [SuperhotArchipelago] section of UserData/MelonPreferences.cfg directly (Server/Slot/Password changes there also trigger a reconnect automatically)."); } ((MelonEventBase>)(object)Config.Server.OnEntryValueChanged).Subscribe((LemonAction)delegate { TryConnect(); }, 0, false); ((MelonEventBase>)(object)Config.Slot.OnEntryValueChanged).Subscribe((LemonAction)delegate { TryConnect(); }, 0, false); ((MelonEventBase>)(object)Config.Password.OnEntryValueChanged).Subscribe((LemonAction)delegate { TryConnect(); }, 0, false); ((MelonEventBase>)(object)Config.Enabled.OnEntryValueChanged).Subscribe((LemonAction)delegate { ApplyEnabledChange(); }, 0, false); } private void TryConnect() { if (!_suppressAutoReconnect && Config.Enabled.Value && Config.IsConfigured) { ((MelonBase)this).LoggerInstance.Msg("Connecting to '" + Config.Server.Value + "' as '" + Config.Slot.Value + "'..."); Connection?.Connect(Config.Server.Value, Config.Slot.Value, (Config.Password.Value == "") ? null : Config.Password.Value); } } public static void SetEnabled(bool enabled) { if (_instance != null) { _suppressAutoReconnect = true; try { Config.Enabled.Value = enabled; Config.Save(); } finally { _suppressAutoReconnect = false; } ApplyEnabledChange(); } } private static void ApplyEnabledChange() { if (_suppressAutoReconnect) { return; } if (Config.Enabled.Value) { Instance? log = Log; if (log != null) { log.Msg("Archipelago mode turned ON."); } _instance?.TryConnect(); } else { Instance? log2 = Log; if (log2 != null) { log2.Msg("Archipelago mode turned OFF -- SUPERHOT will play like vanilla until it's turned back on."); } Connection?.Disconnect(); } } public static void ApplyConnectionSettingsAndConnect(string server, string slot, string password) { if (_instance != null) { _suppressAutoReconnect = true; try { Config.Server.Value = server; Config.Slot.Value = slot; Config.Password.Value = password; Config.Enabled.Value = true; Config.Save(); } finally { _suppressAutoReconnect = false; } _instance.TryConnect(); } } public override void OnUpdate() { RunStep("Items.ProcessQueue", delegate { Items?.ProcessQueue(); }); RunStep("Locations.ProcessPendingNotifications", delegate { Locations?.ProcessPendingNotifications(); }); RunStep("NotificationLog.FlushPendingPopups", NotificationLog.FlushPendingPopups); RunStep("PopupOverlay.Update", PopupOverlay.Update); RunStep("ConnectionButtonPatch.RefreshLabel", ConnectionButtonPatch.RefreshLabel); RunStep("ArchipelagoModeTogglePatch.RefreshLabel", ArchipelagoModeTogglePatch.RefreshLabel); } private void RunStep(string name, Action step) { try { step(); } catch (Exception arg) { ((MelonBase)this).LoggerInstance.Error($"[debug] OnUpdate step '{name}' threw: {arg}"); } } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { ((MelonBase)this).LoggerInstance.Msg($"Scene loaded: {sceneName} (buildIndex {buildIndex})"); try { RunSceneWasLoadedBody(sceneName); } catch (Exception arg) { ((MelonBase)this).LoggerInstance.Error($"OnSceneWasLoaded('{sceneName}') threw: {arg}"); } } private void RunSceneWasLoadedBody(string sceneName) { if (IsEnabled && IsFullyConnected && SaveManager.Instance != null && Locations != null) { foreach (LevelEntry level in LevelCatalog.Levels) { if (level.HasSecret) { bool flag = Locations.IsSecretCompleted(level.LevelId); SaveManager.Instance.SetValue(level.SceneName + "1unlocked", (object)flag); } } } LevelInfo currentLevelInfo = LevelSetup.CurrentLevelInfo; if (sceneName != "SHMenu" && currentLevelInfo != null && LevelAccessGuard.ShouldBlock(currentLevelInfo, out string _)) { ((MelonBase)this).LoggerInstance.Msg("Safety net: scene '" + sceneName + "' resolved to a locked level that no launch-time gate caught -- kicking back to hub."); if ((Object)(object)SHGUI.current != (Object)null) { SHGUI.current.LaunchLevelAppTunnels("SHMenu", false, 1f); } } PopupOverlay.OnSceneLoaded(); } } public static class NotificationColors { public enum ItemClass { Progression, Useful, Trap, Filler } public const char Player = 'D'; public const char ProgressionItem = 'g'; public const char UsefulItem = 'b'; public const char TrapItem = 'r'; public const char FillerItem = 'z'; public const char Default = 'w'; public static ItemClass Classify(ItemFlags flags) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) if (((Enum)flags).HasFlag((Enum)(object)(ItemFlags)4)) { return ItemClass.Trap; } if (((Enum)flags).HasFlag((Enum)(object)(ItemFlags)1)) { return ItemClass.Progression; } if (((Enum)flags).HasFlag((Enum)(object)(ItemFlags)2)) { return ItemClass.Useful; } return ItemClass.Filler; } public static char ForItemFlags(ItemFlags flags) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return Classify(flags) switch { ItemClass.Trap => 'r', ItemClass.Progression => 'g', ItemClass.Useful => 'b', _ => 'z', }; } } public readonly struct LogSegment { public readonly string Text; public readonly char Color; public LogSegment(string text, char color) { Text = text; Color = color; } } public static class NotificationLog { private const int MaxEntries = 200; private static readonly List _entries = new List(); private static readonly List _entryOrderKeys = new List(); private static readonly List _pendingPopups = new List(); private const int MaxPendingPopups = 4; public static IReadOnlyList Entries => _entries; public static void Add(LogSegment[] segments, string? popupText) { Add(segments, popupText, long.MaxValue); } public static void Add(string plainText, string? popupText) { Add(new LogSegment[1] { new LogSegment(plainText, 'w') }, popupText, long.MaxValue); } public static void Add(LogSegment[] segments, string? popupText, long orderKey) { int num = _entries.Count; while (num > 0 && _entryOrderKeys[num - 1] > orderKey) { num--; } _entries.Insert(num, segments); _entryOrderKeys.Insert(num, orderKey); while (_entries.Count > 200) { _entries.RemoveAt(0); _entryOrderKeys.RemoveAt(0); } if (popupText != null) { _pendingPopups.Add(popupText); while (_pendingPopups.Count > 4) { _pendingPopups.RemoveAt(0); } } } public static void FlushPendingPopups() { if (_pendingPopups.Count == 0 || TimeControl.forcedTimeScale == 0f) { return; } foreach (string pendingPopup in _pendingPopups) { PopupOverlay.Show(pendingPopup); } _pendingPopups.Clear(); } public static void Clear() { _entries.Clear(); _entryOrderKeys.Clear(); _pendingPopups.Clear(); } } public static class PopupOverlay { private enum State { Hidden, FadingIn, Visible, FadingOut } private const int ReferenceWidth = 1920; private const int ReferenceHeight = 1080; private const float DedupeWindowSeconds = 1f; private static RectTransform? _panel; private static CanvasGroup? _canvasGroup; private static Text? _text; private static readonly Queue _queue = new Queue(); private static float? _fixedCharAdvance; private static float? _fixedLineHeight; private static State _state = State.Hidden; private static float _timer; private static string? _lastShownMessage; private static float _lastShownRealtime = float.NegativeInfinity; private static GameObject? _canvasRoot; private const string GameFontAssetPath = "Fonts/SHGUIFontAsset_EU"; private static readonly string[] PreferredMonospaceFonts = new string[5] { "Consolas", "Courier New", "DejaVu Sans Mono", "Lucida Console", "Monospace" }; private static PopupTuning T => PopupTuning.Current; public static void Show(string text) { bool num = text == _lastShownMessage && Time.unscaledTime - _lastShownRealtime < 1f; _lastShownMessage = text; _lastShownRealtime = Time.unscaledTime; if (!num) { _queue.Enqueue(text); } } public static void OnSceneLoaded() { } public static void Update() { EnsureCanvas(); switch (_state) { case State.Hidden: if (_queue.Count > 0) { DisplayNext(); } break; case State.FadingIn: _timer += Time.unscaledDeltaTime; ApplyTransition(Mathf.Clamp01(_timer / T.TransitionSeconds)); if (_timer >= T.TransitionSeconds) { _state = State.Visible; _timer = T.DisplaySeconds; } break; case State.Visible: _timer -= Time.unscaledDeltaTime; if (_timer <= 0f) { _state = State.FadingOut; _timer = 0f; } break; case State.FadingOut: _timer += Time.unscaledDeltaTime; ApplyTransition(1f - Mathf.Clamp01(_timer / T.TransitionSeconds)); if (_timer >= T.TransitionSeconds) { _state = State.Hidden; ((Component)_panel).gameObject.SetActive(false); } break; } } private static void ApplyTransition(float t) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) _canvasGroup.alpha = t; ((Transform)_panel).localScale = new Vector3(1f, Mathf.Lerp(T.MinScaleY, 1f, t), 1f); } public static void ReloadTuning() { if ((Object)(object)_canvasRoot != (Object)null) { Object.Destroy((Object)(object)_canvasRoot); } _canvasRoot = null; _text = null; _panel = null; _canvasGroup = null; _fixedCharAdvance = null; _fixedLineHeight = null; _state = State.Hidden; _timer = 0f; } private static void EnsureCanvas() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown //IL_0059: 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_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_00af: 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_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) //IL_019b: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_023c: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_text != (Object)null)) { PopupTuning.Load(); GameObject val = (_canvasRoot = new GameObject("SuperhotArchipelago_PopupCanvas")); Object.DontDestroyOnLoad((Object)(object)val); Canvas obj = val.AddComponent(); obj.renderMode = (RenderMode)0; obj.sortingOrder = 1000; CanvasScaler obj2 = val.AddComponent(); obj2.uiScaleMode = (ScaleMode)1; obj2.referenceResolution = new Vector2(1920f, 1080f); obj2.screenMatchMode = (ScreenMatchMode)0; obj2.matchWidthOrHeight = 0.5f; GameObject val2 = new GameObject("Panel"); val2.transform.SetParent(val.transform, false); _panel = val2.AddComponent(); _panel.anchorMin = new Vector2(0.5f, T.TopAnchorY); _panel.anchorMax = new Vector2(0.5f, T.TopAnchorY); _panel.pivot = new Vector2(0.5f, 1f); _canvasGroup = val2.AddComponent(); CreateStretchImage((Transform)(object)_panel, "Background", T.BackgroundColor, 0f); Font val3 = ResolveFont(); GameObject val4 = new GameObject("Text"); val4.transform.SetParent((Transform)(object)_panel, false); RectTransform val5 = val4.AddComponent(); val5.anchorMin = new Vector2(0.5f, 0.5f); val5.anchorMax = new Vector2(0.5f, 0.5f); val5.pivot = new Vector2(0.5f, 0.5f); ((Transform)val5).localScale = new Vector3(T.PixelScale, T.PixelScale, 1f); _text = val4.AddComponent(); _text.font = val3; _text.fontSize = T.FontSize; _text.fontStyle = (FontStyle)1; _text.alignment = (TextAnchor)4; _text.horizontalOverflow = (HorizontalWrapMode)1; _text.verticalOverflow = (VerticalWrapMode)1; ((Graphic)_text).color = Color.white; if (_fixedLineHeight.HasValue) { val5.anchoredPosition = new Vector2(0f, (0f - _fixedLineHeight.Value) * T.PixelScale * T.TextVerticalNudgeFraction); } if ((Object)(object)val3.material != (Object)null && (Object)(object)val3.material.mainTexture != (Object)null) { val3.material.mainTexture.filterMode = (FilterMode)0; } val2.SetActive(false); } } private static Image CreateStretchImage(Transform parent, string name, Color color, float inset) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name); val.transform.SetParent(parent, false); RectTransform obj = val.AddComponent(); obj.anchorMin = Vector2.zero; obj.anchorMax = Vector2.one; obj.offsetMin = new Vector2(inset, inset); obj.offsetMax = new Vector2(0f - inset, 0f - inset); Image obj2 = val.AddComponent(); ((Graphic)obj2).color = color; return obj2; } private static Font ResolveFont() { Font val = BuildGameFont(); if ((Object)(object)val != (Object)null) { return val; } Font val2 = Font.CreateDynamicFontFromOSFont(PreferredMonospaceFonts, T.FontSize); if ((Object)(object)val2 != (Object)null) { return val2; } Font builtinResource = Resources.GetBuiltinResource("Arial.ttf"); if ((Object)(object)builtinResource != (Object)null) { return builtinResource; } return Font.CreateDynamicFontFromOSFont("Arial", T.FontSize); } private static Font? BuildGameFont() { //IL_0230: Unknown result type (might be due to invalid IL or missing references) //IL_0235: Unknown result type (might be due to invalid IL or missing references) //IL_0240: Unknown result type (might be due to invalid IL or missing references) //IL_0245: Unknown result type (might be due to invalid IL or missing references) //IL_0251: Expected O, but got Unknown //IL_0251: Unknown result type (might be due to invalid IL or missing references) //IL_0289: Expected O, but got Unknown //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01c8: Unknown result type (might be due to invalid IL or missing references) //IL_01cd: Unknown result type (might be due to invalid IL or missing references) //IL_01e2: Unknown result type (might be due to invalid IL or missing references) //IL_01e7: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_0204: Unknown result type (might be due to invalid IL or missing references) SHGUIFontAsset val = Resources.Load("Fonts/SHGUIFontAsset_EU"); if ((Object)(object)val == (Object)null || (Object)(object)val.FontTexture == (Object)null || (Object)(object)val.FontDescription == (Object)null) { return null; } Texture2D fontTexture = val.FontTexture; float num = ((Texture)fontTexture).width; float num2 = ((Texture)fontTexture).height; List list = new List(); foreach (XElement item2 in ((XContainer)XDocument.Parse(val.FontDescription.text)).Descendants(XName.op_Implicit("char"))) { XAttribute obj = item2.Attribute(XName.op_Implicit("id")); if (!int.TryParse((obj != null) ? obj.Value : null, out var result)) { continue; } XAttribute obj2 = item2.Attribute(XName.op_Implicit("x")); if (!int.TryParse((obj2 != null) ? obj2.Value : null, out var result2)) { continue; } XAttribute obj3 = item2.Attribute(XName.op_Implicit("y")); if (!int.TryParse((obj3 != null) ? obj3.Value : null, out var result3)) { continue; } XAttribute obj4 = item2.Attribute(XName.op_Implicit("width")); if (!int.TryParse((obj4 != null) ? obj4.Value : null, out var result4)) { continue; } XAttribute obj5 = item2.Attribute(XName.op_Implicit("height")); if (int.TryParse((obj5 != null) ? obj5.Value : null, out var result5)) { float num3 = (float)result2 / num; float num4 = (float)(result2 + result4) / num; float num5 = 1f - (float)result3 / num2; float num6 = 1f - (float)(result3 + result5) / num2; float num7 = val.CharSize.y; if (result >= 65 && result <= 90) { num7 -= T.CapitalLetterNudge; } CharacterInfo item = new CharacterInfo { index = result, uv = new Rect(num3, num6, num4 - num3, num5 - num6), vert = new Rect(0f, num7, (float)result4, (float)(-result5)), width = val.CharSize.x }; list.Add(item); } } if (list.Count == 0) { return null; } Font val2 = new Font { material = new Material(Shader.Find("UI/Default")) { mainTexture = (Texture)(object)fontTexture }, characterInfo = list.ToArray() }; _fixedCharAdvance = val.CharSize.x; _fixedLineHeight = val.CharSize.y; return val2; } private static void DisplayNext() { //IL_0171: Unknown result type (might be due to invalid IL or missing references) string text = _queue.Dequeue(); ((Component)_panel).gameObject.SetActive(true); float num = _fixedCharAdvance ?? ((float)T.FontSize * 0.6f); float num2 = _fixedLineHeight ?? ((float)T.FontSize * 1.2f); int maxCols = Mathf.Max(4, Mathf.FloorToInt(T.MaxWidthFraction * 1920f / (num * T.PixelScale)) - T.PaddingHorizontalCells * 2); text = WrapText(text, maxCols); _text.text = text; string[] array = text.Split(new char[1] { '\n' }); int num3 = 0; string[] array2 = array; foreach (string text2 in array2) { num3 = Math.Max(num3, text2.Length); } int num4 = num3 + T.PaddingHorizontalCells * 2; int num5 = array.Length + T.PaddingVerticalCells * 2; _panel.sizeDelta = new Vector2(Mathf.Min((float)num4 * num * T.PixelScale, T.MaxWidthFraction * 1920f), Mathf.Min((float)num5 * num2 * T.PixelScale, T.MaxHeightFraction * 1080f)); _state = State.FadingIn; _timer = 0f; ApplyTransition(0f); } private static string WrapText(string text, int maxCols) { List list = new List(); string[] array = text.Split(new char[1] { '\n' }); foreach (string text2 in array) { if (text2.Length <= maxCols) { list.Add(text2); continue; } StringBuilder stringBuilder = new StringBuilder(); string[] array2 = text2.Split(new char[1] { ' ' }); foreach (string text3 in array2) { if (stringBuilder.Length == 0) { stringBuilder.Append(text3); continue; } if (stringBuilder.Length + 1 + text3.Length <= maxCols) { stringBuilder.Append(' ').Append(text3); continue; } list.Add(stringBuilder.ToString()); stringBuilder.Clear(); stringBuilder.Append(text3); } if (stringBuilder.Length > 0) { list.Add(stringBuilder.ToString()); } } return string.Join("\n", list); } } public class PopupTuning { public float DisplaySeconds = 3f; public float TransitionSeconds = 0.12f; public float MinScaleY = 0.01f; public int FontSize = 16; public float PixelScale = 0.9f; public float TextVerticalNudgeFraction = 0.48f; public float TopAnchorY = 0.95f; public float MaxWidthFraction = 1f; public float MaxHeightFraction = 0.08f; public int PaddingHorizontalCells = 2; public int PaddingVerticalCells = 1; public float CapitalLetterNudge = -1f; public float BackgroundR; public float BackgroundG = 0.01f; public float BackgroundB = 0.02f; public float BackgroundA = 0.75f; [JsonIgnore] public Color BackgroundColor => new Color(BackgroundR, BackgroundG, BackgroundB, BackgroundA); private static string FilePath => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "PopupTuning.json"); public static PopupTuning Current { get; private set; } = new PopupTuning(); public static void Load() { try { if (File.Exists(FilePath)) { PopupTuning popupTuning = JsonConvert.DeserializeObject(File.ReadAllText(FilePath)); if (popupTuning != null) { Current = popupTuning; return; } } Current = new PopupTuning(); Save(); } catch (Exception arg) { Instance? log = Mod.Log; if (log != null) { log.Error($"PopupTuning.Load failed, using defaults: {arg}"); } Current = new PopupTuning(); } } private static void Save() { File.WriteAllText(FilePath, JsonConvert.SerializeObject((object)Current, (Formatting)1)); } } public static class UnlockState { private static readonly HashSet _unlockedLevelIds = new HashSet(); public static void Unlock(int levelId) { _unlockedLevelIds.Add(levelId); } public static bool IsUnlocked(int levelId) { return _unlockedLevelIds.Contains(levelId); } public static void Clear() { _unlockedLevelIds.Clear(); } } }