using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Reflection.Emit; 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 Mirror; using Steamworks; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("CrewSizeMod")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.10.8.0")] [assembly: AssemblyInformationalVersion("0.10.8")] [assembly: AssemblyProduct("CrewSizeMod")] [assembly: AssemblyTitle("CrewSizeMod")] [assembly: AssemblyVersion("0.10.8.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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace CrewSizeMod { [BepInPlugin("com.hunter.crewsizemod", "Crew Size Mod", "0.10.9")] public class Plugin : BaseUnityPlugin { public const string Guid = "com.hunter.crewsizemod"; public const string Name = "Crew Size Mod"; public const string Version = "0.10.9"; internal static ManualLogSource Log; internal static ConfigEntry MaxPlayers; internal static ConfigEntry TestSlotLimitOverride; internal static ConfigEntry EnableModPresenceCheck; internal static ConfigEntry ExperimentalExpandPlayerUI; internal static ConfigEntry ExperimentalExpandPreviewCount; internal static ConfigEntry PlaceOverflowInLobbyRow; internal static ConfigEntry ExperimentalCloneLobbyParkingSpots; internal static ConfigEntry ExperimentalLobbyPreviewCount; internal static ConfigEntry LobbyManualSpots; internal static ConfigEntry ShowTwitchCredit; internal static ConfigEntry TwitchCreditCorner; internal static ConfigEntry CreditWidth; private void Awake() { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Expected O, but got Unknown //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Expected O, but got Unknown //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Expected O, but got Unknown //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_026d: Unknown result type (might be due to invalid IL or missing references) //IL_0277: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; MaxPlayers = ((BaseUnityPlugin)this).Config.Bind("General", "MaxPlayers", 8, new ConfigDescription("Total players allowed in a session. EVERY player must run this mod with the SAME value; the HOST's value is the one that actually sizes the session. The game's authored ceiling is ~6-8; higher values are accepted but get rougher (netcode/physics strain, UI authored for 4, and the lobby parking spots only extrapolate cleanly to ~8).", (AcceptableValueBase)(object)new AcceptableValueRange(2, 12), Array.Empty())); TestSlotLimitOverride = ((BaseUnityPlugin)this).Config.Bind("Testing", "TestSlotLimitOverride", 0, new ConfigDescription("TEST ONLY. If > 0, pretend the lobby has only this many slots, so overflow skipping triggers earlier. Set to 1 to reproduce the overflow path with just 2 machines (player 2 becomes the 'overflow' player). Set to 0 for normal play (uses the real slot count).", (AcceptableValueBase)(object)new AcceptableValueRange(0, 16), Array.Empty())); EnableModPresenceCheck = ((BaseUnityPlugin)this).Config.Bind("General", "EnableModPresenceCheck", true, "Advertise this machine as modded via Steam lobby member data, and log which lobby members are running vanilla (their voice/UI may glitch at 5+ players, since those fixes run client-side). Purely informational - no gameplay effect. Also the seam for any future enhancements that require everyone to have the mod."); ExperimentalExpandPlayerUI = ((BaseUnityPlugin)this).Config.Bind("Experimental", "ExpandPlayerUI", false, "EXPERIMENTAL (default OFF). When ON, the ready-up proceed icons are EXPANDED to show ALL players by cloning the game's 4 authored icon slots at runtime, instead of capping the display at 4. Purely local/cosmetic per machine (no networking). Leave OFF for the stable behaviour; turn ON only to test the 'show all 8' layout. If the cloned icons look misaligned, that's the authored layout not auto-arranging - report how it looks and it can be positioned manually."); ExperimentalExpandPreviewCount = ((BaseUnityPlugin)this).Config.Bind("Experimental", "ExpandPreviewCount", 0, new ConfigDescription("TEST AID for ExpandPlayerUI. If > 0 (and ExpandPlayerUI is ON), the proceed-icon row is padded to this many icons by duplicating a player, so you can see the N-icon layout SOLO without needing N real players. e.g. set to 8 to preview the 8-player row with just 1-2 machines. 0 = off (only real players show).", (AcceptableValueBase)(object)new AcceptableValueRange(0, 16), Array.Empty())); LobbyManualSpots = ((BaseUnityPlugin)this).Config.Bind("General", "LobbyManualSpots", "", "Manually position EVERY lobby parking spot (the real ones AND the overflow ones), as semicolon-separated 'x,z' world coordinates, one per player index starting at 0. The host log prints a ready-to-paste line ('Lobby spot positions ...') with the current layout - copy it here, tweak the numbers, relaunch, repeat. An empty entry (e.g. two semicolons in a row) leaves that spot at its default. Leave the whole value blank to use the game's real spots + auto-placed overflow. Example: '-10.8,14.9; -10.8,18; -17,12.6; -17,15.9; -4,17; -4,20; 2,19; 2,22'"); PlaceOverflowInLobbyRow = ((BaseUnityPlugin)this).Config.Bind("General", "PlaceOverflowInLobbyRow", true, "When ON (default), overflow players' lobby bodies are positioned at extrapolated parking spots in line with the 4 real ones, instead of spawning at the prefab's default position. Cosmetic only (these bodies are destroyed before the run). Turn OFF if the extrapolated placement looks wrong in your lobby."); ExperimentalCloneLobbyParkingSpots = ((BaseUnityPlugin)this).Config.Bind("Experimental", "CloneLobbyParkingSpots", false, "EXPERIMENTAL / PATH B (default OFF). When ON, overflow players get a CLONED real LobbyPlayer parking spot (with the assigned animation) instead of a forklift stand-in. Requires EVERY player to have the mod (it registers a Mirror spawn handler so the cloned spot replicates to clients). This is the fragile path - if a clone fails it falls back to the forklift body automatically. Leave OFF unless you're testing it."); ExperimentalLobbyPreviewCount = ((BaseUnityPlugin)this).Config.Bind("Experimental", "LobbyPreviewCount", 0, new ConfigDescription("SOLO TEST AID. If > 0, spawn this many FAKE forklifts at the non-host parking spots (index 1, 2, 3, ...) the moment the lobby loads - so you can lay out the whole lobby ALONE. Set to 7 to see a full 8-player lobby (you at spot 0 + 7 fakes). Each fake sits at its spot's LobbyManualSpots override or the default. Uses forklifts, or cloned spots if CloneLobbyParkingSpots is ON. Fakes are unowned and destroyed at run start. 0 = off.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 16), Array.Empty())); Harmony val = new Harmony("com.hunter.crewsizemod"); val.PatchAll(); Log.LogInfo((object)string.Format("{0} {1} loaded. Target max players = {2}", "Crew Size Mod", "0.10.9", MaxPlayers.Value)); if (TestSlotLimitOverride.Value > 0) { Log.LogWarning((object)($"TestSlotLimitOverride = {TestSlotLimitOverride.Value} - TEST MODE is ON " + "(forces the overflow path early). Set it to 0 in the config for normal play.")); } if (EnableModPresenceCheck.Value) { ModPresence.Init(); } ShowTwitchCredit = ((BaseUnityPlugin)this).Config.Bind("Credit", "ShowTwitchCredit", true, "Show a small 'MoreCrew by Izaya_here / Follow on Twitch' credit in the corner during the lobby and break room (hidden during the actual shift). Default ON."); TwitchCreditCorner = ((BaseUnityPlugin)this).Config.Bind("Credit", "Corner", ScreenCorner.TopLeft, "Which screen corner the credit sits in. Move it if it overlaps the game's UI."); CreditWidth = ((BaseUnityPlugin)this).Config.Bind("Credit", "ImageWidth", 320f, new ConfigDescription("On-screen width (pixels) of the credit image (credit.png, shipped next to the mod DLL). Height auto-scales to the image's aspect ratio. Lower it to make the logo more subtle.", (AcceptableValueBase)(object)new AcceptableValueRange(120f, 900f), Array.Empty())); Log.LogInfo((object)($"Twitch credit enabled (ShowTwitchCredit={ShowTwitchCredit.Value}, " + $"corner={TwitchCreditCorner.Value}); rendered via uGUI canvas.")); foreach (MethodBase patchedMethod in val.GetPatchedMethods()) { Log.LogInfo((object)(" [patched] " + patchedMethod.DeclaringType?.Name + "." + patchedMethod.Name)); } Type type = AccessTools.TypeByName("LobbyManager"); Log.LogInfo((object)(" [resolve] LobbyManager type = " + ((type != null) ? "FOUND" : "NULL") + ", ServerAddPlayer = " + ((AccessTools.Method(type, "ServerAddPlayer", (Type[])null, (Type[])null) != null) ? "FOUND" : "NULL"))); } } [HarmonyPatch(typeof(NetworkServer), "Listen")] internal static class Patch_NetworkServerListen { private static void Prefix(ref int maxConns) { int value = Plugin.MaxPlayers.Value; Plugin.Log.LogInfo((object)$"NetworkServer.Listen maxConns {maxConns} -> {value}"); maxConns = value; } } [HarmonyPatch(typeof(NetworkManager), "Awake")] internal static class Patch_NetworkManagerMaxConnections { private static void Postfix(NetworkManager __instance) { __instance.maxConnections = Plugin.MaxPlayers.Value; Plugin.Log.LogInfo((object)$"NetworkManager.maxConnections -> {__instance.maxConnections}"); } } [HarmonyPatch(typeof(SteamMatchmaking), "CreateLobby")] internal static class Patch_CreateLobby { private static void Prefix(ref int cMaxMembers) { Plugin.Log.LogInfo((object)$"SteamMatchmaking.CreateLobby cMaxMembers {cMaxMembers} -> {Plugin.MaxPlayers.Value}"); cMaxMembers = Plugin.MaxPlayers.Value; } } [HarmonyPatch(typeof(SteamMatchmaking), "SetLobbyMemberLimit")] internal static class Patch_SetLobbyMemberLimit { private static void Prefix(ref int cMaxMembers) { Plugin.Log.LogInfo((object)$"SteamMatchmaking.SetLobbyMemberLimit cMaxMembers {cMaxMembers} -> {Plugin.MaxPlayers.Value}"); cMaxMembers = Plugin.MaxPlayers.Value; } } internal static class Recon { public static void DumpType(string typeName) { Type type = AccessTools.TypeByName(typeName); if (type == null) { Plugin.Log.LogWarning((object)("Type not found: " + typeName)); return; } Plugin.Log.LogInfo((object)("--- " + type.FullName + " ---")); FieldInfo[] fields = type.GetFields(AccessTools.all); foreach (FieldInfo fieldInfo in fields) { Plugin.Log.LogInfo((object)(" field " + fieldInfo.FieldType.Name + " " + fieldInfo.Name)); } MethodInfo[] methods = type.GetMethods(AccessTools.all); foreach (MethodInfo methodInfo in methods) { string[] value = Array.ConvertAll(methodInfo.GetParameters(), (ParameterInfo p) => p.ParameterType.Name); Plugin.Log.LogInfo((object)(" method " + methodInfo.Name + "(" + string.Join(", ", value) + ")")); } } } [HarmonyPatch] internal static class Patch_InviteButtonAlwaysOn { private static readonly Dictionary _btnFields = new Dictionary(); private static PropertyInfo _interactableProp; private static bool _warned; private static IEnumerable TargetMethods() { string[] array = new string[2] { "LobbyButtonsUI", "GameMenuUI" }; for (int i = 0; i < array.Length; i++) { Type type = AccessTools.TypeByName(array[i]); MethodInfo methodInfo = ((type != null) ? AccessTools.Method(type, "OnUpdatePresentation", (Type[])null, (Type[])null) : null); if (methodInfo != null) { yield return methodInfo; } } } private static void Postfix(object __instance) { try { Type type = __instance.GetType(); if (!_btnFields.TryGetValue(type, out var value)) { value = AccessTools.Field(type, "inviteFriendsButton"); _btnFields[type] = value; } object obj = value?.GetValue(__instance); if (obj != null) { if (_interactableProp == null) { _interactableProp = AccessTools.Property(obj.GetType(), "interactable"); } _interactableProp?.SetValue(obj, true); } } catch (Exception ex) { if (!_warned) { _warned = true; Plugin.Log.LogWarning((object)("invite-button postfix: " + ex.Message + " (logged once)")); } } } } internal static class OverflowTracker { public static readonly HashSet Conns = new HashSet(); public static readonly List Bodies = new List(); private static List _manual; private static bool _manualParsed; private static bool _loggedSpots; public static int EffectiveSlots(object lobbyManager) { int num = (AccessTools.Field(lobbyManager.GetType(), "_serverAvailablePlayers")?.GetValue(lobbyManager) as ICollection)?.Count ?? 0; if (Plugin.TestSlotLimitOverride.Value <= 0) { return num; } return Math.Min(num, Plugin.TestSlotLimitOverride.Value); } private static List Manual() { //IL_0096: Unknown result type (might be due to invalid IL or missing references) if (_manualParsed) { return _manual; } _manualParsed = true; _manual = new List(); string text = Plugin.LobbyManualSpots?.Value ?? ""; if (string.IsNullOrWhiteSpace(text)) { return _manual; } string[] array = text.Split(';'); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Split(','); if (array2.Length >= 2 && float.TryParse(array2[0].Trim(), out var result) && float.TryParse(array2[1].Trim(), out var result2)) { _manual.Add((Vector3?)new Vector3(result, 0f, result2)); } else { _manual.Add(null); } } return _manual; } public static Vector3? ManualSpot(int index) { List list = Manual(); if (list == null || index < 0 || index >= list.Count) { return null; } return list[index]; } public static Vector3 AutoSpot(List positions, int index) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //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) int count = positions.Count; if (index >= 0 && index < count) { return positions[index]; } int num = index - count; int num2 = num % count; int num3 = 1 + num / count; int num4 = -1; for (int i = 0; i < count; i++) { if (i != num2 && Mathf.Abs(positions[i].x - positions[num2].x) < 3f) { num4 = i; break; } } float num5 = ((num4 < 0) ? ((num2 % 2 == 0) ? (-1f) : 1f) : ((positions[num2].z < positions[num4].z) ? (-1f) : 1f)); return positions[num2] + new Vector3(0f, 0f, num5 * 2f * (float)num3); } private static List ReadSpotPositions(object lobbyManager, out IList list) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) list = null; list = AccessTools.Field(lobbyManager.GetType(), "_serverAvailablePlayers")?.GetValue(lobbyManager) as IList; if (list == null || list.Count == 0) { return null; } List list2 = new List(); foreach (object item in list) { Component val = (Component)((item is Component) ? item : null); if (val != null) { list2.Add(val.transform.position); } } return list2; } private static void LogSpotsOnce(List positions) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) if (_loggedSpots) { return; } _loggedSpots = true; int num = Math.Max(Plugin.MaxPlayers.Value, positions.Count); StringBuilder stringBuilder = new StringBuilder("Lobby spot positions (copy into [General] LobbyManualSpots to customize): "); for (int i = 0; i < num; i++) { Vector3 val = (Vector3)(((??)ManualSpot(i)) ?? AutoSpot(positions, i)); stringBuilder.Append($"{val.x:0.0},{val.z:0.0}"); if (i < num - 1) { stringBuilder.Append("; "); } } Plugin.Log.LogInfo((object)stringBuilder.ToString()); } public static void ApplyManualToRealSpots(object lobbyManager) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) try { if (ReadSpotPositions(lobbyManager, out var list) == null) { return; } for (int i = 0; i < list.Count; i++) { Vector3? val = ManualSpot(i); if (val.HasValue) { object? obj = list[i]; Component val2 = (Component)((obj is Component) ? obj : null); if (val2 != null) { val2.transform.position = val.Value; } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("apply manual spots: " + ex.Message)); } } public static int RealSpotCount(object lobbyManager) { return (AccessTools.Field(lobbyManager.GetType(), "_serverAvailablePlayers")?.GetValue(lobbyManager) as ICollection)?.Count ?? 0; } public static bool TryGetLobbyRowSpot(object lobbyManager, int playerIndex, out Vector3 pos, out Quaternion rot) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008c: 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_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) pos = Vector3.zero; rot = Quaternion.identity; try { if (!(AccessTools.Field(lobbyManager.GetType(), "_serverAvailablePlayers")?.GetValue(lobbyManager) is IList list) || list.Count == 0) { return false; } List list2 = new List(); foreach (object item in list) { Component val = (Component)((item is Component) ? item : null); if (val != null) { list2.Add(val.transform.position); rot = val.transform.rotation; } } if (list2.Count == 0) { return false; } LogSpotsOnce(list2); pos = (Vector3)(((??)ManualSpot(playerIndex)) ?? AutoSpot(list2, playerIndex)); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("lobby-row spot: " + ex.Message)); return false; } } } [HarmonyPatch] internal static class Patch_LobbyAddOverflow { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("LobbyManager"); if (!(type != null)) { return null; } return AccessTools.Method(type, "ServerAddPlayer", (Type[])null, (Type[])null); } private static bool Prefix(object __instance, NetworkConnectionToClient conn, int playerIndex) { //IL_0015: 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_001b: 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) //IL_005c: 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_012c: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) try { int num = OverflowTracker.EffectiveSlots(__instance); if (num > 0 && playerIndex >= num) { Vector3 pos = Vector3.zero; Quaternion rot = Quaternion.identity; bool flag = Plugin.PlaceOverflowInLobbyRow != null && Plugin.PlaceOverflowInLobbyRow.Value && OverflowTracker.TryGetLobbyRowSpot(__instance, playerIndex, out pos, out rot); if (Plugin.ExperimentalCloneLobbyParkingSpots != null && Plugin.ExperimentalCloneLobbyParkingSpots.Value) { if (LobbyCloneSpawner.TrySpawnClonedSpot(conn, playerIndex, flag, pos, rot, out var body)) { OverflowTracker.Conns.Add(conn.connectionId); OverflowTracker.Bodies.Add(body); Plugin.Log.LogInfo((object)($"[Experimental] Overflow player (index {playerIndex}, connId " + $"{conn.connectionId}) got a CLONED LobbyPlayer spot. Ready gate ignores it.")); return false; } Plugin.Log.LogWarning((object)"[Experimental] LobbyPlayer clone failed - falling back to forklift body."); } NetworkManager singleton = NetworkManager.singleton; GameObject val = (((Object)(object)singleton != (Object)null) ? singleton.playerPrefab : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"Overflow: NetworkManager.singleton.playerPrefab is null; falling back to skip (player will likely strand)."); OverflowTracker.Conns.Add(conn.connectionId); return false; } GameObject val2 = (flag ? Object.Instantiate(val, pos, rot) : Object.Instantiate(val)); NetworkServer.AddPlayerForConnection(conn, val2); OverflowTracker.Conns.Add(conn.connectionId); OverflowTracker.Bodies.Add(val2); Plugin.Log.LogInfo((object)($"Overflow lobby player (index {playerIndex} >= {num} slots, " + $"connId {conn.connectionId}) - gave it a forklift body at " + (flag ? ((object)(Vector3)(ref pos)).ToString() : "default position") + " via AddPlayerForConnection (no lobby slot). Ready gate will ignore it.")); return false; } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"overflow-add prefix: {arg}"); if (conn != null) { OverflowTracker.Conns.Add(conn.connectionId); } return false; } return true; } } [HarmonyPatch] internal static class Patch_LobbyDisconnectOverflow { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("LobbyManager"); if (!(type != null)) { return null; } return AccessTools.Method(type, "ServerDisconnected", (Type[])null, (Type[])null); } private static bool Prefix(object __instance, NetworkConnectionToClient conn, int playerIndex) { try { int num = OverflowTracker.EffectiveSlots(__instance); if ((conn != null && OverflowTracker.Conns.Contains(conn.connectionId)) || (num > 0 && playerIndex >= num)) { if (conn != null) { OverflowTracker.Conns.Remove(conn.connectionId); } OverflowTracker.Bodies.RemoveAll((GameObject b) => (Object)(object)b == (Object)null); Plugin.Log.LogInfo((object)$"Overflow disconnect (index {playerIndex}) - skipping slot teardown."); return false; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("overflow-disconnect prefix: " + ex.Message)); } return true; } } [HarmonyPatch] internal static class Patch_CleanupOverflowBodies { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("AggroNetworkManager"); if (!(type != null)) { return null; } return AccessTools.Method(type, "SpawnPlayers", (Type[])null, (Type[])null); } private static void Prefix() { try { int num = 0; foreach (GameObject body in OverflowTracker.Bodies) { if ((Object)(object)body == (Object)null) { continue; } try { NetworkIdentity component = body.GetComponent(); NetworkConnectionToClient val = (((Object)(object)component != (Object)null) ? component.connectionToClient : null); if (val != null) { NetworkServer.RemovePlayerForConnection(val, (RemovePlayerOptions)2); } else { NetworkServer.Destroy(body); } num++; } catch (Exception ex) { Plugin.Log.LogWarning((object)("overflow body cleanup: " + ex.Message)); try { Object.Destroy((Object)(object)body); } catch { } } } if (num > 0) { Plugin.Log.LogInfo((object)($"Run start: destroyed {num} leftover overflow lobby body(ies); " + "SpawnPlayers will give those connections clean run forklifts.")); } } finally { OverflowTracker.Bodies.Clear(); OverflowTracker.Conns.Clear(); } } } [HarmonyPatch] internal static class Patch_VoiceOverflowGuard { private static IEnumerable TargetMethods() { Type t = AccessTools.TypeByName("VoiceManager"); string[] array = new string[4] { "AddPlayer", "RemovePlayer", "OnPlayerJoinedSession", "OnPlayerLeftSession" }; foreach (string text in array) { MethodInfo methodInfo = ((t != null) ? AccessTools.Method(t, text, (Type[])null, (Type[])null) : null); if (methodInfo != null) { yield return methodInfo; } } } private static Exception Finalizer(Exception __exception, MethodBase __originalMethod) { if (__exception is ArgumentOutOfRangeException || __exception is IndexOutOfRangeException) { Plugin.Log.LogInfo((object)("Voice overflow in " + __originalMethod.Name + " swallowed (player 5+ has no in-game voice; this is expected with Crew Size Mod).")); return null; } return __exception; } } [HarmonyPatch] internal static class Patch_ReadyIgnoreSlotless { private static bool _lastReady; private static bool _resolved; private static bool _warnedCatch; private static PropertyInfo _suppressProp; private static FieldInfo _proceedingField; private static MethodInfo _containsM; private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("PlayersManager"); if (!(type != null)) { return null; } return AccessTools.Method(type, "ServerIsReadyToProceed", (Type[])null, (Type[])null); } private static bool Prefix(object __instance, ref bool __result) { try { if (!NetworkServer.active) { __result = false; _lastReady = false; return false; } if (!_resolved) { _resolved = true; Type type = __instance.GetType(); _suppressProp = AccessTools.Property(type, "serverSuppressProceed"); _proceedingField = AccessTools.Field(type, "_syncPlayersProceeding"); } if (_suppressProp != null && (bool)_suppressProp.GetValue(__instance)) { __result = false; _lastReady = false; return false; } object obj = _proceedingField?.GetValue(__instance); if (obj == null) { return true; } ICollection collection = obj as ICollection; if (collection == null && _containsM == null) { _containsM = AccessTools.Method(obj.GetType(), "Contains", (Type[])null, (Type[])null); } if (collection == null && _containsM == null) { return true; } object[] array = ((collection == null) ? new object[1] : null); foreach (KeyValuePair connection in NetworkServer.connections) { if (OverflowTracker.Conns.Contains(connection.Key)) { continue; } NetworkIdentity val = ((connection.Value != null) ? ((NetworkConnection)connection.Value).identity : null); if (!((Object)(object)val == (Object)null)) { bool flag; if (collection != null) { flag = collection.Contains(val); } else { array[0] = val; flag = (bool)_containsM.Invoke(obj, array); } if (!flag) { __result = false; _lastReady = false; return false; } } } __result = true; if (!_lastReady) { _lastReady = true; Plugin.Log.LogInfo((object)"ReadyToProceed: all slotted players ready (slotless overflow ignored)."); } return false; } catch (Exception ex) { if (!_warnedCatch) { _warnedCatch = true; Plugin.Log.LogWarning((object)("ready-ignore-slotless prefix: " + ex.Message + "; falling back to original (logged once).")); } return true; } } } [HarmonyPatch] internal static class Patch_SwallowOverflowBodyNre { private static readonly HashSet _noticed = new HashSet(); private static IEnumerable TargetMethods() { (string, string[])[] array = new(string, string[])[2] { ("PlayerEffects", new string[3] { "OnUpdateSimulation", "OnUpdateSimulationEarly", "OnUpdateSimulationLate" }), ("FloaterPopulator", new string[1] { "OnEntityCreated" }) }; (string type, string[] methods)[] array2 = array; for (int i = 0; i < array2.Length; i++) { (string type, string[] methods) tuple = array2[i]; string item = tuple.type; string[] item2 = tuple.methods; Type t = AccessTools.TypeByName(item); if (t == null) { continue; } string[] array3 = item2; foreach (string text in array3) { MethodInfo methodInfo = AccessTools.Method(t, text, (Type[])null, (Type[])null); if (methodInfo != null) { yield return methodInfo; } } } } private static Exception Finalizer(Exception __exception, MethodBase __originalMethod) { if (__exception is NullReferenceException) { string text = __originalMethod.DeclaringType?.Name + "." + __originalMethod.Name; if (_noticed.Add(text)) { Plugin.Log.LogInfo((object)("Swallowing " + text + " NullReferenceException on the overflow player's forklift (cosmetic; not a crash). This notice prints once per method.")); } return null; } return __exception; } } [HarmonyPatch] internal static class Patch_QuotaReportPlayerCap { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("QuotaReportUI"); if (!(type != null)) { return null; } return AccessTools.Method(type, "UpdateUIData", (Type[])null, (Type[])null); } private static void Prefix(object __instance, IList playerResults, out object __state) { __state = null; try { if (playerResults == null) { return; } int num = (AccessTools.Field(__instance.GetType(), "allPlayerCrashoutCounts")?.GetValue(__instance) as Array)?.Length ?? 4; if (num < 1) { num = 4; } if (playerResults.Count > num) { List list = new List(); for (int num2 = playerResults.Count - 1; num2 >= num; num2--) { list.Add(playerResults[num2]); playerResults.RemoveAt(num2); } __state = list; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("quota-report cap prefix: " + ex.Message)); } } private static void Postfix(IList playerResults, object __state) { try { if (__state is List list && playerResults != null) { for (int num = list.Count - 1; num >= 0; num--) { playerResults.Add(list[num]); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("quota-report cap postfix: " + ex.Message)); } } } [HarmonyPatch] internal static class Patch_ProceedUiPlayerCap { private static FieldInfo _fProceeds; private static FieldInfo _fIcons; private static FieldInfo _fParents; private static bool _expandNoticed; private static bool _warnedCap; private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("PlayerProceedUI"); if (!(type != null)) { return null; } return AccessTools.Method(type, "OnUpdatePresentation", (Type[])null, (Type[])null); } private static IEnumerable Transpiler(IEnumerable instructions) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown List list = new List(instructions); MethodInfo method = typeof(Patch_ProceedUiPlayerCap).GetMethod("Cap", BindingFlags.Static | BindingFlags.NonPublic); for (int i = 0; i < list.Count; i++) { MethodInfo methodInfo = list[i].operand as MethodInfo; if (methodInfo != null && methodInfo.Name == "GetAllPlayerProceeds") { list.Insert(i + 1, new CodeInstruction(OpCodes.Call, (object)method)); list.Insert(i + 1, new CodeInstruction(OpCodes.Ldarg_0, (object)null)); Plugin.Log.LogInfo((object)"PlayerProceedUI: injected proceed-icon cap after GetAllPlayerProceeds."); return list; } } Plugin.Log.LogWarning((object)"PlayerProceedUI: GetAllPlayerProceeds call not found; proceed-icon cap NOT applied (5+ players may flood this UI). Method left unchanged."); return list; } private static void Cap(object self) { try { Type type = self.GetType(); if (_fProceeds == null) { _fProceeds = AccessTools.Field(type, "_playerProceeds"); } if (_fIcons == null) { _fIcons = AccessTools.Field(type, "playerIcons"); } if (_fParents == null) { _fParents = AccessTools.Field(type, "playerParents"); } IList list = _fProceeds?.GetValue(self) as IList; IList list2 = _fIcons?.GetValue(self) as IList; if (list == null || list2 == null) { return; } if (Plugin.ExperimentalExpandPlayerUI != null && Plugin.ExperimentalExpandPlayerUI.Value) { int num = Plugin.ExperimentalExpandPreviewCount?.Value ?? 0; if (num > list.Count && list.Count > 0) { object value = list[list.Count - 1]; while (list.Count < num) { list.Add(value); } } EnsureExpanded(_fParents?.GetValue(self) as IList, list2, list.Count); } else { while (list.Count > list2.Count) { list.RemoveAt(list.Count - 1); } } } catch (Exception ex) { if (!_warnedCap) { _warnedCap = true; Plugin.Log.LogWarning((object)("proceed-icon adjust: " + ex.Message + " (logged once)")); } } } private static void EnsureExpanded(IList parents, IList icons, int needed) { if (parents == null || icons == null) { return; } int count = icons.Count; if (count == 0 || parents.Count == 0) { return; } int num = Math.Min(needed, 16); int index = count - 1; object? obj = parents[index]; GameObject val = (GameObject)((obj is GameObject) ? obj : null); object? obj2 = icons[index]; Component val2 = (Component)((obj2 is Component) ? obj2 : null); if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null) { return; } int num2 = 0; while (icons.Count < num) { GameObject val3 = Object.Instantiate(val, val.transform.parent); ((Object)val3).name = ((Object)val).name + "_crewmod" + icons.Count; Transform val4 = MapByPath(val.transform, val2.transform, val3.transform); Component val5 = (((Object)(object)val4 != (Object)null) ? ((Component)val4).GetComponent(((object)val2).GetType()) : val3.GetComponentInChildren(((object)val2).GetType(), true)); if ((Object)(object)val5 == (Object)null) { Object.Destroy((Object)(object)val3); break; } parents.Add(val3); icons.Add(val5); num2++; } if (num2 > 0 && !_expandNoticed) { _expandNoticed = true; Plugin.Log.LogInfo((object)($"[Experimental] Expanded proceed icons from {count} to {icons.Count} " + "(cloned authored slots to show all players). Check the layout in-game.")); } } private static Transform MapByPath(Transform root, Transform target, Transform newRoot) { List list = new List(); Transform val = target; while ((Object)(object)val != (Object)null && (Object)(object)val != (Object)(object)root) { list.Add(val.GetSiblingIndex()); val = val.parent; } if ((Object)(object)val != (Object)(object)root) { return null; } list.Reverse(); Transform val2 = newRoot; foreach (int item in list) { if (item < 0 || item >= val2.childCount) { return null; } val2 = val2.GetChild(item); } return val2; } } internal static class ModPresence { public const string Key = "CrewSizeMod"; private static Callback _enterCb; private static Callback _dataCb; private static CSteamID _lobby; private static string _lastSummary = ""; public static bool AllPeersModded { get; private set; } public static int MemberCount { get; private set; } public static int ModdedCount { get; private set; } public static void Init() { try { _enterCb = Callback.Create((DispatchDelegate)OnLobbyEnter); _dataCb = Callback.Create((DispatchDelegate)OnLobbyDataUpdate); Plugin.Log.LogInfo((object)"Mod-presence handshake registered (advertises via Steam lobby member data)."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("presence init failed (non-critical, informational only): " + ex.Message)); } } private static void OnLobbyEnter(LobbyEnter_t e) { //IL_0000: 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_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) try { _lobby = new CSteamID(e.m_ulSteamIDLobby); SteamMatchmaking.SetLobbyMemberData(_lobby, "CrewSizeMod", "0.10.9"); _lastSummary = ""; Scan(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("presence enter: " + ex.Message)); } } private static void OnLobbyDataUpdate(LobbyDataUpdate_t e) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) if (((CSteamID)(ref _lobby)).IsValid() && e.m_ulSteamIDLobby == _lobby.m_SteamID) { Scan(); } } private static void Scan() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) try { if (!((CSteamID)(ref _lobby)).IsValid()) { return; } int numLobbyMembers = SteamMatchmaking.GetNumLobbyMembers(_lobby); int num = 0; for (int i = 0; i < numLobbyMembers; i++) { CSteamID lobbyMemberByIndex = SteamMatchmaking.GetLobbyMemberByIndex(_lobby, i); if (!string.IsNullOrEmpty(SteamMatchmaking.GetLobbyMemberData(_lobby, lobbyMemberByIndex, "CrewSizeMod"))) { num++; } } MemberCount = numLobbyMembers; ModdedCount = num; AllPeersModded = numLobbyMembers > 0 && num >= numLobbyMembers; string text = $"{num}/{numLobbyMembers}"; if (text == _lastSummary) { return; } _lastSummary = text; if (numLobbyMembers > 0) { if (num >= numLobbyMembers) { Plugin.Log.LogInfo((object)$"Crew Size Mod presence: all {numLobbyMembers} lobby member(s) have the mod - full experience."); } else { Plugin.Log.LogWarning((object)($"Crew Size Mod presence: {num}/{numLobbyMembers} lobby member(s) have the mod. " + $"The {numLobbyMembers - num} without it can still play, but may see voice/UI glitches at 5+ players " + "(those fixes run client-side). Ask them to install Crew Size Mod for the clean experience.")); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("presence scan: " + ex.Message)); } } } [HarmonyPatch] internal static class Patch_ProceedUiDebugLog { private static IEnumerable TargetMethods() { Type t = AccessTools.TypeByName("PlayerProceedUI"); string[] array = new string[2] { "Show", "Hide" }; foreach (string text in array) { MethodInfo methodInfo = ((t != null) ? AccessTools.Method(t, text, (Type[])null, (Type[])null) : null); if (methodInfo != null) { yield return methodInfo; } } } private static void Postfix(object __instance, MethodBase __originalMethod) { //IL_006a: 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) if (Plugin.ExperimentalExpandPlayerUI == null || !Plugin.ExperimentalExpandPlayerUI.Value) { return; } try { if (__originalMethod.Name == "Hide") { Plugin.Log.LogInfo((object)"PlayerProceedUI HIDDEN."); return; } int num = 0; if (AccessTools.Field(__instance.GetType(), "playerIcons")?.GetValue(__instance) is ICollection collection) { num = collection.Count; } Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; Plugin.Log.LogInfo((object)($"PlayerProceedUI SHOWN - scene '{name}', {num} authored icon slot(s). " + "(These are the ready-up proceed icons; expand line follows if it grows.)")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("proceed-ui debug log: " + ex.Message)); } } } internal static class LobbyCloneSpawner { public const uint AssetId = 3320164359u; private static bool _registered; private static GameObject _cachedTemplate; public static void RegisterFresh() { _registered = false; _cachedTemplate = null; Register(); } public static void Register() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_003f: Expected O, but got Unknown if (_registered || Plugin.ExperimentalCloneLobbyParkingSpots == null || !Plugin.ExperimentalCloneLobbyParkingSpots.Value) { return; } try { NetworkClient.RegisterSpawnHandler(3320164359u, new SpawnHandlerDelegate(SpawnHandler), new UnSpawnDelegate(UnspawnHandler)); _registered = true; Plugin.Log.LogInfo((object)$"[Experimental] Registered lobby-spot spawn handler (assetId 0x{3320164359u:X})."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[Experimental] register spawn handler: " + ex.Message)); } } private static GameObject SpawnHandler(SpawnMessage msg) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) try { GameObject val = FindTemplate(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[Experimental] spawn handler: no LobbyPlayer template on this client."); return null; } return Object.Instantiate(val, msg.position, msg.rotation); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[Experimental] spawn handler: " + ex.Message)); return null; } } private static void UnspawnHandler(GameObject obj) { if ((Object)(object)obj != (Object)null) { Object.Destroy((Object)(object)obj); } } private static GameObject FindTemplate() { if ((Object)(object)_cachedTemplate != (Object)null) { return _cachedTemplate; } Type type = AccessTools.TypeByName("LobbyPlayer"); if (type == null) { return null; } FieldInfo fieldInfo = AccessTools.Field(type, "lobbyPlayerIndex"); Object[] array = Object.FindObjectsOfType(type); GameObject val = null; int num = int.MaxValue; Object[] array2 = array; foreach (Object obj in array2) { Component val2 = (Component)(object)((obj is Component) ? obj : null); if (val2 == null) { continue; } int num2 = 0; try { if (fieldInfo != null) { num2 = (int)fieldInfo.GetValue(val2); } } catch { } if (num2 < num) { num = num2; val = val2.gameObject; } } _cachedTemplate = val; return val; } private static GameObject MakeStampedClone(bool placed, Vector3 pos, Quaternion rot, int index, out object lp, out Type lpType) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) lp = null; lpType = null; GameObject val = FindTemplate(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"[Experimental] clone: no LobbyPlayer template."); return null; } GameObject val2 = (placed ? Object.Instantiate(val, pos, rot) : Object.Instantiate(val)); NetworkIdentity component = val2.GetComponent(); if ((Object)(object)component == (Object)null) { Object.Destroy((Object)(object)val2); Plugin.Log.LogWarning((object)"[Experimental] clone has no NetworkIdentity."); return null; } Type typeFromHandle = typeof(NetworkIdentity); AccessTools.Field(typeFromHandle, "sceneId")?.SetValue(component, 0uL); AccessTools.Field(typeFromHandle, "_assetId")?.SetValue(component, 3320164359u); lpType = AccessTools.TypeByName("LobbyPlayer"); lp = ((lpType != null) ? val2.GetComponent(lpType) : null); if (lpType != null) { AccessTools.Field(lpType, "lobbyPlayerIndex")?.SetValue(lp, index); } return val2; } private static void MarkAssigned(object lp, Type lpType) { if (lp != null && lpType != null) { AccessTools.Method(lpType, "ServerPlayerAssigned", (Type[])null, (Type[])null)?.Invoke(lp, null); } } public static bool TrySpawnClonedSpot(NetworkConnectionToClient conn, int index, bool placed, Vector3 pos, Quaternion rot, out GameObject body) { //IL_000a: 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) body = null; try { Register(); object lp; Type lpType; GameObject val = MakeStampedClone(placed, pos, rot, index, out lp, out lpType); if ((Object)(object)val == (Object)null) { return false; } NetworkServer.AddPlayerForConnection(conn, val); MarkAssigned(lp, lpType); body = val; return true; } catch (Exception arg) { Plugin.Log.LogWarning((object)$"[Experimental] clone spawn failed: {arg}"); return false; } } public static GameObject TrySpawnPreviewClone(bool placed, Vector3 pos, Quaternion rot, int index) { //IL_0006: 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) try { Register(); object lp; Type lpType; GameObject val = MakeStampedClone(placed, pos, rot, index, out lp, out lpType); if ((Object)(object)val == (Object)null) { return null; } if (NetworkServer.active) { NetworkServer.Spawn(val, (NetworkConnectionToClient)null); } MarkAssigned(lp, lpType); return val; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[Experimental] preview clone: " + ex.Message)); return null; } } } [HarmonyPatch] internal static class Patch_LobbyPreviewSpawner { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("LobbyManager"); if (!(type != null)) { return null; } return AccessTools.Method(type, "OnEntityStart", (Type[])null, (Type[])null); } private static void Postfix(object __instance) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) if (!NetworkServer.active) { return; } try { OverflowTracker.ApplyManualToRealSpots(__instance); int num = Plugin.ExperimentalLobbyPreviewCount?.Value ?? 0; if (num <= 0) { return; } int num2 = 0; for (int i = 0; i < num; i++) { int num3 = 1 + i; Vector3 pos; Quaternion rot; bool flag = OverflowTracker.TryGetLobbyRowSpot(__instance, num3, out pos, out rot); GameObject val = null; if (Plugin.ExperimentalCloneLobbyParkingSpots != null && Plugin.ExperimentalCloneLobbyParkingSpots.Value) { val = LobbyCloneSpawner.TrySpawnPreviewClone(flag, pos, rot, num3); } if ((Object)(object)val == (Object)null) { GameObject val2 = NetworkManager.singleton?.playerPrefab; if ((Object)(object)val2 == (Object)null) { continue; } val = (flag ? Object.Instantiate(val2, pos, rot) : Object.Instantiate(val2)); NetworkIdentity component = val.GetComponent(); if ((Object)(object)component != (Object)null && component.netId == 0) { NetworkServer.Spawn(val, (NetworkConnectionToClient)null); } } if ((Object)(object)val != (Object)null) { OverflowTracker.Bodies.Add(val); num2++; } } Plugin.Log.LogInfo((object)$"[Experimental] Spawned {num2} fake lobby forklift(s) at spots 1..{num} for solo layout testing."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("lobby preview spawn: " + ex.Message)); } } } [HarmonyPatch] internal static class Patch_RegisterLobbyCloneHandler { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("AggroNetworkManager"); if (!(type != null)) { return null; } return AccessTools.Method(type, "OnStartClient", (Type[])null, (Type[])null); } private static void Postfix() { LobbyCloneSpawner.RegisterFresh(); } } internal static class UiExpand { public static void EnsureExpanded(object instance, FieldInfo field, int target) { if (instance == null || field == null) { return; } int num = Math.Min(target, 16); object value = field.GetValue(instance); if (value is Array array) { int length = array.Length; if (length == 0 || length >= num) { return; } Type elementType = array.GetType().GetElementType(); object? value2 = array.GetValue(length - 1); Component val = (Component)((value2 is Component) ? value2 : null); if ((Object)(object)val == (Object)null) { return; } Array array2 = Array.CreateInstance(elementType, num); Array.Copy(array, array2, length); for (int i = length; i < num; i++) { GameObject val2 = Object.Instantiate(val.gameObject, val.gameObject.transform.parent); Component component = val2.GetComponent(elementType); if ((Object)(object)component == (Object)null) { Object.Destroy((Object)(object)val2); break; } array2.SetValue(component, i); } field.SetValue(instance, array2); } else { if (!(value is IList list) || list.Count == 0 || list.Count >= num) { return; } object? obj = list[list.Count - 1]; Component val3 = (Component)((obj is Component) ? obj : null); if ((Object)(object)val3 == (Object)null) { return; } Type type = ((object)val3).GetType(); while (list.Count < num) { GameObject val4 = Object.Instantiate(val3.gameObject, val3.gameObject.transform.parent); Component component2 = val4.GetComponent(type); if ((Object)(object)component2 == (Object)null) { Object.Destroy((Object)(object)val4); break; } list.Add(component2); } } } } [HarmonyPatch] internal static class Patch_ShiftProceedAreaExpand { private static FieldInfo _f; private static bool _warned; private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("ShiftProceedArea"); if (!(type != null)) { return null; } return AccessTools.Method(type, "OnUpdatePresentation", (Type[])null, (Type[])null); } private static void Prefix(object __instance) { try { if (_f == null) { _f = AccessTools.Field(__instance.GetType(), "playerImages"); } UiExpand.EnsureExpanded(__instance, _f, Plugin.MaxPlayers.Value); } catch (Exception ex) { if (!_warned) { _warned = true; Plugin.Log.LogWarning((object)("shift-proceed-area expand: " + ex.Message + " (logged once)")); } } } } [HarmonyPatch] internal static class Patch_ModifierVoteExpand { private static bool _resolved; private static bool _warned; private static FieldInfo _fUndecided; private static FieldInfo _fBtnA; private static FieldInfo _fBtnB; private static FieldInfo _fPlayers; private static FieldInfo[] _btnFields; private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("ModifierChoiceManagerUI"); if (!(type != null)) { return null; } return AccessTools.Method(type, "RefreshVotes", (Type[])null, (Type[])null); } private static void Prefix(object __instance) { try { int value = Plugin.MaxPlayers.Value; if (!_resolved) { _resolved = true; Type type = __instance.GetType(); _fUndecided = AccessTools.Field(type, "undecidedPlayers"); _fBtnA = AccessTools.Field(type, "choiceButtonA"); _fBtnB = AccessTools.Field(type, "choiceButtonB"); _btnFields = new FieldInfo[2] { _fBtnA, _fBtnB }; } UiExpand.EnsureExpanded(__instance, _fUndecided, value); FieldInfo[] btnFields = _btnFields; for (int i = 0; i < btnFields.Length; i++) { object obj = btnFields[i]?.GetValue(__instance); if (obj != null) { if (_fPlayers == null) { _fPlayers = AccessTools.Field(obj.GetType(), "players"); } UiExpand.EnsureExpanded(obj, _fPlayers, value); } } } catch (Exception ex) { if (!_warned) { _warned = true; Plugin.Log.LogWarning((object)("modifier-vote expand: " + ex.Message + " (logged once)")); } } } } [HarmonyPatch] internal static class Patch_SwallowUiOverflow { private static readonly HashSet _noticed = new HashSet(); private static IEnumerable TargetMethods() { (string, string[])[] array = new(string, string[])[2] { ("ShiftProceedArea", new string[1] { "OnUpdatePresentation" }), ("ModifierChoiceManagerUI", new string[2] { "RefreshVotes", "OnUpdatePresentation" }) }; (string type, string[] methods)[] array2 = array; for (int i = 0; i < array2.Length; i++) { (string type, string[] methods) tuple = array2[i]; string item = tuple.type; string[] item2 = tuple.methods; Type t = AccessTools.TypeByName(item); if (t == null) { continue; } string[] array3 = item2; foreach (string text in array3) { MethodInfo methodInfo = AccessTools.Method(t, text, (Type[])null, (Type[])null); if (methodInfo != null) { yield return methodInfo; } } } } private static Exception Finalizer(Exception __exception, MethodBase __originalMethod) { if (__exception is IndexOutOfRangeException || __exception is ArgumentOutOfRangeException) { string text = __originalMethod.DeclaringType?.Name + "." + __originalMethod.Name; if (_noticed.Add(text)) { Plugin.Log.LogInfo((object)("Swallowing " + text + " index overflow at 5+ players (cosmetic; prevents flood/lag). Prints once.")); } return null; } return __exception; } } internal enum ScreenCorner { TopLeft, TopRight, BottomLeft, BottomRight } internal static class CreditUI { private const string Text = "MoreCrew by Izaya_here\nFollow on Twitch: twitch.tv/izaya_here"; private static GameObject _go; private static RectTransform _rt; private static Text _text; private static RawImage _raw; private static Texture2D _tex; private static bool _created; private static bool _failed; internal static object ShiftMgr; private static FieldInfo _phaseField; private const int PHASE_SHIFT = 3; private static bool _lastInShift; private static int _lastFrame = -1; private static ScreenCorner _appliedCorner = (ScreenCorner)(-1); private static float _appliedWidth = float.NaN; internal static void SetShiftManager(object mgr) { ShiftMgr = mgr; if (mgr != null && _phaseField == null) { _phaseField = AccessTools.Field(mgr.GetType(), "_shiftPhase"); } } internal static void ClearShiftManager(object mgr) { if (mgr == null || ShiftMgr == mgr) { ShiftMgr = null; } } private static bool InActiveShift() { try { object shiftMgr = ShiftMgr; if ((Object)((shiftMgr is Object) ? shiftMgr : null) == (Object)null) { return false; } if (_phaseField == null) { return false; } return Convert.ToInt32(_phaseField.GetValue(ShiftMgr)) == 3; } catch { return false; } } public static bool ShouldShow() { if (Plugin.ShowTwitchCredit == null || !Plugin.ShowTwitchCredit.Value) { return false; } bool flag = InActiveShift(); if (flag != _lastInShift) { _lastInShift = flag; Plugin.Log.LogInfo((object)$"Credit: in-round = {flag} (credit hidden while true)."); } return !flag; } public static void Tick() { if (_failed) { return; } int frameCount = Time.frameCount; if (frameCount == _lastFrame) { return; } _lastFrame = frameCount; try { if (!_created) { Create(); } if ((Object)(object)_go != (Object)null) { ApplyCorner(); bool flag = ShouldShow(); if (_go.activeSelf != flag) { _go.SetActive(flag); } } } catch (Exception ex) { _failed = true; Plugin.Log.LogWarning((object)("credit ui: " + ex.Message)); } } private static string PngPath() { try { string directoryName = Path.GetDirectoryName(typeof(Plugin).Assembly.Location); return string.IsNullOrEmpty(directoryName) ? null : Path.Combine(directoryName, "credit.png"); } catch { return null; } } private static Texture2D LoadPng() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown try { string text = PngPath(); if (text == null || !File.Exists(text)) { return null; } byte[] array = File.ReadAllBytes(text); Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false); if (!ImageConversion.LoadImage(val, array)) { return null; } ((Texture)val).wrapMode = (TextureWrapMode)1; ((Texture)val).filterMode = (FilterMode)1; return val; } catch (Exception ex) { Plugin.Log.LogWarning((object)("credit png load: " + ex.Message)); return null; } } private static void Create() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_021b: Unknown result type (might be due to invalid IL or missing references) //IL_0243: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) _created = true; _go = new GameObject("CrewSizeMod.CreditCanvas"); Object.DontDestroyOnLoad((Object)(object)_go); Canvas obj = _go.AddComponent(); obj.renderMode = (RenderMode)0; obj.sortingOrder = 30000; _go.AddComponent().uiScaleMode = (ScaleMode)0; _tex = LoadPng(); if ((Object)(object)_tex != (Object)null) { GameObject val = new GameObject("Image"); val.transform.SetParent(_go.transform, false); _raw = val.AddComponent(); _raw.texture = (Texture)(object)_tex; ((Graphic)_raw).raycastTarget = false; _rt = ((Graphic)_raw).rectTransform; float num = Plugin.CreditWidth?.Value ?? 320f; float num2 = num * (float)((Texture)_tex).height / (float)Mathf.Max(1, ((Texture)_tex).width); _rt.sizeDelta = new Vector2(num, num2); ApplyCorner(); Plugin.Log.LogInfo((object)$"Credit UI canvas created (uGUI image, credit.png {((Texture)_tex).width}x{((Texture)_tex).height} -> {(int)num}x{(int)num2} px)."); } else { GameObject val2 = new GameObject("Text"); val2.transform.SetParent(_go.transform, false); _text = val2.AddComponent(); _text.font = Font.CreateDynamicFontFromOSFont("Arial", 20); _text.fontSize = 20; _text.fontStyle = (FontStyle)1; ((Graphic)_text).color = Color.white; _text.text = "MoreCrew by Izaya_here\nFollow on Twitch: twitch.tv/izaya_here"; _text.horizontalOverflow = (HorizontalWrapMode)1; _text.verticalOverflow = (VerticalWrapMode)1; ((Graphic)_text).raycastTarget = false; Shadow obj2 = val2.AddComponent(); obj2.effectColor = new Color(0f, 0f, 0f, 0.85f); obj2.effectDistance = new Vector2(1.5f, -1.5f); _rt = ((Graphic)_text).rectTransform; _rt.sizeDelta = new Vector2(520f, 64f); ApplyCorner(); Plugin.Log.LogInfo((object)"Credit UI canvas created (uGUI text fallback - credit.png not found next to the DLL)."); } } private static void ApplyCorner() { //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009f: 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_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_rt == (Object)null) { return; } ScreenCorner screenCorner = Plugin.TwitchCreditCorner?.Value ?? ScreenCorner.TopLeft; float num = Plugin.CreditWidth?.Value ?? 320f; if (screenCorner != _appliedCorner || num != _appliedWidth) { _appliedCorner = screenCorner; _appliedWidth = num; bool flag = screenCorner == ScreenCorner.TopLeft || screenCorner == ScreenCorner.BottomLeft; bool flag2 = screenCorner == ScreenCorner.TopLeft || screenCorner == ScreenCorner.TopRight; Vector2 val = default(Vector2); ((Vector2)(ref val))..ctor(flag ? 0f : 1f, flag2 ? 1f : 0f); _rt.anchorMin = val; _rt.anchorMax = val; _rt.pivot = val; _rt.anchoredPosition = new Vector2(flag ? 18f : (-18f), flag2 ? (-14f) : 14f); if ((Object)(object)_raw != (Object)null && (Object)(object)_tex != (Object)null) { float num2 = num * (float)((Texture)_tex).height / (float)Mathf.Max(1, ((Texture)_tex).width); _rt.sizeDelta = new Vector2(num, num2); } if ((Object)(object)_text != (Object)null) { _text.alignment = (TextAnchor)(screenCorner switch { ScreenCorner.BottomRight => 8, ScreenCorner.TopRight => 2, ScreenCorner.TopLeft => 0, _ => 6, }); } } } } [HarmonyPatch] internal static class Patch_CreditDriver { private static bool _logged; private static IEnumerable TargetMethods() { (string, string)[] array = new(string, string)[4] { ("GameManager", "Update"), ("ShiftManager", "OnUpdatePresentation"), ("GameMenuUI", "OnUpdatePresentation"), ("LobbyButtonsUI", "OnUpdatePresentation") }; (string type, string method)[] array2 = array; for (int i = 0; i < array2.Length; i++) { (string type, string method) tuple = array2[i]; string item = tuple.type; string item2 = tuple.method; Type type = AccessTools.TypeByName(item); MethodInfo methodInfo = ((type != null) ? AccessTools.Method(type, item2, (Type[])null, (Type[])null) : null); if (methodInfo != null) { yield return methodInfo; } } } private static void Postfix() { if (!_logged) { _logged = true; Plugin.Log.LogInfo((object)"Credit driver is firing (uGUI path active)."); } CreditUI.Tick(); } } [HarmonyPatch] internal static class Patch_ShiftLifecycle { private static IEnumerable TargetMethods() { Type t = AccessTools.TypeByName("ShiftManager"); if (t == null) { yield break; } string[] array = new string[2] { "OnEntityStart", "OnEntityDestroyed" }; foreach (string text in array) { MethodInfo methodInfo = AccessTools.Method(t, text, (Type[])null, (Type[])null); if (methodInfo != null) { yield return methodInfo; } } } private static void Postfix(object __instance, MethodBase __originalMethod) { if (__originalMethod.Name == "OnEntityStart") { CreditUI.SetShiftManager(__instance); } else { CreditUI.ClearShiftManager(__instance); } } } [HarmonyPatch] internal static class Patch_FinalReportPlayerCap { private static readonly string[] PerPlayerLists = new string[12] { "playerIconImages", "playerNameTexts", "playerHighlightImages", "playerIconObjects", "crashoutCountObjects", "nitroCountObjects", "driftCountObjects", "upgradeCountObjects", "crashoutCountTexts", "nitroCountTexts", "driftCountTexts", "upgradeCountTexts" }; private static bool _noticed; private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("ReportUI"); if (!(type != null)) { return null; } return AccessTools.Method(type, "StartSequenceCo", (Type[])null, (Type[])null); } private static void Postfix(object __instance, object __result) { try { if (__instance == null || __result == null) { return; } FieldInfo fieldInfo = AccessTools.Field(__result.GetType(), "playerResults"); if (fieldInfo == null || !(fieldInfo.GetValue(__result) is Array array)) { return; } int num = RowCapacity(__instance); if (num >= 1 && array.Length > num) { Array array2 = Array.CreateInstance(array.GetType().GetElementType(), num); Array.Copy(array, array2, num); fieldInfo.SetValue(__result, array2); if (!_noticed) { _noticed = true; Plugin.Log.LogWarning((object)($"Final report (ReportUI): capped {array.Length} players -> {num} UI rows to " + "avoid the end-of-run freeze. Players 5+ get no row on the grade screen (cosmetic).")); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("final-report cap postfix: " + ex.Message)); } } private static int RowCapacity(object reportUi) { int num = int.MaxValue; Type type = reportUi.GetType(); string[] perPlayerLists = PerPlayerLists; foreach (string text in perPlayerLists) { if (AccessTools.Field(type, text)?.GetValue(reportUi) is IList list && list.Count > 0 && list.Count < num) { num = list.Count; } } if (num != int.MaxValue) { return num; } return 4; } } }