using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Archipelago.MultiClient.Net; using Archipelago.MultiClient.Net.BounceFeatures.DeathLink; using Archipelago.MultiClient.Net.Enums; using Archipelago.MultiClient.Net.Helpers; using Archipelago.MultiClient.Net.MessageLog.Messages; using Archipelago.MultiClient.Net.Models; using Archipelago.MultiClient.Net.Packets; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json.Linq; using PulseLib; using PulseLib.Extensions; using PulseLib.Localization; using RDLevelEditor; using RhythmDoctor.Archipelago.Client; using RhythmDoctor.Archipelago.Extensions; using RhythmDoctor.Archipelago.Helpers; using RhythmDoctor.Archipelago.Interfaces; using RhythmDoctor.Archipelago.Patches; using RhythmDoctor.Archipelago.Patches.Gameplay; using RhythmDoctor.Archipelago.Patches.Gameplay.Powerups; using RhythmDoctor.Archipelago.Patches.Gameplay.Traps; using RhythmDoctor.Archipelago.Patches.Menu; using RhythmDoctor.Archipelago.World; using RhythmDoctor.Archipelago.World.Data; using RhythmWeightlifter; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("gg.archipelago.rhythmdoctor")] [assembly: AssemblyConfiguration("Release-BepInEx5")] [assembly: AssemblyFileVersion("0.6.13.0")] [assembly: AssemblyInformationalVersion("0.6.13+71162631dc2f6e169ab8e9362130d32633f21ee2")] [assembly: AssemblyProduct("RhythmDoctor.Archipelago")] [assembly: AssemblyTitle("gg.archipelago.rhythmdoctor")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.6.13.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] [CompilerGenerated] internal sealed class <>z__ReadOnlyArray : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList { int ICollection.Count => _items.Length; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection.Count => _items.Length; T IReadOnlyList.this[int index] => _items[index]; int ICollection.Count => _items.Length; bool ICollection.IsReadOnly => true; T IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } public <>z__ReadOnlyArray(T[] items) { _items = items; } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.CopyTo(Array array, int index) { ((ICollection)_items).CopyTo(array, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return ((IList)_items).Contains(value); } int IList.IndexOf(object value) { return ((IList)_items).IndexOf(value); } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.Add(T item) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } bool ICollection.Contains(T item) { return ((ICollection)_items).Contains(item); } void ICollection.CopyTo(T[] array, int arrayIndex) { ((ICollection)_items).CopyTo(array, arrayIndex); } bool ICollection.Remove(T item) { throw new NotSupportedException(); } int IList.IndexOf(T item) { return ((IList)_items).IndexOf(item); } void IList.Insert(int index, T item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } } [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator { object IEnumerator.Current => _item; T IEnumerator.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection.Count => 1; T IReadOnlyList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection.Count => 1; bool ICollection.IsReadOnly => true; T IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return EqualityComparer.Default.Equals(_item, (T)value); } int IList.IndexOf(object value) { if (!EqualityComparer.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.Add(T item) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } bool ICollection.Contains(T item) { return EqualityComparer.Default.Equals(_item, item); } void ICollection.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection.Remove(T item) { throw new NotSupportedException(); } int IList.IndexOf(T item) { if (!EqualityComparer.Default.Equals(_item, item)) { return -1; } return 0; } void IList.Insert(int index, T item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } } 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 Microsoft.CodeAnalysis { [Embedded] [AttributeUsage(AttributeTargets.All)] [ExcludeFromCodeCoverage] internal sealed class EmbeddedAttribute : Attribute { } } namespace Archipelago.MultiClient.Net { [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = false)] internal sealed class DataStoragePropertyAttribute : Attribute { public string? SessionVariable { get; } public Scope Scope { get; } public string Key { get; } public DataStoragePropertyAttribute(string sessionVariable, Scope scope, string key) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) SessionVariable = sessionVariable; Scope = scope; Key = key; } public DataStoragePropertyAttribute(string sessionVariable, string key) : this(sessionVariable, (Scope)0, key) { } } } namespace RhythmDoctor.Archipelago { internal static class Configuration { internal enum DeathLinkConfig { FollowSlot, On, Off } private static ConfigEntry _deathLink; private static ConfigEntry _autoReconnectMaxRetries; private static ConfigEntry _remoteTrapClearsTimeout; private static ConfigEntry _slotToUse; internal static void Bind(ConfigFile config) { Plugin.Logger.LogInfo((object)"Binding configuration"); _deathLink = config.Bind("Gameplay", "DeathLink", DeathLinkConfig.FollowSlot, "Whether to enable DeathLink when connected to an Archipelago slot.\nSee the respective Game Page in Archipelago for more info."); _autoReconnectMaxRetries = config.Bind("zzzDebugDoNotTouchUnlessAsked", "AutoReconnectMaxRetries", 3, "How many times to attempt to reconnect to an Archipelago server before the client is considered unsalvagable and the Client is abandoned."); _remoteTrapClearsTimeout = config.Bind("zzzDebugDoNotTouchUnlessAsked", "RemoteTrapClearsTimeout", 3000, "How long to wait in milliseconds until getting remote trap clear status times out and defaults to 0/last known good value."); _slotToUse = config.Bind("zzzDebugDoNotTouchUnlessAsked", "SlotToUse", 0, "Slot to use for Archipelago."); } internal static async Task GetDeathLink() { if (Plugin.Client == null || Plugin.Client.Session == null) { return _deathLink.Value; } return (!(await ((IDataStorageWrapper)Plugin.Client.Session.DataStorage).GetRaceModeAsync())) ? _deathLink.Value : DeathLinkConfig.FollowSlot; } internal static int GetSlotToUse() { return _slotToUse.Value; } internal static int GetAutoReconnectMaxRetries() { return _autoReconnectMaxRetries.Value; } internal static int GetRemoteTrapClearsTimeout() { return _remoteTrapClearsTimeout.Value; } } internal static class Paths { internal static string Assembly; internal static string Assets; internal static string Localization; internal static string WardIcons; internal static void PopulatePaths() { Assembly = Path.GetDirectoryName(((BaseUnityPlugin)Plugin.Instance).Info.Location); Assets = Path.Combine(Assembly, "Assets"); Localization = Path.Combine(Assets, "Localization"); WardIcons = Path.Combine(Assets, "WardIcons"); } } [BepInPlugin("gg.archipelago.rhythmdoctor", "RhythmDoctor.Archipelago", "0.6.13")] [BepInDependency("io.github.nonperforming.pulse", "0.0.6")] [BepInProcess("Rhythm Doctor.exe")] public class Plugin : BaseUnityPlugin { internal static RhythmDoctor.Archipelago.Client.Client Client = null; internal static ManualLogSource Logger = null; internal static readonly Random Random = new Random(); internal static readonly ConcurrentQueue ToExecuteOnMainThread = new ConcurrentQueue(); internal static Plugin Instance = null; internal const string PATCH_ID_ALWAYS_ACTIVE = "gg.archipelago.rhythmdoctor"; internal const string PATCH_ID_ARCHIPELAGO_MENU = "gg.archipelago.rhythmdoctor.cls"; internal const string PATCH_ID_POST_LOGIN = "gg.archipelago.rhythmdoctor.post"; internal const string PATCH_ID_TRAP = "gg.archipelago.rhythmdoctor.trap"; internal const string PATCH_ID_SLEEVE_PAINT = "gg.archipelago.rhythmdoctor.sleevepaint"; private static readonly Type[] AlwaysActivePatches = new Type[3] { typeof(ArchipelagoMainMenuOptionPatch), typeof(VersionTextPatch), typeof(scnGameExtensions) }; private static readonly Type[] PostLoginPatches = new Type[15] { typeof(Act5Patch), typeof(ClearLocationPatch), typeof(DeathLinkPatch), typeof(LevelSelectVisualFixesPatch), typeof(RhythmDogtorLevelPatch), typeof(RhythmWeightlifterPatch), typeof(RunningCharactersPatch), typeof(SkipCutscenePatch), typeof(SkipTutorialPatch), typeof(StateReplicationPatch), typeof(TrapManagerPatch), typeof(UnlockItemPatch), typeof(WelcomeBackPatch), typeof(SavingPatch), typeof(UnapplyPatchesPatch) }; private static readonly Type CustomLoginScreenPatch = typeof(ArchipelagoLoginPatch); private bool _quitting; private void Awake() { Instance = this; Logger = ((BaseUnityPlugin)this).Logger; Logger.LogInfo((object)"gg.archipelago.rhythmdoctor v0.6.13 loading"); Configuration.Bind(((BaseUnityPlugin)this).Config); Logger.LogInfo((object)"Setting up paths"); Paths.PopulatePaths(); Logger.LogInfo((object)("Registering custom localization (" + Paths.Localization + ")")); CustomLocalizationHelper.SearchAndRegisterDirectory(Paths.Localization); Logger.LogInfo((object)"Applying always active patches"); ApplyPatches("gg.archipelago.rhythmdoctor", AlwaysActivePatches); Logger.LogInfo((object)"gg.archipelago.rhythmdoctor v0.6.13 loaded"); } private void Start() { Version version = new Version("1.1.1"); Version version2 = new Version(Application.version); if (Releases.releaseNumber < 42 || version2 < version) { Logger.LogWarning((object)"================================================================================"); Logger.LogWarning((object)"| ╭─╮╷ ╭─╴╭─╮╭─╮╭─╴ ╷ ╷╭─╮╶┬╮╭─╮╶┬╴╭─╴ ╷ ╷╭─╮╷ ╷╭─╮ ╭─╴╭─╮╭┬╮╭─╴╷╷╷ |"); Logger.LogWarning((object)"| ├─╯│ ├╴ ├─┤╰─╮├╴ │ │├─╯ ││├─┤ │ ├╴ ╰┬╯│ ││ │├┬╯ │╶╮├─┤│││├╴ ╵╵╵ |"); Logger.LogWarning((object)"| ╵ ╰─╴╰─╴╵ ╵╰─╯╰─╴ ╰─╯╵ ╶┴╯╵ ╵ ╵ ╰─╴ ╵ ╰─╯╰─╯╵╰╴ ╰─╯╵ ╵╵ ╵╰─╴╵╵╵ |"); Logger.LogWarning((object)"================================================================================"); Logger.LogWarning((object)string.Format("This version of the mod (v{0}) was built for: v{1} (release {2}, commit {3}, date {4})", "0.6.13", version, 42, "e43207b", "2026/06/15 12:42 AM")); Logger.LogWarning((object)$"Your version is: v{Application.version} (release {Releases.releaseNumber}, commit {Releases.buildCommit}, date {Releases.buildDate})"); Logger.LogWarning((object)"================================================================================"); } else if (Releases.releaseNumber > 42 || version2 > version) { Logger.LogWarning((object)"================================================================================="); Logger.LogWarning((object)"| ╭─╮╷ ╭─╴╭─╮╭─╮╭─╴ ╭─╮╭─╮╭─╴╭╮╷ ╭─╮╭╮╷ ╷╭─╮╭─╮╷ ╷╭─╴╷╷╷ |"); Logger.LogWarning((object)"| ├─╯│ ├╴ ├─┤╰─╮├╴ │ │├─╯├╴ │╰┤ ├─┤│╰┤ │╰─╮╰─╮│ │├╴ ╵╵╵ |"); Logger.LogWarning((object)"| ╵ ╰─╴╰─╴╵ ╵╰─╯╰─╴ ╰─╯╵ ╰─╴╵ ╵ ╵ ╵╵ ╵ ╵╰─╯╰─╯╰─╯╰─╴╵╵╵ |"); Logger.LogWarning((object)"================================================================================="); Logger.LogWarning((object)string.Format("This version of the mod (v{0}) was built for: v{1} (release {2}, commit {3}, date {4})", "0.6.13", version, 42, "e43207b", "2026/06/15 12:42 AM")); Logger.LogWarning((object)$"Your version is: v{Application.version} (release {Releases.releaseNumber}, commit {Releases.buildCommit}, date {Releases.buildDate})"); Logger.LogWarning((object)null); Logger.LogWarning((object)"First, please check that there is an updated version here: https://github.com/nonperforming/RhythmDoctor.Archipelago/releases. Remember to update Pulse (https://github.com/nonperforming/Pulse/releases) as well."); Logger.LogWarning((object)$"If there is no update yet, please open an issue at https://github.com/nonperforming/RhythmDoctor.Archipelago/issues/new?labels=\"plugin outdated\"&title=v{Application.version}+support&body=Release:+{Releases.releaseNumber}+/+Commit:+{Releases.buildCommit}+/+Date:+{Releases.buildDate}&assignees=nonperforming"); Logger.LogWarning((object)"================================================================================="); } } private void Update() { Action result; while (ToExecuteOnMainThread.TryDequeue(out result)) { result(); } } private static void ApplyPatches(string id, params Type[] patches) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown Logger.LogInfo((object)("Applying patches as " + id)); Harmony val = new Harmony(id); foreach (Type type in patches) { Logger.LogInfo((object)("Applying " + type.Name)); val.PatchAll(type); } } internal static void ApplyGameplayPatches() { Logger.LogInfo((object)"Applying gameplay patches"); ApplyPatches("gg.archipelago.rhythmdoctor.post", PostLoginPatches); ApplyPatches("gg.archipelago.rhythmdoctor.sleevepaint", typeof(LockSleevePaintPatch)); } internal static void UnapplyGameplayPatches() { Logger.LogInfo((object)"Unapplying gameplay patches"); Harmony.UnpatchID("gg.archipelago.rhythmdoctor.post"); Harmony.UnpatchID("gg.archipelago.rhythmdoctor.sleevepaint"); } internal static void ApplyArchipelagoMenuPatch() { Logger.LogInfo((object)"Applying Archipelago menu patch"); Harmony.CreateAndPatchAll(CustomLoginScreenPatch, "gg.archipelago.rhythmdoctor.cls"); } internal static void UnapplyArchipelagoMenuPatch() { Logger.LogInfo((object)"Unapplying Archipelago menu patch"); Harmony.UnpatchID("gg.archipelago.rhythmdoctor.cls"); } private void OnApplicationQuit() { Logger.LogDebug((object)"Quitting..."); _quitting = true; } private void OnDestroy() { if (!_quitting) { Logger.LogWarning((object)"Tearing down plugin. This is unsupported!"); UnapplyPatchesPatch.TearDownClientPluginPatch(); Harmony.UnpatchID("gg.archipelago.rhythmdoctor"); Harmony.UnpatchID("gg.archipelago.rhythmdoctor.cls"); Harmony.UnpatchID("gg.archipelago.rhythmdoctor.post"); Harmony.UnpatchID("gg.archipelago.rhythmdoctor.trap"); } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "gg.archipelago.rhythmdoctor"; public const string PLUGIN_NAME = "RhythmDoctor.Archipelago"; public const string PLUGIN_VERSION = "0.6.13"; } } namespace RhythmDoctor.Archipelago.World { internal enum Act { None, Act1, Act2, Act3, Act4, Act5, Act6, Act7 } internal static class Bindings { internal const string GAME = "Rhythm Doctor"; internal const long SLEEVE_PAINT_ITEM_ID = 56L; internal const Level RHYTHM_DOGTOR_LEVEL = (Level)84; internal static readonly Dictionary LevelToStage = new Dictionary { { (Level)2, new RegularStage(Act.Act1, 1L, 2L, 3L) }, { (Level)3, new RegularStage(Act.Act1, 4L, 5L, 6L) }, { (Level)4, new RegularStage(Act.Act1, 7L, 8L, 9L) }, { (Level)5, new RegularStage(Act.Act1, 10L, 11L, 12L) }, { (Level)53, new RegularStage(Act.Act1, 13L, 14L, 15L) }, { (Level)54, new RegularStage(Act.Act1, 16L, 17L, 18L) }, { (Level)18, new RegularStage(Act.Act3, 21L, 22L, 23L) }, { (Level)19, new RegularStage(Act.Act3, 24L, 25L, 26L) }, { (Level)20, new RegularStage(Act.Act3, 27L, 28L, 29L) }, { (Level)21, new RegularStage(Act.Act3, 30L, 31L, 32L) }, { (Level)22, new RegularStage(Act.Act3, 33L, 34L, 35L) }, { (Level)23, new RegularStage(Act.Act3, 36L, 37L, 38L) }, { (Level)26, new BossStage(Act.Act3, 39L, null, 40L) }, { (Level)84, new BossStage(Act.Act3, 41L, null, 42L) }, { (Level)8, new RegularStage(Act.Act2, 43L, 44L, 45L) }, { (Level)9, new RegularStage(Act.Act2, 46L, 47L, 48L) }, { (Level)10, new RegularStage(Act.Act2, 49L, 50L, 51L) }, { (Level)11, new RegularStage(Act.Act2, 52L, 53L, 54L) }, { (Level)12, new RegularStage(Act.Act2, 55L, 56L, 57L) }, { (Level)13, new RegularStage(Act.Act2, 58L, 59L, 60L) }, { (Level)14, new RegularStage(Act.Act2, 171L, 172L, 61L) }, { (Level)15, new RegularStage(Act.Act2, 173L, 174L, 62L) }, { (Level)17, new RegularStage(Act.Act2, 63L, 64L, 65L) }, { (Level)48, new BossStage(Act.Act7, 66L, 67L, 68L) }, { (Level)27, new RegularStage(Act.Act4, 71L, 72L, 73L) }, { (Level)28, new RegularStage(Act.Act4, 74L, 75L, 76L) }, { (Level)29, new RegularStage(Act.Act4, 77L, 78L, 79L) }, { (Level)30, new RegularStage(Act.Act4, 80L, 81L, 82L) }, { (Level)31, new RegularStage(Act.Act4, 83L, 84L, 85L) }, { (Level)32, new RegularStage(Act.Act4, 86L, 87L, 88L) }, { (Level)33, new RegularStage(Act.Act4, 89L, 90L, 91L) }, { (Level)34, new RegularStage(Act.Act4, 92L, 93L, 94L) }, { (Level)35, new RegularStage(Act.Act5, 97L, 98L, 99L) }, { (Level)36, new RegularStage(Act.Act5, 100L, 101L, 102L) }, { (Level)37, new RegularStage(Act.Act5, 103L, 104L, 105L) }, { (Level)38, new RegularStage(Act.Act5, 106L, 107L, 108L) }, { (Level)39, new RegularStage(Act.Act5, 175L, 176L, 109L) }, { (Level)40, new RegularStage(Act.Act5, 110L, 111L, 112L) }, { (Level)43, new RegularStage(Act.Act6, 126L, 127L, 128L) }, { (Level)45, new RegularStage(Act.Act6, 129L, 130L, 131L) }, { (Level)47, new RegularStage(Act.Act7, 135L, 136L, 137L) }, { (Level)58, new RegularStage(Act.None, 144L, 145L, 146L) }, { (Level)59, new RegularStage(Act.None, 147L, 148L, 149L) }, { (Level)56, new RegularStage(Act.None, 150L, 151L, 152L) }, { (Level)57, new RegularStage(Act.None, 153L, 154L, 155L) }, { (Level)61, new RegularStage(Act.None, 156L, 157L, 158L) }, { (Level)62, new RegularStage(Act.None, 159L, 160L, 161L) }, { (Level)63, new RegularStage(Act.None, 162L, 163L, 164L) }, { (Level)55, new RegularStage(Act.None, 165L, 166L, 167L) }, { (Level)25, new RegularStage(Act.None, 168L, 169L, 170L) }, { (Level)60, new RegularStage(Act.None, 177L, 178L, 179L) }, { (Level)6, new BossStage(Act.Act1, 19L, null, 20L) }, { (Level)16, new BossStage(Act.Act2, 69L, null, 70L) }, { (Level)7, new BossStage(Act.Act4, 95L, null, 96L) }, { (Level)42, new BossStage(Act.Act5, 123L, 124L, 125L) }, { (Level)46, new BossStage(Act.Act6, 132L, null, 134L) }, { (Level)49, new BossStage(Act.Act7, 138L, 139L, 140L) }, { (Level)50, new BossStage(Act.Act7, 141L, 142L, 143L) } }; internal static readonly Dictionary ItemIdToLevel = new Dictionary { { 1L, (Level)2 }, { 2L, (Level)3 }, { 3L, (Level)4 }, { 4L, (Level)5 }, { 5L, (Level)53 }, { 6L, (Level)54 }, { 7L, (Level)18 }, { 8L, (Level)19 }, { 9L, (Level)20 }, { 10L, (Level)21 }, { 11L, (Level)22 }, { 12L, (Level)23 }, { 13L, (Level)8 }, { 14L, (Level)9 }, { 15L, (Level)10 }, { 16L, (Level)11 }, { 17L, (Level)12 }, { 18L, (Level)13 }, { 19L, (Level)14 }, { 20L, (Level)15 }, { 21L, (Level)17 }, { 22L, (Level)48 }, { 23L, (Level)27 }, { 24L, (Level)28 }, { 25L, (Level)29 }, { 26L, (Level)30 }, { 27L, (Level)31 }, { 28L, (Level)32 }, { 29L, (Level)33 }, { 30L, (Level)34 }, { 31L, (Level)35 }, { 32L, (Level)36 }, { 33L, (Level)37 }, { 34L, (Level)38 }, { 35L, (Level)39 }, { 36L, (Level)40 }, { 37L, (Level)41 }, { 38L, (Level)43 }, { 39L, (Level)45 }, { 40L, (Level)47 }, { 41L, (Level)58 }, { 42L, (Level)59 }, { 43L, (Level)56 }, { 44L, (Level)57 }, { 45L, (Level)61 }, { 46L, (Level)62 }, { 47L, (Level)63 }, { 48L, (Level)55 }, { 49L, (Level)25 }, { 67L, (Level)60 } }; internal static readonly Dictionary TrapItemIdToLevel = new Dictionary { { 60L, typeof(FragileHeartTrapPatch) }, { 61L, typeof(HardDifficultyTrapPatch) }, { 62L, typeof(ScrambleCharactersTrapPatch) }, { 63L, typeof(ScrambleBeatsoundsTrapPatch) }, { 64L, typeof(ScrambleHitsoundsTrapPatch) }, { 65L, typeof(ChilliSpeedTrapPatch) }, { 66L, typeof(GhostTapTrapPatch) }, { 57L, typeof(StrongHeartPowerupPatch) }, { 58L, typeof(EasyDifficultyPowerupPatch) }, { 59L, typeof(IceSpeedTrapPatch) } }; internal static readonly Dictionary KeyItemIdToWard = new Dictionary { { 50L, Region.SVTWard }, { 51L, Region.Train }, { 52L, Region.PhysiotherapyWard }, { 55L, Region.RecordsRoom }, { 53L, Region.Basement }, { 54L, Region.GardenRoom } }; internal static readonly long[] RhythmWeightlifterStageToLocationID = new long[12] { 113L, 114L, 115L, 116L, 117L, 118L, 119L, 120L, 121L, 122L, 180L, 181L }; internal static readonly IReadOnlyCollection Traps = TrapItemIdToLevel.Values; internal static readonly IReadOnlyCollection Levels = LevelToStage.Keys; internal static readonly Dictionary LevelToAct = LevelToStage.ToDictionary, Level, Act>((KeyValuePair pair) => pair.Key, (KeyValuePair pair) => pair.Value.StageAct); internal static readonly Dictionary RegionToKeyID = KeyItemIdToWard.ToDictionary((KeyValuePair pair) => pair.Value, (KeyValuePair pair) => pair.Key); internal static readonly Dictionary> LevelsInAct = (from pair in LevelToStage group pair by pair.Value.StageAct).ToDictionary((IGrouping> grouping) => grouping.Key, (IGrouping> grouping) => grouping.Select((KeyValuePair c) => c.Key)); internal static readonly Level[] LevelsThatDoNotUnlockBoss = (Level[])(object)new Level[1] { (Level)41 }; internal static readonly Dictionary ActBoss = new Dictionary { { Act.Act1, (Level[])(object)new Level[1] { (Level)6 } }, { Act.Act2, (Level[])(object)new Level[1] { (Level)16 } }, { Act.Act3, (Level[])(object)new Level[2] { (Level)26, (Level)84 } }, { Act.Act4, (Level[])(object)new Level[1] { (Level)7 } }, { Act.Act5, (Level[])(object)new Level[1] { (Level)42 } }, { Act.Act6, (Level[])(object)new Level[1] { (Level)46 } }, { Act.Act7, (Level[])(object)new Level[2] { (Level)49, (Level)50 } } }; } internal enum Region { MainWard, SVTWard, Train, PhysiotherapyWard, RecordsRoom, Basement, GardenRoom } } namespace RhythmDoctor.Archipelago.World.Data { internal abstract class BaseStage { internal Act StageAct { get; init; } internal abstract IEnumerable GetLocationsToClear(Rank rank); } internal class BossStage : BaseStage { internal long ClearLocation { get; init; } internal long? CompletePlusLocation { get; init; } internal long PerfectLocation { get; init; } internal BossStage(Act act, long clearLocation, long? completePlusLocation, long perfectLocation) { base.StageAct = act; ClearLocation = clearLocation; CompletePlusLocation = completePlusLocation; PerfectLocation = perfectLocation; } internal override IEnumerable GetLocationsToClear(Rank rank) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) Plugin.Logger.LogDebug((object)$"Getting locations to clear for rank {rank.internalValue}"); List list = new List(); if (((Rank)(ref rank)).perfected) { list.Add(PerfectLocation); } if (CompletePlusLocation.HasValue && (((Rank)(ref rank)).perfected || scnGame.instance.GetPassedLevelWithoutCheckpoints())) { list.Add(CompletePlusLocation.Value); } list.Add(ClearLocation); return list.AsReadOnly(); } } internal class RegularStage : BaseStage { internal long? BRankLocation { get; init; } internal long? ARankLocation { get; init; } internal long SRankLocation { get; init; } internal RegularStage(Act act, long? bRankLocation, long? aRankLocation, long sRankLocation) { base.StageAct = act; BRankLocation = bRankLocation; ARankLocation = aRankLocation; SRankLocation = sRankLocation; } internal override IEnumerable GetLocationsToClear(Rank rank) { //IL_000a: 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) Plugin.Logger.LogDebug((object)$"Getting locations to clear for rank {rank.internalValue}"); List list = new List(); switch (Rank.op_Implicit(rank)) { case -15: case 5: case 15: list.Add(SRankLocation); goto case -14; case -14: case 4: case 14: if (ARankLocation.HasValue) { list.Add(ARankLocation.Value); } goto case -13; case -13: case 3: case 13: if (BRankLocation.HasValue) { list.Add(BRankLocation.Value); } break; } return list.AsReadOnly(); } } } namespace RhythmDoctor.Archipelago.Patches { [HarmonyPatch(typeof(ArmSkin))] internal static class LockSleevePaintPatch { [HarmonyPatch("Load")] [HarmonyPrefix] private static void LoadDefaultSleevePatch(ref ArmSkin __instance, ref bool __runOriginal) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_003b: 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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Invalid comparison between Unknown and I4 //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: 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_010f: Unknown result type (might be due to invalid IL or missing references) __runOriginal = false; int num = Plugin.Random.Next(0, 2); RDPlayer player = __instance.player; if ((int)player != 0) { if ((int)player == 1) { __instance.skinColor = RDConstants.data.skinColors[RDConstants.data.defaultP2SkinColor[num]]; __instance.sleeveColor = RDConstants.data.sleeveColors[RDConstants.data.defaultP2SleeveColor[num]]; __instance.nailsColor = RDConstants.data.nailColors[RDConstants.data.defaultP2NailColor[num]]; __instance.palmLightness = RDConstants.data.defaultP2PalmLightness[num]; __instance.drawing = RDConstants.data.emptyTexture; } } else { __instance.skinColor = RDConstants.data.skinColors[RDConstants.data.defaultP1SkinColor[num]]; __instance.sleeveColor = RDConstants.data.sleeveColors[RDConstants.data.defaultP1SleeveColor[num]]; __instance.nailsColor = RDConstants.data.nailColors[RDConstants.data.defaultP1NailColor[num]]; __instance.palmLightness = RDConstants.data.defaultP1PalmLightness[num]; __instance.drawing = RDConstants.data.emptyTexture; } } } [HarmonyPatch(typeof(RDVersionText))] internal static class VersionTextPatch { [HarmonyPatch("SetPage")] [HarmonyPostfix] private static void AppendClientVersionPatch(RDVersionText __instance) { Text text = __instance.text; string text2 = (text.text += " / Archipelago v0.6.13"); string text4 = text2; __instance.text.text = text4; } } [HarmonyPatch] internal static class SavingPatch { [HarmonyPatch(typeof(PlayerPrefsJson), "Save")] [HarmonyPatch(typeof(PlayerPrefsJson), "SaveBackup")] [HarmonyPrefix] private static void DisableSavingToFilePatch(ref bool __runOriginal, ref PlayerPrefsJson __instance) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 __runOriginal = (int)__instance.fileType == 0; } [HarmonyPatch(typeof(ArmSkin), "GetDrawingPath")] [HarmonyPrefix] private unsafe static void RedirectSleevePaintFilePatch(RDPlayer player, ref string __result, ref bool __runOriginal) { __runOriginal = false; __result = Path.Combine(PlayerPrefsJson.GetFileFolderPath(), "scribble" + ((object)(*(RDPlayer*)(&player))/*cast due to .constrained prefix*/).ToString() + "_AP.png"); } } [HarmonyPatch] internal static class UnapplyPatchesPatch { [HarmonyPatch(typeof(scnMenu), "Awake")] [HarmonyPatch(typeof(scnBase), "GoToMainMenu")] [HarmonyPrefix] internal static void TearDownClientPluginPatch() { Plugin.Logger.LogInfo((object)"Tearing down client plugin"); Plugin.UnapplyGameplayPatches(); Persistence.Load(); try { scnMenu.instance.slots[Configuration.GetSlotToUse()].LoadSlotData(); Plugin.Logger.LogDebug((object)"Reloaded slot data"); } catch (NullReferenceException) { Plugin.Logger.LogDebug((object)"Not in main menu, could not load slot data"); } Plugin.Client?.Dispose(); Plugin.Client = null; } } } namespace RhythmDoctor.Archipelago.Patches.Menu { [HarmonyPatch] internal static class ArchipelagoLoginPatch { [HarmonyPatch(typeof(scnCLS), "Start")] [HarmonyPostfix] private static void ConstructArchipelagoMenuPatch(scnCLS __instance) { Plugin.Logger.LogInfo((object)"Renaming custom level ward items"); Plugin.Logger.LogInfo((object)"Renaming LED sign"); GameObject.Find("Canvas/Ward Container/LEDSign Container/WardTitle Text").GetComponent().text = RDString.Get("archipelago.connectScreenTitle"); Plugin.Logger.LogInfo((object)"Renaming ward options"); WardOption val = __instance.wardOptions.Find((WardOption wardOption) => (int)wardOption.name == 0); WardOption val2 = __instance.wardOptions.Find((WardOption wardOption) => (int)wardOption.name == 1); WardOption? obj = __instance.wardOptions.Find((WardOption wardOption) => (int)wardOption.name == 2); __instance.ChangeToImportOption(); if (val != null) { RectTransform rect = val.rect; if (rect != null) { Transform parent = ((Component)rect).transform.parent; if (parent != null) { ((Component)parent).gameObject.SetActive(false); } } __instance.wardOptions.Remove(val); } if (val2 != null) { RectTransform rect2 = val2.rect; if (rect2 != null) { Transform parent2 = ((Component)rect2).transform.parent; if (parent2 != null) { ((Component)parent2).gameObject.SetActive(false); } } __instance.wardOptions.Remove(val2); } Plugin.Logger.LogInfo((object)"Renaming Import to Archipelago option"); Transform obj2 = ((Transform)obj.rect).Find("Button"); ((Component)obj2.Find("Icon Image")).GetComponent().sprite = AssetHelper.LoadSprite("WardIcons", "Archipelago.png"); ((Component)obj2.Find("Text")).GetComponent().text = RDString.Get("archipelago.loginButton"); Plugin.Logger.LogInfo((object)"Disabling DragAndDrop"); __instance.levelImporter.dragAndDrop.SetActive(false); __instance._currentWardOptionIndex = 0; } [HarmonyPatch(typeof(LevelImporter), "Install")] [HarmonyPostfix] private static IEnumerator OverrideInstallButtonPatch(IEnumerator result, LevelImporter __instance) { Plugin.Logger.LogInfo((object)"Locking input"); ((RDBase)__instance).cls.CLSPlaySound("sndImportInstallButtonClick", 1f, "CustomLevelSelect", 1f, 0f); __instance.ToggleInsertUrlContainer(false, true, false); __instance.CurrentContentName = (ContentName)2; __instance.stoppedInstallCoroutine = false; __instance.CanToggleClearButton = false; __instance.levelsToInstall = new List(__instance.toInstallIS.levels); foreach (ImportLevel item in __instance.levelsToInstall) { item.canToggleRemoveButton = false; } yield return null; ((Selectable)__instance.stopButton).interactable = false; string text = ((Component)((Component)__instance).transform.Find("screen/Contents/InsertURL Container/URL InputField/Text")).GetComponent().text; string[] array = (from text2 in text.Split('\n') select text2.Trim()).ToArray(); Plugin.Logger.LogInfo((object)("Input: '" + GeneralExtensions.Join((IEnumerable)array, (Func)null, ", ") + "' (raw: " + Convert.ToBase64String(Encoding.UTF8.GetBytes(text)))); if (array.Length < 2) { Plugin.Logger.LogError((object)"URL or Name not input, bailing out"); __instance.CurrentContentName = (ContentName)3; ((RDBase)__instance).cls.CLSPlaySound("sndImportInstallFinish", 1f, "CustomLevelSelect", 1f, 0f); __instance.AddLevelToErrorSection(__instance.levelsToInstall[0], RDString.Get("archipelago.login.fail.missingInformation")); yield break; } string url = array[0]; string name = array[1]; string password = null; try { password = array[2]; } catch (IndexOutOfRangeException) { } Plugin.Logger.LogInfo((object)("URL: " + url + ", Slot Name: " + name)); if (Utility.IsNullOrWhiteSpace(url) || Utility.IsNullOrWhiteSpace(name)) { Plugin.Logger.LogError((object)"Invalid information, bailing out"); } else { Plugin.Logger.LogInfo((object)"Creating client"); Plugin.Client = new RhythmDoctor.Archipelago.Client.Client(); Task login = Task.Run(() => Plugin.Client.CreateSessionAndConnect(url, name, password)); yield return (object)new WaitUntil((Func)(() => login.IsCompleted)); if (login.IsCanceled || login.IsFaulted) { string arg = (login.IsFaulted ? login.Exception.ToString() : "false"); Plugin.Logger.LogError((object)$"Login has cancelled or faulted (cancel: {login.IsCanceled} / fault: {arg})"); } else { LoginResult result2 = login.Result; if (result2 is LoginSuccessful) { Plugin.Logger.LogInfo((object)"Logged in!"); ((RDBase)__instance).cls.CLSPlaySound("sndImportInstallFinish", 1f, "CustomLevelSelect", 1f, 0f); yield return null; UnpatchMenuPatch(); while (!Plugin.Client.Setup) { yield return (object)new WaitForSecondsRealtime(1f); } Plugin.Logger.LogInfo((object)"Heading to Level Select..."); scnBase.GoToScene("scnLevelSelect"); yield break; } LoginFailure val = (LoginFailure)(object)((result2 is LoginFailure) ? result2 : null); if (val == null) { Plugin.Logger.LogWarning((object)"Got to the end of OverrideInstallButtonPatch - this should never happen!"); yield break; } Plugin.Logger.LogError((object)("Got LoginFailure: " + string.Join(", ", val.ErrorCodes) + " / " + string.Join(", ", val.Errors))); } Plugin.Logger.LogError((object)"Login failed (Login)"); UnapplyPatchesPatch.TearDownClientPluginPatch(); Plugin.Client?.Dispose(); Plugin.Client = null; } __instance.CurrentContentName = (ContentName)3; ((RDBase)__instance).cls.CLSPlaySound("sndImportInstallFinish", 1f, "CustomLevelSelect", 1f, 0f); ((Component)((Component)__instance).transform.Find("screen/Contents/Top Panel/Title Text")).GetComponent().text = RDString.Get("archipelago.login.fail.title"); } [HarmonyPatch(typeof(scnCLS), "Exit")] [HarmonyPostfix] private static void UnpatchMenuPatch() { Plugin.UnapplyArchipelagoMenuPatch(); } [HarmonyPatch(typeof(scnCLS), "SelectWardOption")] [HarmonyPostfix] private static void CustomSelectOptionPatch(ref bool __runOriginal, scnCLS __instance) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 __runOriginal = false; WardOptionName name = __instance.CurrentWardOption.name; if ((int)name <= 1) { Plugin.Logger.LogWarning((object)"Library/Steam Workshop selected in Archipelago login screen"); } else { __runOriginal = true; } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPostfix] private static void ArchipelagoImportScreenPatch() { GameObject gameObject = ((Component)((Component)((Component)scnCLS.instance.levelImporter).gameObject.transform.Find("screen")).gameObject.transform.Find("Contents")).gameObject; GameObject gameObject2 = ((Component)gameObject.transform.Find("InsertURL Container")).gameObject; GameObject gameObject3 = ((Component)gameObject2.transform.Find("Add Button")).gameObject; Text component = ((Component)((Component)gameObject2.transform.Find("URL InputField")).gameObject.transform.Find("Placeholder")).gameObject.GetComponent(); Text component2 = ((Component)gameObject2.transform.Find("Instructions")).GetComponent(); Text component3 = ((Component)gameObject3.transform.Find("Text")).GetComponent(); ((UnityEvent)((Component)gameObject.transform.Find("Draggable Content/AddURL Button")).GetComponent