Due to update 2.4.3, some mods may no longer function. FixedConfig may be necessary.
Decompiled source of FixedMoreBopl v0.0.13
BoplMorePlayersLocal8.dll
Decompiled an hour agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.Utilities; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("BoplMorePlayersLocal8")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyDescription("8-player local couch multiplayer expansion for Bopl Battle.")] [assembly: AssemblyFileVersion("0.0.13.0")] [assembly: AssemblyInformationalVersion("0.0.13+acf943e98315d25a64ef744bf4a13c18070cd98b")] [assembly: AssemblyProduct("BoplMorePlayersLocal8")] [assembly: AssemblyTitle("BoplMorePlayersLocal8")] [assembly: AssemblyVersion("0.0.13.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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 BoplMorePlayersLocal8 { [BepInPlugin("com.geddesworks.fixedmorebopl", "FixedMoreBopl", "0.1.0")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.geddesworks.fixedmorebopl"; public const string PluginName = "FixedMoreBopl"; public const string PluginVersion = "0.1.0"; internal static ConfigEntry<int> TargetLocalPlayers; internal static ConfigEntry<bool> EnableDiagnostics; internal static ConfigEntry<bool> VerboseDiagnostics; internal static ConfigEntry<bool> DisableReplayRecording; internal static ConfigEntry<bool> RepositionCharacterSelectBoxes; internal static ConfigEntry<bool> CompressMidRoundSelectorSpacing; internal static ConfigEntry<bool> IncreaseCameraZoomForCrowdedMatches; internal static ConfigEntry<bool> RelaxColorUniquenessWhenFull; internal static ConfigEntry<bool> ExpandDrawWinnerUiSlots; private Harmony? _harmony; internal static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal static int TargetPlayerCount => Mathf.Clamp(TargetLocalPlayers.Value, 4, 8); internal static int SlotArrayLength => TargetPlayerCount + 1; private void Awake() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; BindConfig(); ApplyEarlyRuntimeOverrides(); SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; _harmony = new Harmony("com.geddesworks.fixedmorebopl"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); string[] array = (from method in _harmony.GetPatchedMethods().Where(delegate(MethodBase method) { Patches patchInfo = Harmony.GetPatchInfo(method); return patchInfo != null && patchInfo.Owners.Contains("com.geddesworks.fixedmorebopl"); }) select method.DeclaringType?.FullName + "." + method.Name into name orderby name select name).ToArray(); Log.LogInfo((object)"FixedMoreBopl 0.1.0 loaded."); Log.LogInfo((object)$"Target local players: {TargetPlayerCount}. Slot array length: {SlotArrayLength}."); Log.LogInfo((object)string.Format("Patched {0} methods:\n - {1}", array.Length, string.Join("\n - ", array))); RuntimeSnapshot.LogGlobalState("Plugin.Awake", verbose: false); } private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; SceneManager.sceneUnloaded -= OnSceneUnloaded; } private static void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) RuntimeSnapshot.Log($"Scene loaded: '{((Scene)(ref scene)).name}' ({mode}).", verbose: false); RuntimeSnapshot.LogGlobalState("SceneLoaded:" + ((Scene)(ref scene)).name, verbose: true); } private static void OnSceneUnloaded(Scene scene) { RuntimeSnapshot.Log("Scene unloaded: '" + ((Scene)(ref scene)).name + "'.", verbose: false); } private void BindConfig() { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown TargetLocalPlayers = ((BaseUnityPlugin)this).Config.Bind<int>("General", "TargetLocalPlayers", 8, new ConfigDescription("Target number of local couch players.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(4, 8), Array.Empty<object>())); EnableDiagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "EnableDiagnostics", true, "Enable runtime diagnostics for joins, arrays, player counts, and scene transitions."); VerboseDiagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "VerboseDiagnostics", false, "Log additional verbose state snapshots."); DisableReplayRecording = ((BaseUnityPlugin)this).Config.Bind<bool>("Stability", "DisableReplayRecording", true, "Disable replay recording to avoid 4-player replay packet assumptions during local 8-player testing."); RepositionCharacterSelectBoxes = ((BaseUnityPlugin)this).Config.Bind<bool>("Layout", "RepositionCharacterSelectBoxes", true, "Re-layout character select boxes into a compact multi-row arrangement when expanding beyond vanilla slots."); CompressMidRoundSelectorSpacing = ((BaseUnityPlugin)this).Config.Bind<bool>("Layout", "CompressMidRoundSelectorSpacing", true, "Reduce between-round selector spacing for 5+ players so circles stay on-screen."); IncreaseCameraZoomForCrowdedMatches = ((BaseUnityPlugin)this).Config.Bind<bool>("Match", "IncreaseCameraZoomForCrowdedMatches", true, "Increase max camera zoom and breathing room when player count exceeds 4."); RelaxColorUniquenessWhenFull = ((BaseUnityPlugin)this).Config.Bind<bool>("CharacterSelect", "RelaxColorUniquenessWhenFull", true, "Allow duplicate colors once all unique colors are consumed so 5+ players can still ready up."); ExpandDrawWinnerUiSlots = ((BaseUnityPlugin)this).Config.Bind<bool>("Match", "ExpandDrawWinnerUiSlots", true, "Clone draw-winner UI slots when needed to avoid out-of-range errors with more than 4 players."); } private void ApplyEarlyRuntimeOverrides() { if (!DisableReplayRecording.Value) { return; } try { FieldInfo fieldInfo = AccessTools.Field(typeof(Host), "recordReplay"); if (fieldInfo != null && fieldInfo.IsStatic && fieldInfo.FieldType == typeof(bool)) { fieldInfo.SetValue(null, false); Log.LogInfo((object)"Disabled Host.recordReplay for local 8-player stability."); } } catch (Exception ex) { Log.LogWarning((object)("Failed to disable replay recording: " + ex.Message)); } } } internal static class RuntimeSnapshot { internal static void Log(string message, bool verbose) { if (Plugin.EnableDiagnostics.Value && (!verbose || Plugin.VerboseDiagnostics.Value)) { Plugin.Log.LogInfo((object)("[Diag] " + message)); } } internal static void LogGlobalState(string context, bool verbose) { if (Plugin.EnableDiagnostics.Value) { PlayerHandler obj = PlayerHandler.Get(); List<Player> list = ((obj != null) ? obj.PlayerList() : null); int num = list?.Count ?? (-1); int num2 = list?.Count((Player p) => p.IsLocalPlayer) ?? (-1); int num3 = list?.Count((Player p) => p.IsAlive) ?? (-1); bool[] value = Traverse.Create(typeof(CharacterSelectBox)).Field<bool[]>("occupiedRectangles").Value; int[] value2 = Traverse.Create(typeof(CharacterSelectBox)).Field<int[]>("deviceIds").Value; CharacterSelectHandler val = Object.FindObjectOfType<CharacterSelectHandler>(); CharacterSelectBox[] array = (((Object)(object)val == (Object)null) ? null : Traverse.Create((object)val).Field<CharacterSelectBox[]>("characterSelectBoxes").Value); GameSessionHandler val2 = Object.FindObjectOfType<GameSessionHandler>(); SlimeController[] array2 = (((Object)(object)val2 == (Object)null) ? null : Traverse.Create((object)val2).Field<SlimeController[]>("slimeControllers").Value); Log($"{context}: players={num}, localPlayers={num2}, alivePlayers={num3}, " + "occupiedSlots=" + FormatBoolArray(value) + ", deviceSlots=" + FormatIntArray(value2) + ", " + $"charBoxes={((array != null) ? array.Length : (-1))}, slimeControllers={((array2 != null) ? array2.Length : (-1))}, " + $"targetPlayers={Plugin.TargetPlayerCount}, slotArrayLength={Plugin.SlotArrayLength}", verbose); } } private static string FormatBoolArray(bool[]? values) { if (values == null) { return "null"; } return string.Format("len={0} [{1}]", values.Length, string.Join(",", values.Select((bool v) => v ? 1 : 0))); } private static string FormatIntArray(int[]? values) { if (values == null) { return "null"; } return string.Format("len={0} [{1}]", values.Length, string.Join(",", values)); } } } namespace BoplMorePlayersLocal8.Patches { [HarmonyPatch(typeof(HandleAbilitySelectUI), "Init")] internal static class HandleAbilitySelectUiSpacingPatch { [HarmonyPrefix] private static void Prefix(HandleAbilitySelectUI __instance, ref float __state) { Traverse<float> val = Traverse.Create((object)__instance).Field<float>("Separation"); __state = val.Value; if (Plugin.CompressMidRoundSelectorSpacing.Value) { PlayerHandler obj = PlayerHandler.Get(); int num = ((obj != null) ? obj.NumberOfPlayers() : 0); if (num > 4) { float num2 = 4f / (float)num; RuntimeSnapshot.Log(string.Format(arg2: val.Value = Mathf.Max(48f, __state * num2), format: "Compressed mid-round selector separation for {0} players: {1} -> {2}.", arg0: num, arg1: __state), verbose: false); } } } [HarmonyPostfix] private static void Postfix(HandleAbilitySelectUI __instance, float __state) { Traverse.Create((object)__instance).Field<float>("Separation").Value = __state; } } [HarmonyPatch(typeof(PlayerAverageCamera), "Start")] internal static class PlayerAverageCameraStartPatch { [HarmonyPostfix] private static void Postfix(PlayerAverageCamera __instance) { if (Plugin.IncreaseCameraZoomForCrowdedMatches.Value) { PlayerHandler obj = PlayerHandler.Get(); int num = ((obj != null) ? obj.NumberOfPlayers() : 0); if (num > 4) { Traverse val = Traverse.Create((object)__instance); float value = val.Field<float>("MAX_ZOOM").Value; float value2 = val.Field<float>("MIN_ZOOM").Value; float value3 = val.Field<float>("extraZoomRoom").Value; float num2 = Mathf.Lerp(1.2f, 1.6f, (float)(num - 4) / 4f); float num3 = value * num2; float num4 = value2 * 1.05f; float num5 = value3 + (float)(num - 4) * 0.25f; val.Field<float>("MAX_ZOOM").Value = num3; val.Field<float>("MIN_ZOOM").Value = num4; val.Field<float>("extraZoomRoom").Value = num5; RuntimeSnapshot.Log($"Expanded camera zoom for {num} players (MIN_ZOOM {value2}->{num4}, MAX_ZOOM {value}->{num3}, extraZoomRoom {value3}->{num5}).", verbose: false); } } } } [HarmonyPatch(typeof(CharacterStatsList), "OnEnable")] internal static class CharacterStatsListDrawSlotsPatch { [HarmonyPrefix] private static void Prefix(CharacterStatsList __instance) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) if (Plugin.ExpandDrawWinnerUiSlots.Value) { PlayerHandler obj = PlayerHandler.Get(); int num = ((obj != null) ? obj.NumberOfPlayers() : 0); if (num > 4) { EnsureImageArrayCapacity(__instance, "winnersOfDraw", num, Vector2.right, 75f); } } } private static void EnsureImageArrayCapacity(CharacterStatsList owner, string fieldName, int required, Vector2 direction, float spacing) { //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: 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_00f5: Unknown result type (might be due to invalid IL or missing references) Traverse<Image[]> val = Traverse.Create((object)owner).Field<Image[]>(fieldName); Image[] value = val.Value; if (value == null || value.Length == 0 || value.Length >= required) { return; } List<Image> list = value.ToList(); Image val2 = value[^1]; RectTransform component = ((Component)val2).GetComponent<RectTransform>(); for (int i = list.Count; i < required; i++) { GameObject val3 = Object.Instantiate<GameObject>(((Component)val2).gameObject, ((Component)val2).transform.parent); ((Object)val3).name = $"{((Object)((Component)val2).gameObject).name}_P{i + 1}"; Image component2 = val3.GetComponent<Image>(); if (!((Object)(object)component2 == (Object)null)) { RectTransform component3 = val3.GetComponent<RectTransform>(); if ((Object)(object)component3 != (Object)null && (Object)(object)component != (Object)null) { component3.anchoredPosition = component.anchoredPosition + direction * spacing * (float)(i - (value.Length - 1)); } val3.SetActive(false); list.Add(component2); } } val.Value = list.ToArray(); RuntimeSnapshot.Log($"Expanded {fieldName} from {value.Length} to {list.Count}.", verbose: false); } } [HarmonyPatch(typeof(GameSessionHandler), "SpawnPlayers")] internal static class GameSessionSpawnDiagnosticsPatch { [HarmonyPostfix] private static void Postfix(GameSessionHandler __instance) { PlayerHandler obj = PlayerHandler.Get(); List<Player> list = ((obj != null) ? obj.PlayerList() : null) ?? new List<Player>(); SlimeController[] value = Traverse.Create((object)__instance).Field<SlimeController[]>("slimeControllers").Value; object arg = list.Count; object arg2 = ((value != null) ? value.Length : (-1)); PlayerHandler obj2 = PlayerHandler.Get(); RuntimeSnapshot.Log($"SpawnPlayers complete: players={arg}, slimes={arg2}, teamsLeft={((obj2 != null) ? obj2.TeamsLeft() : (-1))}.", verbose: false); RuntimeSnapshot.LogGlobalState("GameSessionHandler.SpawnPlayers", verbose: true); } } [HarmonyPatch(typeof(CharacterSelectHandler), "Awake")] internal static class CharacterSelectHandlerAwakePatch { [HarmonyPostfix] private static void Postfix(CharacterSelectHandler __instance) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) CharacterSelectExpander.Expand(__instance); ReadOnlyArray<Gamepad> all = Gamepad.all; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append($"Connected gamepads: {all.Count}"); for (int i = 0; i < all.Count; i++) { stringBuilder.Append($"\n [{i}] id={((InputDevice)all[i]).deviceId} name='{((InputControl)all[i]).displayName}'"); } RuntimeSnapshot.Log(stringBuilder.ToString(), verbose: false); CharSelectClickToJoin[] array = Object.FindObjectsOfType<CharSelectClickToJoin>(true); RuntimeSnapshot.Log($"CharSelectClickToJoin instances: {array.Length} " + "[" + string.Join(", ", array.Select((CharSelectClickToJoin c) => $"box{c.csb?.RectangleIndex}(active={((Component)c).gameObject.activeInHierarchy})")) + "]", verbose: false); RuntimeSnapshot.LogGlobalState("CharacterSelectHandler.Awake.Postfix", verbose: false); } } [HarmonyPatch(typeof(CharSelectClickToJoin), "CurrentlyActiveIndex")] internal static class CharSelectClickToJoinCurrentIndexPatch { [HarmonyPrefix] private static bool Prefix(ref int __result) { bool[] value = Traverse.Create(typeof(CharacterSelectBox)).Field<bool[]>("occupiedRectangles").Value; if (value == null || value.Length == 0) { return true; } int num = Mathf.Min(Plugin.TargetPlayerCount, Mathf.Max(0, value.Length - 1)); for (int i = 0; i < num; i++) { if (!value[i]) { __result = i; return false; } } __result = value.Length - 1; return false; } } [HarmonyPatch(typeof(CharSelectClickToJoin), "LateUpdate")] internal static class CharSelectClickToJoinExpandedSlotsPatch { [HarmonyPostfix] private static void Postfix(CharSelectClickToJoin __instance) { //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Invalid comparison between Unknown and I4 //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01cf: Unknown result type (might be due to invalid IL or missing references) if (__instance.csb.RectangleIndex != 0) { return; } bool[] occupiedRectangles = CharacterSelectBox.occupiedRectangles; if (occupiedRectangles == null) { return; } int num = -1; int num2 = Mathf.Min(Plugin.TargetPlayerCount, Mathf.Max(0, occupiedRectangles.Length - 1)); for (int i = 0; i < num2; i++) { if (!occupiedRectangles[i]) { num = i; break; } } if (num < 4) { return; } CharacterSelectHandler value = Traverse.Create(typeof(CharacterSelectHandler)).Field<CharacterSelectHandler>("selfRef").Value; if ((Object)(object)value == (Object)null) { return; } CharacterSelectBox[] value2 = Traverse.Create((object)value).Field<CharacterSelectBox[]>("characterSelectBoxes").Value; if (value2 == null || num >= value2.Length) { return; } CharacterSelectBox val = value2[num]; if ((Object)(object)val == (Object)null) { return; } CharSelectMenu value3 = Traverse.Create((object)val).Field<CharSelectMenu>("menuState").Value; if ((int)value3 > 0) { return; } if (!CharacterSelectBox.keyboardMouseIsOccupied) { Keyboard current = Keyboard.current; if (current != null && ((ButtonControl)current.anyKey).wasPressedThisFrame && !((ButtonControl)current.escapeKey).wasPressedThisFrame) { CharacterSelectBox.deviceIds[num] = 0; val.UseskeyboardMouse = true; CharacterSelectBox.keyboardMouseIsOccupied = true; val.OnEnterSelect(); AudioManager.Get().Play("accept"); RuntimeSnapshot.Log($"Expanded slot join: keyboard → slot {num}.", verbose: false); return; } } int[] deviceIds = CharacterSelectBox.deviceIds; Enumerator<Gamepad> enumerator = Gamepad.all.GetEnumerator(); try { while (enumerator.MoveNext()) { Gamepad current2 = enumerator.Current; bool flag = false; for (int j = 0; j < deviceIds.Length; j++) { if (deviceIds[j] == ((InputDevice)current2).deviceId) { flag = true; break; } } if (flag || (!current2.startButton.wasPressedThisFrame && !current2.aButton.wasPressedThisFrame)) { continue; } CharacterSelectBox.deviceIds[num] = ((InputDevice)current2).deviceId; val.UseskeyboardMouse = false; val.currentGamePad = current2; val.OnEnterSelect(); AudioManager.Get().Play("accept"); RuntimeSnapshot.Log($"Expanded slot join: gamepad {((InputDevice)current2).deviceId} → slot {num}.", verbose: false); break; } } finally { ((IDisposable)enumerator).Dispose(); } } } [HarmonyPatch(typeof(CharacterSelectHandler), "IsColorTaken")] internal static class CharacterSelectColorLimitPatch { [HarmonyPrefix] private static bool Prefix(int __0, CharacterSelectBox __1, ref bool __result) { //IL_00d4: Unknown result type (might be due to invalid IL or missing references) CharacterSelectBox __2 = __1; if (!Plugin.RelaxColorUniquenessWhenFull.Value || GameLobby.isOnlineGame) { return true; } try { CharacterSelectHandler value = Traverse.Create(typeof(CharacterSelectHandler)).Field<CharacterSelectHandler>("selfRef").Value; if ((Object)(object)value == (Object)null) { return true; } CharacterSelectBox[] value2 = Traverse.Create((object)value).Field<CharacterSelectBox[]>("characterSelectBoxes").Value; PlayerColors value3 = Traverse.Create((object)value).Field<PlayerColors>("playerColors").Value; int num = ((value3 != null) ? value3.Length : 0); if (value2 == null || value2.Length == 0 || num <= 0) { return true; } CharSelectMenu readyState = (CharSelectMenu)2; int num2 = value2.Count((CharacterSelectBox box) => (Object)(object)box != (Object)null && (Object)(object)box != (Object)(object)__2 && Traverse.Create((object)box).Field<CharSelectMenu>("menuState").Value == readyState); if (num2 < num) { return true; } __result = false; RuntimeSnapshot.Log($"Color uniqueness relaxed (readyPlayers={num2}, availableColors={num}, requestedColor={__0}).", verbose: false); return false; } catch (Exception ex) { RuntimeSnapshot.Log("Color uniqueness relaxation failed: " + ex.Message, verbose: false); return true; } } } [HarmonyPatch(typeof(CharacterSelectBox), "OnEnterSelect")] internal static class CharacterSelectJoinDiagnosticsPatch { [HarmonyPostfix] private static void Postfix(CharacterSelectBox __instance) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) RuntimeSnapshot.Log(string.Format("Player joined slot={0}, usesKeyboard={1}, menuState={2}.", __instance.RectangleIndex, __instance.UseskeyboardMouse, Traverse.Create((object)__instance).Field<CharSelectMenu>("menuState").Value), verbose: false); RuntimeSnapshot.LogGlobalState("CharacterSelectBox.OnEnterSelect", verbose: true); } } internal static class CharacterSelectExpander { internal static void Expand(CharacterSelectHandler handler) { CharacterSelectBox[] array = Traverse.Create((object)handler).Field<CharacterSelectBox[]>("characterSelectBoxes").Value; if (array == null || array.Length == 0) { RuntimeSnapshot.Log("CharacterSelectExpander: characterSelectBoxes missing.", verbose: false); return; } EnsureSlotArrays(); int targetPlayerCount = Plugin.TargetPlayerCount; int num = array.Length; if (array.Length < targetPlayerCount) { array = ExpandBoxes(handler, array, targetPlayerCount); Traverse.Create((object)handler).Field<CharacterSelectBox[]>("characterSelectBoxes").Value = array; RuntimeSnapshot.Log($"Expanded characterSelectBoxes from {num} to {array.Length}.", verbose: false); } EnsureAnimateOutDelays(handler, array.Length); if (Plugin.RepositionCharacterSelectBoxes.Value) { RepositionBoxes(handler, array, num); } } private static void EnsureSlotArrays() { int slotArrayLength = Plugin.SlotArrayLength; Traverse<bool[]> val = Traverse.Create(typeof(CharacterSelectBox)).Field<bool[]>("occupiedRectangles"); bool[] array = val.Value ?? Array.Empty<bool>(); if (array.Length < slotArrayLength) { bool[] array2 = new bool[slotArrayLength]; Array.Copy(array, array2, array.Length); val.Value = array2; RuntimeSnapshot.Log($"Expanded occupiedRectangles from {array.Length} to {slotArrayLength}.", verbose: false); } Traverse<int[]> val2 = Traverse.Create(typeof(CharacterSelectBox)).Field<int[]>("deviceIds"); int[] array3 = val2.Value ?? Array.Empty<int>(); if (array3.Length < slotArrayLength) { int[] array4 = new int[slotArrayLength]; Array.Copy(array3, array4, array3.Length); val2.Value = array4; RuntimeSnapshot.Log($"Expanded deviceIds from {array3.Length} to {slotArrayLength}.", verbose: false); } } private static CharacterSelectBox[] ExpandBoxes(CharacterSelectHandler handler, CharacterSelectBox[] existing, int targetCount) { List<CharacterSelectBox> list = existing.ToList(); CharacterSelectBox val = existing[^1]; Transform parent = ((Component)val).transform.parent; for (int i = existing.Length; i < targetCount; i++) { GameObject val2 = Object.Instantiate<GameObject>(((Component)val).gameObject, parent); ((Object)val2).name = $"{((Object)((Component)val).gameObject).name}_P{i + 1}"; CharacterSelectBox component = val2.GetComponent<CharacterSelectBox>(); if ((Object)(object)component == (Object)null) { RuntimeSnapshot.Log($"CharacterSelectExpander: clone missing CharacterSelectBox for slot {i}.", verbose: false); continue; } component.RectangleIndex = i; component.UseskeyboardMouse = false; component.currentGamePad = null; Traverse.Create((object)component).Field<int>("selectedIndex").Value = 0; Traverse.Create((object)component).Field<CharSelectMenu>("menuState").Value = (CharSelectMenu)0; RebindNestedCharacterSelectReferences(component); CharSelectClickToJoin componentInChildren = val2.GetComponentInChildren<CharSelectClickToJoin>(true); if ((Object)(object)componentInChildren != (Object)null) { Object.Destroy((Object)(object)componentInChildren); RuntimeSnapshot.Log($"Destroyed duplicate CharSelectClickToJoin on clone slot {i}.", verbose: false); } component.OnEnterJoin(); list.Add(component); RuntimeSnapshot.Log($"Created character select slot {i + 1} from template '{((Object)val).name}'.", verbose: false); } return list.ToArray(); } private static void RebindNestedCharacterSelectReferences(CharacterSelectBox owner) { Type typeFromHandle = typeof(CharacterSelectBox); Component[] componentsInChildren = ((Component)owner).GetComponentsInChildren<Component>(true); foreach (Component val in componentsInChildren) { if (!((Object)(object)val == (Object)null)) { FieldInfo field = ((object)val).GetType().GetField("csb", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(field == null) && !(field.FieldType != typeFromHandle)) { field.SetValue(val, owner); } } } } private static void EnsureAnimateOutDelays(CharacterSelectHandler handler, int targetLength) { Traverse<float[]> val = Traverse.Create((object)handler).Field<float[]>("animateOutDelays"); float[] array = val.Value ?? Array.Empty<float>(); if (array.Length >= targetLength) { return; } float[] array2 = new float[targetLength]; Array.Copy(array, array2, array.Length); float num = 0.05f; if (array.Length >= 2) { num = Mathf.Abs(array[^1] - array[^2]); if (num < 0.005f) { num = 0.05f; } } float num2 = ((array.Length != 0) ? array[^1] : 0f); for (int i = array.Length; i < array2.Length; i++) { array2[i] = num2 + (float)(i - array.Length + 1) * num; } val.Value = array2; RuntimeSnapshot.Log($"Expanded animateOutDelays from {array.Length} to {array2.Length}.", verbose: false); } private static void RepositionBoxes(CharacterSelectHandler handler, CharacterSelectBox[] boxes, int originalCount) { //IL_020c: Unknown result type (might be due to invalid IL or missing references) //IL_0219: Unknown result type (might be due to invalid IL or missing references) if (boxes.Length <= originalCount || originalCount <= 0) { return; } RectTransform[] array = (from box in boxes.Take(originalCount) select ((Component)box).GetComponent<RectTransform>() into rect where (Object)(object)rect != (Object)null select rect).Cast<RectTransform>().ToArray(); if (array.Length == 0) { return; } int num = array.Length; int num2 = Mathf.CeilToInt((float)boxes.Length / (float)num); float num3 = array.Min((RectTransform rect) => rect.anchoredPosition.x); float num4 = array.Max((RectTransform rect) => rect.anchoredPosition.x); float num5 = (num3 + num4) * 0.5f; float num6 = array.Average((RectTransform rect) => rect.anchoredPosition.y); float num7 = boxes.Take(originalCount).Select(MeasureJoinCardWidth).DefaultIfEmpty(260f) .Max(); float num8 = Mathf.Max(num4 - num3, num7 * 3.2f); float num9 = num7 * 0.15f; float num10 = num8 + num9 * 2f; int num11 = boxes.Length; float num12 = ((num11 <= 1) ? 0f : (num10 / (float)(num11 - 1))); float num13 = num12 * 0.93f; float num14 = Mathf.Clamp(num13 / num7, 0.4f, 1f); float num15 = num5 - num12 * (float)(num11 - 1) * 0.5f; float num16 = num6; List<string> list = new List<string>(num11); for (int i = 0; i < num11; i++) { RectTransform component = ((Component)boxes[i]).GetComponent<RectTransform>(); if (!((Object)(object)component == (Object)null)) { float num17 = num15 + (float)i * num12; component.anchoredPosition = new Vector2(num17, num16); ((Transform)component).localScale = Vector3.one; ScaleVisibleChildren(boxes[i], num14); list.Add($"slot={i + 1} x={num17:0.0} y={num16:0.0} scale={num14:0.000}"); } } RuntimeSnapshot.Log($"Repositioned character select to single row: slots={num11}, baseColumns={num}, rowsBeforeCompression={num2}, centerX={num5:0.0}, centerY={num6:0.0}, rowY={num16:0.0}, availableWidth={num10:0.0}, spacing={num12:0.0}, templateVisualWidth={num7:0.0}, desiredVisualWidth={num13:0.0}, scale={num14:0.000}.", verbose: false); RuntimeSnapshot.Log("Row placement details: " + string.Join(" | ", list), verbose: false); } private static float MeasureJoinCardWidth(CharacterSelectBox box) { //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) RectTransform value = Traverse.Create((object)box).Field<RectTransform>("joinBorder").Value; Rect rect; if ((Object)(object)value != (Object)null) { Transform parent = ((Transform)value).parent; if ((Object)(object)parent == (Object)null) { rect = value.rect; return Mathf.Max(220f, ((Rect)(ref rect)).width); } Bounds val = RectTransformUtility.CalculateRelativeRectTransformBounds(parent, (Transform)(object)value); rect = value.rect; return Mathf.Max(((Rect)(ref rect)).width, ((Bounds)(ref val)).size.x); } RectTransform component = ((Component)box).GetComponent<RectTransform>(); if ((Object)(object)component == (Object)null) { return 260f; } rect = component.rect; return Mathf.Max(220f, ((Rect)(ref rect)).width * 2f); } private static void ScaleVisibleChildren(CharacterSelectBox box, float scale) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) if (!(scale <= 0f) && !Mathf.Approximately(scale, 1f)) { Vector3 localScale = Vector3.one * scale; List<string> list = new List<string>(); for (int i = 0; i < ((Component)box).transform.childCount; i++) { Transform child = ((Component)box).transform.GetChild(i); child.localScale = localScale; list.Add(((Object)child).name); } RuntimeSnapshot.Log(string.Format("Box '{0}' scaled all {1} children [{2}] to {3:0.000}.", ((Object)box).name, list.Count, string.Join(",", list), scale), verbose: false); } } } [HarmonyPatch(typeof(GameSessionHandler), "LoadAbilitySelectScene")] internal static class LoadAbilitySelectDiagnosticsPatch { [HarmonyPostfix] private static void Postfix() { RuntimeSnapshot.Log("GameSessionHandler.LoadAbilitySelectScene invoked.", verbose: false); RuntimeSnapshot.LogGlobalState("LoadAbilitySelectScene", verbose: true); } } [HarmonyPatch(typeof(GameSessionHandler), "LoadNextLevelScene")] internal static class LoadNextLevelDiagnosticsPatch { [HarmonyPostfix] private static void Postfix() { RuntimeSnapshot.Log("GameSessionHandler.LoadNextLevelScene invoked.", verbose: false); RuntimeSnapshot.LogGlobalState("LoadNextLevelScene", verbose: true); } } [HarmonyPatch(typeof(CharacterSelectHandler), "TryStartGame_inner")] internal static class CharacterSelectStartDiagnosticsPatch { [HarmonyPrefix] private static void Prefix(CharacterSelectHandler __instance) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) CharacterSelectBox[] value = Traverse.Create((object)__instance).Field<CharacterSelectBox[]>("characterSelectBoxes").Value; CharSelectMenu readyState = (CharSelectMenu)2; int num = value?.Count((CharacterSelectBox box) => (Object)(object)box != (Object)null && Traverse.Create((object)box).Field<CharSelectMenu>("menuState").Value == readyState) ?? (-1); RuntimeSnapshot.Log(string.Format("TryStartGame_inner: boxes={0}, ready={1}, startButton={2}.", (value != null) ? value.Length : (-1), num, Traverse.Create(typeof(CharacterSelectHandler)).Field<bool>("startButtonAvailable").Value), verbose: false); } [HarmonyPostfix] private static void Postfix() { RuntimeSnapshot.LogGlobalState("TryStartGame_inner.Postfix", verbose: true); } } [HarmonyPatch(typeof(CharSelectClickToJoin), "LateUpdate")] internal static class CharSelectClickToJoinDiagnosticsPatch { private static float _lastLogTime; [HarmonyPostfix] private static void Postfix(CharSelectClickToJoin __instance) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) if (Time.time - _lastLogTime < 5f) { return; } _lastLogTime = Time.time; CharacterSelectBox csb = __instance.csb; CharSelectMenu value = Traverse.Create((object)csb).Field<CharSelectMenu>("menuState").Value; int rectangleIndex = csb.RectangleIndex; int num = -1; bool[] value2 = Traverse.Create(typeof(CharacterSelectBox)).Field<bool[]>("occupiedRectangles").Value; if (value2 != null) { int num2 = Mathf.Min(Plugin.TargetPlayerCount, Mathf.Max(0, value2.Length - 1)); for (int i = 0; i < num2; i++) { if (!value2[i]) { num = i; break; } } if (num == -1) { num = value2.Length - 1; } } bool flag = num == rectangleIndex; int count = Gamepad.all.Count; int num3 = 0; int[] value3 = Traverse.Create(typeof(CharacterSelectBox)).Field<int[]>("deviceIds").Value; if (value3 != null) { int[] array = value3; for (int j = 0; j < array.Length; j++) { if (array[j] != 0) { num3++; } } } RuntimeSnapshot.Log($"[ClickToJoin] box={rectangleIndex} menuState={value} activeIndex={num} responds={flag} " + string.Format("gamepads={0} assigned={1} occupied=[{2}]", count, num3, string.Join(",", value2?.Select((bool o) => o ? "1" : "0") ?? Array.Empty<string>())), verbose: false); } } }