Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of BiggerLobbies v2.0.0
mods/BiggerLobbies_Il2cpp.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Pipes; using System.Linq; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BiggerLobbies; using BiggerLobbies.Integrations; using BiggerLobbies.Utils; using HarmonyLib; using Il2Cpp; using Il2CppFishNet; using Il2CppFishNet.Connection; using Il2CppFishNet.Managing.Server; using Il2CppFishNet.Managing.Transporting; using Il2CppFishNet.Transporting; using Il2CppFishNet.Transporting.Multipass; using Il2CppFishySteamworks; using Il2CppFishySteamworks.Server; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppScheduleOne.AvatarFramework; using Il2CppScheduleOne.Core.Items.Framework; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.GameTime; using Il2CppScheduleOne.Growing; using Il2CppScheduleOne.ItemFramework; using Il2CppScheduleOne.NPCs.Behaviour; using Il2CppScheduleOne.Networking; using Il2CppScheduleOne.ObjectScripts; using Il2CppScheduleOne.Persistence; using Il2CppScheduleOne.Persistence.Datas; using Il2CppScheduleOne.Platform; using Il2CppScheduleOne.PlayerScripts; using Il2CppScheduleOne.State; using Il2CppScheduleOne.UI; using Il2CppScheduleOne.UI.MainMenu; using Il2CppScheduleOne.UI.Multiplayer; using Il2CppSteamworks; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using Il2CppSystem.Reflection; using Il2CppTMPro; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.Rendering; 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: MelonInfo(typeof(Core), "BiggerLobbies", "2.0.0", "Bars", null)] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: MelonIncompatibleAssemblies(new string[] { "ModManager&PhoneApp" })] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("BiggerLobbies_Il2cpp")] [assembly: AssemblyConfiguration("Il2cpp")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+19afa723c03a18bac3568b8fe811fc6c8cf8e1c8")] [assembly: AssemblyProduct("BiggerLobbies_Il2cpp")] [assembly: AssemblyTitle("BiggerLobbies_Il2cpp")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace BiggerLobbies { public class Core : MelonMod { public static Core? Instance { get; private set; } public override void OnInitializeMelon() { Instance = this; HarmonyPatches.SetModInstance(this); AutoSave.Init(); } public override void OnUpdate() { AutoSave.Update(); DebugDummies.Update(); DepartedPlayers.Update(); HostSettingsMenu.Update(); DiscordPresence.Update(); } public override void OnSceneWasInitialized(int buildIndex, string sceneName) { DebugDummies.OnSceneLoaded(sceneName); HostSettingsMenu.OnSceneLoaded(sceneName); } public override void OnApplicationQuit() { DiscordPresence.Shutdown(); Instance = null; } } } namespace BiggerLobbies.Utils { public static class Constants { public static class Game { public const string GAME_STUDIO = "TVGS"; public const string GAME_NAME = "Schedule I"; } public const string MOD_NAME = "BiggerLobbies"; public const string MOD_VERSION = "2.0.0"; public const string MOD_AUTHOR = "Bars"; public const int MAX_PLAYERS = 20; public const int VANILLA_LIMIT = 4; } public static class Log { private static MelonPreferences_Entry<bool>? _diag; public static bool Verbose => _diag?.Value ?? false; public static void Init(MelonPreferences_Category cat) { _diag = cat.CreateEntry<bool>("DiagnosticLog", false, "Diagnostic log", "Writes detailed info lines to the MelonLoader log (lobby events, kicks, bans, saves). Leave off unless you're reporting a problem. Warnings and errors are always logged.", false, false, (ValueValidator)null, (string)null); } public static void Info(string s) { if (Verbose) { MelonLogger.Msg(s); } } } public static class MiniJson { public static object? Parse(string s) { int i = 0; object result = Value(s, ref i); Ws(s, ref i); if (i != s.Length) { throw new FormatException($"unexpected text at {i}"); } return result; } private static void Ws(string s, ref int i) { while (i < s.Length && char.IsWhiteSpace(s[i])) { i++; } } private static object? Value(string s, ref int i) { Ws(s, ref i); if (i >= s.Length) { throw new FormatException("unexpected end"); } switch (s[i]) { case '{': return Obj(s, ref i); case '[': return Arr(s, ref i); case '"': return Str(s, ref i); default: if (Word(s, ref i, "true")) { return true; } if (Word(s, ref i, "false")) { return false; } if (Word(s, ref i, "null")) { return null; } return Num(s, ref i); } } private static bool Word(string s, ref int i, string w) { if (string.CompareOrdinal(s, i, w, 0, w.Length) != 0) { return false; } i += w.Length; return true; } private static Dictionary<string, object?> Obj(string s, ref int i) { Dictionary<string, object> dictionary = new Dictionary<string, object>(); i++; Ws(s, ref i); if (i < s.Length && s[i] == '}') { i++; return dictionary; } while (true) { Ws(s, ref i); if (i >= s.Length || s[i] != '"') { throw new FormatException($"expected a key at {i}"); } string key = Str(s, ref i); Ws(s, ref i); if (i >= s.Length || s[i] != ':') { throw new FormatException($"expected ':' at {i}"); } i++; dictionary[key] = Value(s, ref i); Ws(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == '}') { i++; return dictionary; } throw new FormatException($"expected ',' or '}}' at {i}"); } private static List<object?> Arr(string s, ref int i) { List<object> list = new List<object>(); i++; Ws(s, ref i); if (i < s.Length && s[i] == ']') { i++; return list; } while (true) { list.Add(Value(s, ref i)); Ws(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == ']') { i++; return list; } throw new FormatException($"expected ',' or ']' at {i}"); } private static string Str(string s, ref int i) { StringBuilder stringBuilder = new StringBuilder(); i++; while (i < s.Length) { char c = s[i++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); continue; case '\\': break; } if (i >= s.Length) { break; } char c2 = s[i++]; switch (c2) { case 'n': stringBuilder.Append('\n'); break; case 't': stringBuilder.Append('\t'); break; case 'r': stringBuilder.Append('\r'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'u': { if (i + 4 > s.Length || !int.TryParse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { throw new FormatException($"bad \\u escape at {i}"); } stringBuilder.Append((char)result); i += 4; break; } default: stringBuilder.Append(c2); break; } } throw new FormatException("unterminated string"); } private static double Num(string s, ref int i) { int num = i; while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0) { i++; } if (i == num || !double.TryParse(s.Substring(num, i - num), NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw new FormatException($"bad value at {num}"); } return result; } public static string Quote(string v) { StringBuilder stringBuilder = new StringBuilder("\""); string text = v ?? ""; foreach (char c in text) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.Append('"').ToString(); } public static string Number(double v) { return v.ToString("R", CultureInfo.InvariantCulture); } public static string Canonical(object? v) { if (v != null) { if (!(v is bool)) { if (!(v is double v2)) { if (!(v is string v3)) { Dictionary<string, object?> dictionary = v as Dictionary<string, object>; if (dictionary == null) { if (v is List<object> source) { return "[" + string.Join(",", source.Select(Canonical)) + "]"; } return Quote(v.ToString() ?? ""); } return "{" + string.Join(",", from k in dictionary.Keys.OrderBy<string, string>((string k) => k, StringComparer.Ordinal) select Quote(k) + ":" + Canonical(dictionary[k])) + "}"; } return Quote(v3); } return Number(v2); } return ((bool)v) ? "true" : "false"; } return "null"; } public static string Str(Dictionary<string, object?> o, string key) { object value; return (o.TryGetValue(key, out value) && value is string text) ? text : ""; } public static double Num(Dictionary<string, object?> o, string key) { object value; return (o.TryGetValue(key, out value) && value is double num) ? num : 0.0; } } } namespace BiggerLobbies.Integrations { [HarmonyPatch] public static class HarmonyPatches { private static Core? _modInstance; private static LobbyInterface? _ui; public static void SetModInstance(Core modInstance) { _modInstance = modInstance; } [HarmonyPrefix] [HarmonyPatch(typeof(Lobby), "TryOpenInviteInterface")] private static bool Lobby_TryOpenInviteInterface_Prefix(Lobby __instance) { try { if (!__instance.IsInLobby || __instance.PlayerCount < 4) { return true; } if (__instance.PlayerCount >= LobbySize.MaxPlayers) { MelonLogger.Warning("Lobby already at max capacity!"); return false; } ILobbyService lobbyService = __instance._lobbyService; SteamLobbyService val = ((lobbyService != null) ? ((Il2CppObjectBase)lobbyService).TryCast<SteamLobbyService>() : null); if (val == null) { return true; } val.OpenInviteUI(); return false; } catch (Exception arg) { MelonLogger.Error($"Failed to patch TryOpenInviteInterface: {arg}"); return true; } } private static void EnsurePlayersArray(SteamLobbyService svc) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) Il2CppStructArray<CSteamID> players = svc._players; if (players != null && ((Il2CppArrayBase<CSteamID>)(object)players).Length >= 250) { return; } Il2CppStructArray<CSteamID> val = new Il2CppStructArray<CSteamID>(250L); if (players != null) { for (int i = 0; i < ((Il2CppArrayBase<CSteamID>)(object)players).Length; i++) { ((Il2CppArrayBase<CSteamID>)(object)val)[i] = ((Il2CppArrayBase<CSteamID>)(object)players)[i]; } } svc._players = val; } [HarmonyPostfix] [HarmonyPatch(typeof(SteamLobbyService), "Initialize")] private static void Service_Initialize_Postfix(SteamLobbyService __instance) { try { EnsurePlayersArray(__instance); } catch (Exception arg) { MelonLogger.Error($"Failed to resize players array: {arg}"); } } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "UpdateLobbyMembers")] private static void Service_UpdateLobbyMembers_Prefix(SteamLobbyService __instance) { try { EnsurePlayersArray(__instance); } catch (Exception arg) { MelonLogger.Error($"Failed to resize players array: {arg}"); } } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "CreateLobby")] private static void Service_CreateLobby_Prefix(SteamLobbyService __instance, ref int maxPlayers) { if (maxPlayers < LobbySize.MaxPlayers) { maxPlayers = LobbySize.MaxPlayers; } try { EnsurePlayersArray(__instance); } catch (Exception arg) { MelonLogger.Error($"Failed to resize players array: {arg}"); } } [HarmonyPostfix] [HarmonyPatch(typeof(SteamLobbyService), "OnLobbyCreated")] private static void Service_OnLobbyCreated_Postfix(LobbyCreated_t result) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_004b: 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) try { if ((int)result.m_eResult == 1) { CSteamID val = default(CSteamID); ((CSteamID)(ref val))..ctor(result.m_ulSteamIDLobby); string text = LobbySize.MaxPlayers.ToString(); SteamMatchmaking.SetLobbyMemberLimit(val, LobbySize.MaxPlayers); SteamMatchmaking.SetLobbyData(val, "max_players", text); SteamMatchmaking.SetLobbyData(val, "num_slots", text); Log.Info($"Lobby {result.m_ulSteamIDLobby} created with {LobbySize.MaxPlayers} slots"); } } catch (Exception arg) { MelonLogger.Error($"Failed to set lobby limits: {arg}"); } } [HarmonyPostfix] [HarmonyPatch(typeof(LobbyInterface), "Start")] private static void LobbyInterface_Start_Postfix(LobbyInterface __instance) { try { ExpandSlots(__instance); } catch (Exception arg) { MelonLogger.Error($"Failed to expand lobby UI: {arg}"); } } public static void RefreshLobbyScreen() { try { if (!((Object)(object)_ui == (Object)null) && !((Il2CppObjectBase)_ui).WasCollected) { ExpandSlots(_ui); _ui.UpdatePlayers(); _ui.UpdateButtons(); } } catch (Exception ex) { MelonLogger.Warning("Couldn't refresh the lobby screen: " + ex.Message); } } private static void ExpandSlots(LobbyInterface ui) { _ui = ui; GridLayoutGroup componentInChildren = ((Component)ui).GetComponentInChildren<GridLayoutGroup>(true); if ((Object)(object)componentInChildren == (Object)null) { MelonLogger.Error("Could not find GridLayoutGroup in LobbyInterface"); return; } Transform transform = ((Component)componentInChildren).transform; if (transform.childCount < 2) { MelonLogger.Warning("Lobby UI has no player slot to clone"); return; } int num = transform.childCount - 1; Transform child = transform.GetChild(1); for (int i = num; i < LobbySize.MaxPlayers; i++) { GameObject val = Object.Instantiate<GameObject>(((Component)child).gameObject, transform); ((Object)val).name = $"{((Object)((Component)child).gameObject).name} ({transform.childCount - 1})"; } LobbySize.RememberGrid(componentInChildren); int num2 = transform.childCount - 1; Il2CppReferenceArray<RectTransform> val2 = new Il2CppReferenceArray<RectTransform>((long)num2); for (int j = 1; j < transform.childCount; j++) { ((Il2CppArrayBase<RectTransform>)(object)val2)[j - 1] = ((Component)transform.GetChild(j)).GetComponent<RectTransform>(); } ui.PlayerSlots = val2; UpdateTitle(ui); try { ui.UpdateUI(); } catch { } } private static void UpdateTitle(LobbyInterface ui, int hidden = 0, int fake = 0) { if (!((Object)(object)ui.Lobby == (Object)null) && !((Object)(object)ui.LobbyTitle == (Object)null)) { string text = ((hidden > 0) ? $" +{hidden}" : ""); string text2 = ((fake > 0) ? " [test]" : ""); if (LobbySize.MaxPlayers > 20) { ((TMP_Text)ui.LobbyTitle).enableWordWrapping = false; } ((TMP_Text)ui.LobbyTitle).text = $"Lobby ({ui.Lobby.PlayerCount + fake}/{LobbySize.MaxPlayers}){text}{text2}"; } } private static int CountActive(Il2CppReferenceArray<RectTransform>? slots) { int num = 0; if (slots != null) { for (int i = 0; i < ((Il2CppArrayBase<RectTransform>)(object)slots).Length; i++) { if ((Object)(object)((Il2CppArrayBase<RectTransform>)(object)slots)[i] != (Object)null && ((Component)((Il2CppArrayBase<RectTransform>)(object)slots)[i]).gameObject.activeSelf) { num++; } } } return num; } [HarmonyPostfix] [HarmonyPatch(typeof(LobbyInterface), "UpdatePlayers")] private static void LobbyInterface_UpdatePlayers_Postfix(LobbyInterface __instance) { try { int hidden = 0; int num = 0; int num2 = 0; try { Il2CppReferenceArray<RectTransform> playerSlots = __instance.PlayerSlots; if (LobbySize.FakePlayers > 0) { int num3 = CountActive(playerSlots); num = LobbySize.ShowFakePlayers(playerSlots); num2 = num - (CountActive(playerSlots) - num3); } hidden = LobbySize.FitRow(playerSlots) + num2; } catch (Exception arg) { MelonLogger.Error($"Failed to fit lobby row: {arg}"); } UpdateTitle(__instance, hidden, num); } catch (Exception arg2) { MelonLogger.Error($"Failed to update lobby title: {arg2}"); } } [HarmonyPostfix] [HarmonyPatch(typeof(LobbyInterface), "UpdateButtons")] private static void LobbyInterface_UpdateButtons_Postfix(LobbyInterface __instance) { try { if (!((Object)(object)__instance.Lobby == (Object)null) && !((Object)(object)__instance.InviteButton == (Object)null)) { ((Component)__instance.InviteButton).gameObject.SetActive(__instance.Lobby.IsHost && __instance.Lobby.PlayerCount < LobbySize.MaxPlayers); } } catch (Exception arg) { MelonLogger.Error($"Failed to patch UpdateButtons: {arg}"); } } } [HarmonyPatch] public static class LobbyDedupePatches { private static float _pendingSince = -1f; private const float PendingTimeout = 15f; private static bool _openInviteWhenReady; private static SteamLobbyService? _service; private static bool CreatePending => _pendingSince >= 0f && Time.realtimeSinceStartup - _pendingSince < 15f; private static void Log(string s) { BiggerLobbies.Utils.Log.Info("[diag] " + s); } private static bool ServiceInLobby() { try { return _service != null && _service.IsInLobby; } catch { return false; } } [HarmonyPostfix] [HarmonyPatch(typeof(SteamLobbyService), "Initialize")] private static void Svc_Initialize_Postfix(SteamLobbyService __instance) { _service = __instance; } [HarmonyPrefix] [HarmonyPatch(typeof(SteamMatchmaking), "CreateLobby")] private static bool Steam_CreateLobby_Prefix(ELobbyType eLobbyType, int cMaxMembers, ref SteamAPICall_t __result) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) if (CreatePending || ServiceInLobby()) { Log($"Steam.CreateLobby BLOCKED duplicate (pending={CreatePending} inLobby={ServiceInLobby()})"); __result = SteamAPICall_t.Invalid; return false; } _pendingSince = Time.realtimeSinceStartup; Log($"Steam.CreateLobby type={eLobbyType} max={cMaxMembers}"); return true; } [HarmonyPostfix] [HarmonyPatch(typeof(SteamLobbyService), "OnLobbyCreated")] private static void Svc_OnLobbyCreated_Postfix() { _pendingSince = -1f; } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "OpenInviteUI")] private static bool Svc_OpenInviteUI_Prefix(SteamLobbyService __instance) { _service = __instance; bool isInLobby = __instance.IsInLobby; Log($"Svc.OpenInviteUI inLobby={isInLobby} pending={CreatePending}"); if (isInLobby) { return true; } _openInviteWhenReady = true; if (!CreatePending) { try { __instance.CreateLobby(LobbySize.MaxPlayers); } catch (Exception arg) { MelonLogger.Error($"CreateLobby from OpenInviteUI failed: {arg}"); } } return false; } [HarmonyPostfix] [HarmonyPatch(typeof(SteamLobbyService), "OnLobbyEntered")] private static void Svc_OnLobbyEntered_Postfix(SteamLobbyService __instance) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) _pendingSince = -1f; if (!_openInviteWhenReady) { return; } _openInviteWhenReady = false; try { Log($"Opening deferred invite overlay for lobby {__instance._lobbySteamID.m_SteamID}"); __instance.OpenInviteUI(); } catch (Exception arg) { MelonLogger.Error($"Deferred OpenInviteUI failed: {arg}"); } } [HarmonyPrefix] [HarmonyPatch(typeof(SteamMatchmaking), "JoinLobby")] private static void Steam_JoinLobby(CSteamID steamIDLobby) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) Log($"Steam.JoinLobby {steamIDLobby.m_SteamID}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamMatchmaking), "LeaveLobby")] private static void Steam_LeaveLobby(CSteamID steamIDLobby) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) Log($"Steam.LeaveLobby {steamIDLobby.m_SteamID}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamFriends), "ActivateGameOverlayInviteDialog")] private static void Steam_InviteDialog(CSteamID steamIDLobby) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) Log($"Steam.ActivateGameOverlayInviteDialog {steamIDLobby.m_SteamID}"); } } [HarmonyPatch] public static class DiagnosticPatches { private static string Id(Il2CppObjectBase? o) { return (o == null) ? "null" : o.Pointer.ToString("X"); } private static void Log(string s) { BiggerLobbies.Utils.Log.Info("[diag] " + s); } [HarmonyPrefix] [HarmonyPatch(typeof(Lobby), "Awake")] private static void Lobby_Awake(Lobby __instance) { Log("Lobby.Awake lobby=" + Id((Il2CppObjectBase?)(object)__instance)); } [HarmonyPrefix] [HarmonyPatch(typeof(Lobby), "Start")] private static void Lobby_Start(Lobby __instance) { Log("Lobby.Start lobby=" + Id((Il2CppObjectBase?)(object)__instance)); } [HarmonyPostfix] [HarmonyPatch(typeof(Lobby), "CreateLobbyService")] private static void Lobby_CreateLobbyService(Lobby __instance) { Log("Lobby.CreateLobbyService lobby=" + Id((Il2CppObjectBase?)(object)__instance) + " service=" + Id((Il2CppObjectBase?)(object)__instance._lobbyService)); } [HarmonyPrefix] [HarmonyPatch(typeof(Lobby), "CreateLobby")] private static void Lobby_CreateLobby(Lobby __instance) { Log($"Lobby.CreateLobby lobby={Id((Il2CppObjectBase?)(object)__instance)} inLobby={__instance.IsInLobby}"); } [HarmonyPrefix] [HarmonyPatch(typeof(Lobby), "TryOpenInviteInterface")] private static void Lobby_TryOpenInvite(Lobby __instance) { Log($"Lobby.TryOpenInviteInterface lobby={Id((Il2CppObjectBase?)(object)__instance)} inLobby={__instance.IsInLobby} count={__instance.PlayerCount}"); } [HarmonyPrefix] [HarmonyPatch(typeof(Lobby), "SendLobbyMessage")] private static void Lobby_SendLobbyMessage(string message) { Log("Lobby.SendLobbyMessage '" + message + "'"); } [HarmonyPrefix] [HarmonyPatch(typeof(Lobby), "SetLobbyData")] private static void Lobby_SetLobbyData(string key, string value) { Log("Lobby.SetLobbyData " + key + "=" + value); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "Initialize")] private static void Svc_Initialize(SteamLobbyService __instance) { Log("Svc.Initialize service=" + Id((Il2CppObjectBase?)(object)__instance)); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "CreateLobby")] private static void Svc_CreateLobby(SteamLobbyService __instance, int maxPlayers) { Log($"Svc.CreateLobby service={Id((Il2CppObjectBase?)(object)__instance)} maxPlayers={maxPlayers} inLobby={__instance.IsInLobby}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "OnLobbyCreated")] private static void Svc_OnLobbyCreated(SteamLobbyService __instance, LobbyCreated_t result) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) Log($"Svc.OnLobbyCreated service={Id((Il2CppObjectBase?)(object)__instance)} lobby={result.m_ulSteamIDLobby} result={result.m_eResult}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "OnLobbyEntered")] private static void Svc_OnLobbyEntered(SteamLobbyService __instance, LobbyEnter_t result) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) Log($"Svc.OnLobbyEntered service={Id((Il2CppObjectBase?)(object)__instance)} lobby={result.m_ulSteamIDLobby} response={result.m_EChatRoomEnterResponse}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "JoinLobby")] private static void Svc_JoinLobby(SteamLobbyService __instance) { Log("Svc.JoinLobby service=" + Id((Il2CppObjectBase?)(object)__instance)); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "LeaveLobby")] private static void Svc_LeaveLobby(SteamLobbyService __instance) { Log("Svc.LeaveLobby service=" + Id((Il2CppObjectBase?)(object)__instance)); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "SendMessage")] private static void Svc_SendMessage(SteamLobbyService __instance, string message) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) Log($"Svc.SendMessage service={Id((Il2CppObjectBase?)(object)__instance)} '{message}' lobby={__instance._lobbySteamID.m_SteamID}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "SetLobbyData")] private static void Svc_SetLobbyData(SteamLobbyService __instance, string key, string value) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) Log($"Svc.SetLobbyData service={Id((Il2CppObjectBase?)(object)__instance)} {key}={value} lobby={__instance._lobbySteamID.m_SteamID}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "UpdateLobbyMembers")] private static void Svc_UpdateMembers(SteamLobbyService __instance) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) Log($"Svc.UpdateLobbyMembers service={Id((Il2CppObjectBase?)(object)__instance)} lobby={__instance._lobbySteamID.m_SteamID}"); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "PlayerEnterOrLeave")] private static void Svc_PlayerEnterOrLeave(SteamLobbyService __instance) { Log("Svc.PlayerEnterOrLeave service=" + Id((Il2CppObjectBase?)(object)__instance)); } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "OnLobbyChatMessage")] private static void Svc_OnChat(SteamLobbyService __instance) { Log("Svc.OnLobbyChatMessage service=" + Id((Il2CppObjectBase?)(object)__instance)); } } public static class AutoSave { private static MelonPreferences_Category? _cat; private static MelonPreferences_Entry<bool>? _enabled; private static MelonPreferences_Entry<int>? _minutes; private static MelonPreferences_Entry<bool>? _playerSaveEnabled; private static MelonPreferences_Entry<int>? _playerSaveSeconds; private static float _nextCheck; private static float _nextPlayerSave; private static int _lastPlayerCount = -1; private static readonly HashSet<string> _remoteCodes = new HashSet<string>(); public static bool WorldEnabled { get { return _enabled?.Value ?? false; } set { if (_enabled != null) { _enabled.Value = value; } } } public static int WorldMinutes { get { return Math.Max(1, _minutes?.Value ?? 5); } set { if (_minutes != null) { _minutes.Value = Math.Clamp(value, 1, 60); } } } public static bool PlayersEnabled { get { return _playerSaveEnabled?.Value ?? false; } set { if (_playerSaveEnabled != null) { _playerSaveEnabled.Value = value; } } } public static int PlayerSeconds { get { return Math.Max(10, _playerSaveSeconds?.Value ?? 30); } set { if (_playerSaveSeconds != null) { _playerSaveSeconds.Value = Math.Clamp(value, 10, 300); } } } public static void Init() { _cat = MelonPreferences.CreateCategory("BiggerLobbies"); Log.Init(_cat); LobbySize.Init(_cat); DebugDummies.Init(_cat); LobbyPrivacy.Init(_cat); BanList.Init(_cat); Whitelist.Init(_cat); Crews.Init(_cat); ModHandshake.Init(_cat); DiscordPresence.Init(_cat); HostSettingsMenu.Init(_cat); _enabled = _cat.CreateEntry<bool>("AutoSaveEnabled", true, "Host autosave enabled", "As host, periodically run the game's normal save so a crash doesn't lose progress since the last sleep.", false, false, (ValueValidator)null, (string)null); _minutes = _cat.CreateEntry<int>("AutoSaveMinutes", 5, "Autosave interval (minutes)", (string)null, false, false, (ValueValidator)null, (string)null); _playerSaveEnabled = _cat.CreateEntry<bool>("PlayerSaveEnabled", true, "Client player saves enabled", "As host, save each connected client's money, inventory and position more often than the full autosave.", false, false, (ValueValidator)null, (string)null); _playerSaveSeconds = _cat.CreateEntry<int>("PlayerSaveSeconds", 30, "Client player save interval (seconds)", (string)null, false, false, (ValueValidator)null, (string)null); GrowSyncFix.Init(_cat); LateJoinFix.Init(_cat); AlwaysHostLobby.Init(_cat); } private static bool Hosting() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name == "Main" && InstanceFinder.IsServer; } public static float? SecondsSinceLastSave() { try { if (!Hosting()) { return null; } SaveManager instance = Singleton<SaveManager>.Instance; return ((Object)(object)instance == (Object)null) ? ((float?)null) : new float?(instance.SecondsSinceLastSave); } catch { return null; } } public static string SaveNow() { try { if (!Hosting()) { return "Load a save as host to save"; } SaveManager instance = Singleton<SaveManager>.Instance; if ((Object)(object)instance == (Object)null) { return "The game isn't ready to save"; } if (instance.IsSaving) { return "Already saving"; } SleepController instance2 = NetworkSingleton<SleepController>.Instance; if ((Object)(object)instance2 != (Object)null && instance2.IsSleepInProgress) { return "Can't save while everyone's sleeping"; } instance.Save(); return "Saved"; } catch (Exception arg) { MelonLogger.Error($"Save now failed: {arg}"); return "Save failed. See the log"; } } public static void Update() { //IL_0025: 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) float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup < _nextCheck) { return; } _nextCheck = realtimeSinceStartup + 5f; try { Scene activeScene = SceneManager.GetActiveScene(); if (((Scene)(ref activeScene)).name != "Main" || !InstanceFinder.IsServer) { return; } SaveManager instance = Singleton<SaveManager>.Instance; if ((Object)(object)instance == (Object)null || instance.IsSaving) { return; } SleepController instance2 = NetworkSingleton<SleepController>.Instance; if (!((Object)(object)instance2 != (Object)null) || !instance2.IsSleepInProgress) { if (ClientLeft()) { Log.Info("A player left; saving now"); instance.Save(); } else if (!TryFullSave(instance)) { TrySavePlayers(realtimeSinceStartup); } } } catch (Exception arg) { MelonLogger.Error($"Autosave failed: {arg}"); _nextCheck = realtimeSinceStartup + 60f; } } private static bool ClientLeft() { if (_enabled == null || !_enabled.Value) { return false; } List<Player> playerList = Player.PlayerList; if (playerList == null) { return false; } HashSet<string> hashSet = new HashSet<string>(); for (int i = 0; i < playerList.Count; i++) { Player val = playerList[i]; if (!((Object)(object)val == (Object)null) && !val.IsLocalPlayer && !string.IsNullOrEmpty(val.PlayerCode)) { hashSet.Add(val.PlayerCode); } } bool result = false; foreach (string remoteCode in _remoteCodes) { if (!hashSet.Contains(remoteCode)) { result = true; break; } } _remoteCodes.Clear(); _remoteCodes.UnionWith(hashSet); return result; } private static bool TryFullSave(SaveManager save) { if (_enabled == null || !_enabled.Value) { return false; } float num = (float)Math.Max(1, _minutes.Value) * 60f; if (save.SecondsSinceLastSave < num) { return false; } Log.Info($"Autosaving (last save {save.SecondsSinceLastSave / 60f:0.0} min ago)"); save.Save(); return true; } private static void TrySavePlayers(float now) { if (_playerSaveEnabled == null || !_playerSaveEnabled.Value || now < _nextPlayerSave) { return; } _nextPlayerSave = now + (float)Math.Max(10, _playerSaveSeconds.Value); PlayerManager instance = Singleton<PlayerManager>.Instance; List<Player> playerList = Player.PlayerList; if ((Object)(object)instance == (Object)null || playerList == null) { return; } int num = 0; for (int i = 0; i < playerList.Count; i++) { Player val = playerList[i]; if (!((Object)(object)val == (Object)null) && !val.IsLocalPlayer && !string.IsNullOrEmpty(val.PlayerCode)) { try { instance.SavePlayer(val); num++; } catch (Exception arg) { MelonLogger.Error($"Saving player {val.PlayerCode} failed: {arg}"); } } } if (num != _lastPlayerCount) { if (num > 0) { Log.Info($"Saving {num} connected player(s) every {Math.Max(10, _playerSaveSeconds.Value)} s"); } _lastPlayerCount = num; } } } public static class GrowSyncFix { [HarmonyPatch(typeof(WaterPotBehaviour), "OnActionSuccess")] private static class WaterPotPatch { private static void Postfix(WaterPotBehaviour __instance) { if (!Server) { return; } try { PushMoisture(((GrowContainerBehaviour)__instance)._growContainer); } catch (Exception arg) { MelonLogger.Error($"Plant sync (botanist water) failed: {arg}"); } } } [HarmonyPatch(typeof(GrowContainer), "OnTimeSkipped")] private static class TimeSkipPatch { private static void Postfix(GrowContainer __instance) { if (!Server) { return; } try { PushMoisture(__instance); } catch (Exception arg) { MelonLogger.Error($"Plant sync (time skip) failed: {arg}"); } } } [HarmonyPatch(typeof(Pot), "OnMinPass")] private static class MinPassPatch { private static readonly Dictionary<int, int> Ticks = new Dictionary<int, int>(); private static void Postfix(Pot __instance) { if (!Server) { return; } int num = _minutes?.Value ?? 30; if (num <= 0) { return; } try { int instanceID = ((Object)__instance).GetInstanceID(); Ticks.TryGetValue(instanceID, out var value); if (++value < num) { Ticks[instanceID] = value; return; } Ticks[instanceID] = (instanceID & 0x7FFFFFFF) % 7; PushMoisture((GrowContainer?)(object)__instance); Plant plant = __instance.Plant; if ((Object)(object)plant != (Object)null) { __instance.SetGrowthProgress_Client(plant.NormalizedGrowthProgress); } } catch (Exception arg) { MelonLogger.Error($"Plant sync (resync) failed: {arg}"); } } } private static MelonPreferences_Entry<bool>? _enabled; private static MelonPreferences_Entry<int>? _minutes; private static bool Server { get { try { return _enabled != null && _enabled.Value && InstanceFinder.IsServer; } catch { return false; } } } public static void Init(MelonPreferences_Category cat) { _enabled = cat.CreateEntry<bool>("PlantSyncEnabled", true, "Plant sync enabled", "As host, send pot water level and growth progress to clients so everyone sees the same values.", false, false, (ValueValidator)null, (string)null); _minutes = cat.CreateEntry<int>("PlantSyncMinutes", 30, "Plant resync interval (in-game minutes)", (string)null, false, false, (ValueValidator)null, (string)null); } private static void PushMoisture(GrowContainer? c) { if (!((Object)(object)c == (Object)null)) { c.SetMoistureData_Client((NetworkConnection)null, c._currentMoistureAmount); } } } public static class LateJoinFix { [HarmonyPatch(typeof(SteamLobbyService), "PlayerEnterOrLeave")] private static class PlayerEnterOrLeavePatch { private static void Postfix(SteamLobbyService __instance, LobbyChatUpdate_t result) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) try { if (_enabled != null && _enabled.Value && __instance.IsHost && (result.m_rgfChatMemberStateChange & 1) != 0) { LoadManager instance = Singleton<LoadManager>.Instance; if (!((Object)(object)instance == (Object)null) && instance.IsGameLoaded && !_pending) { _pending = true; MelonCoroutines.Start(SendReadySoon(__instance)); } } } catch (Exception arg) { MelonLogger.Error($"Late join check failed: {arg}"); } } } private static MelonPreferences_Entry<bool>? _enabled; private static bool _pending; private static float _lastSend = -999f; private const float FirstDelay = 2f; private const float MinSpacing = 5f; public static void Init(MelonPreferences_Category cat) { _enabled = cat.CreateEntry<bool>("LateJoinEnabled", true, "Late join enabled", "As host, let players join a game that has already started (re-sends the lobby 'ready' message when someone joins).", false, false, (ValueValidator)null, (string)null); } private static IEnumerator SendReadySoon(SteamLobbyService svc) { float now = Time.realtimeSinceStartup; float sendAt = Math.Max(now + 2f, _lastSend + 5f); while (Time.realtimeSinceStartup < sendAt) { yield return null; } _pending = false; try { if (svc.IsHost && svc.IsInLobby) { int members = SteamMatchmaking.GetNumLobbyMembers(svc._lobbySteamID); if (members <= 4) { Log.Info($"Player joined mid-game ({members} in lobby); the game connects them itself"); yield break; } _lastSend = Time.realtimeSinceStartup; svc.SetLobbyData("ready", "true"); svc.SendMessage("ready"); Log.Info("Player joined mid-game; re-sent lobby 'ready' so they can connect"); } } catch (Exception ex) { Exception e = ex; MelonLogger.Error($"Late join 'ready' re-send failed: {e}"); } } } [HarmonyPatch] public static class AlwaysHostLobby { [HarmonyPatch(typeof(LoadManager), "StartGame")] private static class StartGamePatch { private static bool Prefix(LoadManager __instance, SaveInfo info, bool allowLoadStacking, bool allowSaveBackup) { if (_resuming) { return true; } if (_waiting) { return false; } try { if (_enabled == null || !_enabled.Value) { return true; } Lobby instance = Singleton<Lobby>.Instance; if ((Object)(object)instance == (Object)null || instance.IsInLobby) { return true; } string text = WhyNotOnline(); if (text != null) { Log.Info("Always host lobby: " + text + "; loading solo"); return true; } Log.Info("Always host lobby: creating a Steam lobby before loading"); _waiting = true; _created = false; _failed = false; instance.CreateLobby(); MelonCoroutines.Start(WaitThenLoad(__instance, info, allowLoadStacking, allowSaveBackup)); return false; } catch (Exception arg) { MelonLogger.Error($"Always host lobby failed, loading solo: {arg}"); _waiting = false; return true; } } } private static MelonPreferences_Entry<bool>? _enabled; private const float TimeoutSeconds = 10f; private static bool _waiting; private static bool _resuming; private static bool _created; private static bool _failed; public static void Init(MelonPreferences_Category cat) { _enabled = cat.CreateEntry<bool>("AlwaysHostLobby", true, "Always host a lobby", "Create a Steam lobby automatically when loading a save, so friends can be invited or use Steam's Join Game at any time.", false, false, (ValueValidator)null, (string)null); } private static string? WhyNotOnline() { try { if (!SteamManager.Initialized) { return "Steam isn't initialised"; } if (!SteamAPI.IsSteamRunning()) { return "Steam isn't running"; } if (!SteamUser.BLoggedOn()) { return "not signed in to Steam (offline)"; } } catch (Exception ex) { return "Steam check failed (" + ex.Message + ")"; } return null; } [HarmonyPostfix] [HarmonyPatch(typeof(SteamLobbyService), "OnLobbyCreated")] private static void OnLobbyCreated_Postfix(LobbyCreated_t result) { //IL_000f: 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) //IL_0016: Invalid comparison between Unknown and I4 //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) if (_waiting) { if ((int)result.m_eResult == 1) { _created = true; return; } _failed = true; MelonLogger.Warning($"Always host lobby: Steam couldn't create the lobby ({result.m_eResult})"); } } private static IEnumerator WaitThenLoad(LoadManager load, SaveInfo info, bool allowLoadStacking, bool allowSaveBackup) { float deadline = Time.realtimeSinceStartup + 10f; while (true) { bool inLobby = false; try { Lobby lobby = Singleton<Lobby>.Instance; if ((Object)(object)lobby != (Object)null) { ILobbyService lobbyService = lobby._lobbyService; SteamLobbyService svc = ((lobbyService != null) ? ((Il2CppObjectBase)lobbyService).TryCast<SteamLobbyService>() : null); inLobby = lobby.IsInLobby || (svc != null && svc.IsInLobby); } } catch { } if (inLobby || _created) { Log.Info("Always host lobby: lobby ready; loading as host"); break; } if (_failed) { Log.Info("Always host lobby: loading solo"); break; } if (Time.realtimeSinceStartup > deadline) { MelonLogger.Warning($"Always host lobby: no answer from Steam after {10f:0} s; loading solo"); break; } yield return null; } _waiting = false; _resuming = true; try { load.StartGame(info, allowLoadStacking, allowSaveBackup); } catch (Exception ex) { Exception e = ex; MelonLogger.Error($"Always host lobby: StartGame failed: {e}"); } finally { _resuming = false; } } } [HarmonyPatch] public static class StaleAuthFix { [HarmonyPrefix] [HarmonyPatch(typeof(FishNetSteamAuthenticator), "OnSteamAuthBroadcast")] private static bool OnSteamAuthBroadcast_Prefix(FishNetSteamAuthenticator __instance, NetworkConnection conn, SteamSessionAuthTicket auth) { //IL_01a8: 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_0126: Unknown result type (might be due to invalid IL or missing references) //IL_012b: 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) //IL_0130: Invalid comparison between Unknown and I4 //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) try { if (conn == (NetworkConnection)null || auth == null) { return true; } if (BanList.RejectIfBanned(conn, auth.SteamId)) { return false; } if (Whitelist.RejectIfNotListed(conn, auth.SteamId)) { return false; } if (!conn.Authenticated && auth.SteamId != 0) { BanList.RecordLogin(conn.ClientId, auth.SteamId); KickNotice.Unhide(auth.SteamId); } if (conn.Authenticated) { ulong steamId = auth.SteamId; if (steamId == 0) { return true; } ulong num = BanList.VerifiedSteamId(conn.ClientId); if (num != 0L && num != steamId) { MelonLogger.Warning($"Connection {conn.ClientId} logged in again as a different Steam ID ({steamId}, was {num}); disconnecting"); try { conn.Disconnect(true); } catch { } return false; } PlatformAuth.EndAuthSession(new CSteamID(steamId)); EBeginAuthSessionResult val = PlatformAuth.BeginAuthSession(auth); if ((int)val > 0) { MelonLogger.Warning($"Connection {conn.ClientId} re-authentication failed ({val}); disconnecting"); return true; } __instance.SendAuthenticationResponse(conn, true); Log.Info($"Connection {conn.ClientId} sent authentication again; re-authenticated with Steam ({val})"); return false; } ulong steamId2 = auth.SteamId; if (steamId2 == 0) { return true; } PlatformAuth.EndAuthSession(new CSteamID(steamId2)); Log.Info($"Cleared any old Steam auth session for {steamId2} before authenticating connection {conn.ClientId}"); } catch (Exception arg) { MelonLogger.Error($"Steam auth fix failed: {arg}"); } return true; } [HarmonyPostfix] [HarmonyPatch(typeof(ServerManager), "Transport_OnRemoteConnectionState")] private static void Transport_OnRemoteConnectionState_Postfix(RemoteConnectionStateArgs args) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 //IL_000f: Unknown result type (might be due to invalid IL or missing references) try { if ((int)args.ConnectionState == 0) { BanList.ForgetLogin(args.ConnectionId); } } catch (Exception ex) { MelonLogger.Warning("Couldn't clear a closed connection's login: " + ex.Message); } } } [HarmonyPatch] public static class LobbySize { public const int Min = 4; public const int Max = 250; public const int Tested = 20; private static MelonPreferences_Entry<int>? _entry; private static MelonPreferences_Entry<int>? _fake; private static int _lastLogged = -1; public const int PerRow = 20; public const int MaxRows = 5; private static GridLayoutGroup? _grid; private static float _cellX = 75f; private static float _origSpacingX = -150f; private static float _spacingY; private static bool _loggedLayout; public static int FakePlayers => Math.Clamp(_fake?.Value ?? 0, 0, 250); public static int MaxPlayers { get; private set; } = 20; public static int LiveMin => Math.Max(4, HostLobby.MemberCount()); public static int MaxShown => 100; public static GridLayoutGroup? Grid => _grid; public static void Init(MelonPreferences_Category cat) { _entry = cat.CreateEntry<int>("MaxPlayers", 20, "Max players", $"Lobby size, {4} to {250}. Default {20}. If you edit this file, restart the game. The host panel changes it without a restart.", false, false, (ValueValidator)null, (string)null); int value = _entry.Value; int num = Math.Clamp(value, 4, 250); if (num != value) { MelonLogger.Warning($"MaxPlayers {value} is outside {4}-{250}; using {num}"); } MaxPlayers = num; _fake = cat.CreateEntry<int>("DebugFakePlayers", 0, "Debug: fake lobby players", "Testing only. Shows this many placeholder players on the lobby screen to check the layout. They aren't real, aren't sent over the network and can't be invited. Leave at 0.", false, false, (ValueValidator)null, (string)null); if (FakePlayers > 0) { MelonLogger.Warning($"DebugFakePlayers is {FakePlayers}: the lobby screen shows placeholder players for testing. Set it back to 0 to play normally."); } Log.Info($"Lobby size: {MaxPlayers} players"); } public static void EnsureTransportCapacity(string when) { try { TransportManager transportManager = InstanceFinder.TransportManager; Transport val = ((transportManager != null) ? transportManager.Transport : null); if ((Object)(object)val == (Object)null) { return; } Multipass val2 = ((Il2CppObjectBase)val).TryCast<Multipass>(); if (((val2 != null) ? val2._transports : null) != null) { List<Transport> transports = val2._transports; for (int i = 0; i < transports.Count; i++) { RaiseSteamLimit(transports[i], when); } } else { RaiseSteamLimit(val, when); } } catch (Exception ex) { MelonLogger.Warning("Couldn't check the network transport's player limit: " + ex.Message); } } private static void RaiseSteamLimit(Transport? t, string when) { FishySteamworks val = ((t != null) ? ((Il2CppObjectBase)t).TryCast<FishySteamworks>() : null); if ((Object)(object)val == (Object)null) { return; } int maximumClients = val._maximumClients; if (maximumClients < MaxPlayers) { val._maximumClients = (ushort)MaxPlayers; try { if (val._server != null) { ((Transport)val).SetMaximumClients(MaxPlayers); } } catch { } Log.Info($"Steam transport player limit raised from {maximumClients} to {MaxPlayers} ({when})"); _lastLogged = MaxPlayers; } else if (maximumClients != _lastLogged) { Log.Info($"Steam transport player limit is {maximumClients}; lobby size {MaxPlayers} fits ({when})"); _lastLogged = maximumClients; } } public static string SetLive(int requested) { //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) int num = Math.Clamp(requested, LiveMin, 250); int maxPlayers = MaxPlayers; if (num == maxPlayers) { return $"Lobby size is {num}"; } MaxPlayers = num; if (_entry != null) { _entry.Value = num; MelonPreferences.Save(); } string text = "not hosting a Steam lobby yet"; if (HostLobby.TryGet(out var lobby)) { bool flag = false; int num2 = -1; try { flag = SteamMatchmaking.SetLobbyMemberLimit(lobby, num); SteamMatchmaking.SetLobbyData(lobby, "max_players", num.ToString()); SteamMatchmaking.SetLobbyData(lobby, "num_slots", num.ToString()); num2 = SteamMatchmaking.GetLobbyMemberLimit(lobby); } catch (Exception ex) { MelonLogger.Warning("SetLobbyMemberLimit failed: " + ex.Message); } text = ((flag && num2 == num) ? $"Steam lobby limit is now {num2}" : string.Format("Steam {0} the change and reports {1}", flag ? "accepted" : "refused", num2)); } string text2 = SetSteamLimit(num); HarmonyPatches.RefreshLobbyScreen(); string s = $"Lobby size changed from {maxPlayers} to {num} while running: {text}; {text2}"; Log.Info(s); return $"Lobby size {num}. {text}"; } private static string SetSteamLimit(int n) { try { TransportManager transportManager = InstanceFinder.TransportManager; Transport val = ((transportManager != null) ? transportManager.Transport : null); if ((Object)(object)val == (Object)null) { return "no network transport yet"; } Multipass val2 = ((Il2CppObjectBase)val).TryCast<Multipass>(); FishySteamworks val3 = null; if (((val2 != null) ? val2._transports : null) != null) { for (int i = 0; i < val2._transports.Count; i++) { if (!((Object)(object)val3 == (Object)null)) { break; } Transport obj = val2._transports[i]; val3 = ((obj != null) ? ((Il2CppObjectBase)obj).TryCast<FishySteamworks>() : null); } } else { val3 = ((Il2CppObjectBase)val).TryCast<FishySteamworks>(); } if ((Object)(object)val3 == (Object)null) { return "no Steam transport found"; } val3._maximumClients = (ushort)n; _lastLogged = n; if (val3._server == null) { return $"transport limit {n} (server not started)"; } val3._server.SetMaximumClients(n); return $"transport limit now {val3._server.GetMaximumClients()}"; } catch (Exception ex) { return "transport limit not changed (" + ex.Message + ")"; } } [HarmonyPrefix] [HarmonyPatch(typeof(SteamLobbyService), "CreateLobby")] private static void CreateLobby_Prefix() { EnsureTransportCapacity("creating lobby"); } [HarmonyPrefix] [HarmonyPatch(typeof(LoadManager), "StartGame")] private static void StartGame_Prefix() { EnsureTransportCapacity("loading save"); } public static void RememberGrid(GridLayoutGroup grid) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) _grid = grid; _cellX = grid.cellSize.x; _origSpacingX = grid.spacing.x; _spacingY = grid.spacing.y; if (_loggedLayout) { return; } _loggedLayout = true; try { RectTransform component = ((Component)grid).GetComponent<RectTransform>(); Log.Info($"[diag] lobby row: constraint={grid.constraint} count={grid.constraintCount} cell={grid.cellSize} spacing={grid.spacing} " + $"start={grid.startCorner}/{grid.startAxis} align={((LayoutGroup)grid).childAlignment} anchors={component.anchorMin}-{component.anchorMax} pos={component.anchoredPosition}"); } catch { } } public static int ShowFakePlayers(Il2CppReferenceArray<RectTransform>? slots) { int fakePlayers = FakePlayers; if (fakePlayers <= 0 || slots == null) { return 0; } int num = 0; for (int i = 0; i < ((Il2CppArrayBase<RectTransform>)(object)slots).Length; i++) { if (num >= fakePlayers) { break; } RectTransform val = ((Il2CppArrayBase<RectTransform>)(object)slots)[i]; if (!((Object)(object)val == (Object)null) && !((Component)val).gameObject.activeSelf) { ((Component)val).gameObject.SetActive(true); num++; } } return fakePlayers; } private static void MoveInviteOutOfTable() { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)_grid == (Object)null || ((Component)_grid).transform.childCount == 0) { return; } RectTransform component = ((Component)_grid).GetComponent<RectTransform>(); LayoutRebuilder.ForceRebuildLayoutImmediate(component); RectTransform component2 = ((Component)((Component)_grid).transform.GetChild(0)).GetComponent<RectTransform>(); if (!((Object)(object)component2 == (Object)null)) { Vector2 anchoredPosition = component2.anchoredPosition; LayoutElement val = ((Component)component2).GetComponent<LayoutElement>(); if ((Object)(object)val == (Object)null) { val = ((Component)component2).gameObject.AddComponent<LayoutElement>(); } val.ignoreLayout = true; float num = 0f - (_cellX + _origSpacingX); component2.anchoredPosition = new Vector2(anchoredPosition.x - num, anchoredPosition.y); LayoutRebuilder.ForceRebuildLayoutImmediate(component); } } catch (Exception ex) { MelonLogger.Warning("Couldn't move the invite button out of the table: " + ex.Message); } } public static int FitRow(Il2CppReferenceArray<RectTransform>? slots) { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Invalid comparison between Unknown and I4 //IL_00ee: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_grid == (Object)null || slots == null) { return 0; } int num = 0; for (int i = 0; i < ((Il2CppArrayBase<RectTransform>)(object)slots).Length; i++) { RectTransform val = ((Il2CppArrayBase<RectTransform>)(object)slots)[i]; if (!((Object)(object)val == (Object)null) && ((Component)val).gameObject.activeSelf) { num++; if (num > MaxShown) { ((Component)val).gameObject.SetActive(false); } } } if (MaxPlayers <= 20) { return 0; } if ((int)_grid.constraint != 1 || _grid.constraintCount != 20) { _grid.constraint = (Constraint)1; _grid.constraintCount = 20; _grid.spacing = new Vector2(_origSpacingX, _spacingY); MoveInviteOutOfTable(); } return Math.Max(0, num - MaxShown); } } public static class DebugDummies { private static MelonPreferences_Entry<int>? _count; private static readonly List<GameObject> _dummies = new List<GameObject>(); private static readonly List<float> _phase = new List<float>(); private static Vector3 _centre; private static float _spawnAfter = -1f; private static float _fpsWindowStart; private static float _minFps = float.MaxValue; private static int _frames; public static int Count => Math.Clamp(_count?.Value ?? 0, 0, 250); public static void Init(MelonPreferences_Category cat) { _count = cat.CreateEntry<int>("DebugDummyPlayers", 0, "Debug: dummy characters", "Testing only. After a save loads, spawns this many copies of your character around you and logs the frame rate every 10 s. They aren't real players, aren't networked and aren't saved. Leave at 0.", false, false, (ValueValidator)null, (string)null); if (Count > 0) { MelonLogger.Warning($"DebugDummyPlayers is {Count}: {Count} dummy characters will spawn around you after a save loads. Set it back to 0 to play normally."); } } public static void OnSceneLoaded(string scene) { Despawn(); _spawnAfter = ((scene == "Main" && Count > 0) ? (Time.realtimeSinceStartup + 10f) : (-1f)); } public static void Update() { if (Count <= 0) { return; } try { if (_spawnAfter > 0f && Time.realtimeSinceStartup >= _spawnAfter) { _spawnAfter = -1f; Spawn(); ResetFps(); } Walk(); TrackFps(); } catch (Exception arg) { MelonLogger.Error($"[dummies] {arg}"); _spawnAfter = -1f; } } private static void Spawn() { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) Despawn(); Player local = Player.Local; object obj; if (local == null) { obj = null; } else { Avatar avatar = local.Avatar; obj = ((avatar != null) ? ((Component)avatar).gameObject : null); } GameObject val = (GameObject)obj; if ((Object)(object)local == (Object)null || (Object)(object)val == (Object)null) { MelonLogger.Warning("[dummies] no local player yet; not spawning"); return; } _centre = ((Component)local).transform.position; int count = Count; for (int i = 0; i < count; i++) { GameObject val2 = Object.Instantiate<GameObject>(val); ((Object)val2).name = $"BiggerLobbies_Dummy_{i + 1}"; MakeVisible(val2); _dummies.Add(val2); _phase.Add(i); } Place(0f); Log.Info($"[dummies] spawned {_dummies.Count} dummy characters around you"); } private static void MakeVisible(GameObject go) { foreach (Transform componentsInChild in go.GetComponentsInChildren<Transform>(true)) { ((Component)componentsInChild).gameObject.layer = 0; } foreach (Renderer componentsInChild2 in go.GetComponentsInChildren<Renderer>(true)) { componentsInChild2.enabled = true; componentsInChild2.shadowCastingMode = (ShadowCastingMode)1; } go.SetActive(true); } private static void Place(float t) { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //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_00b0: 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_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: 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_00e4: Unknown result type (might be due to invalid IL or missing references) int num = 0; int num2 = 0; while (num < _dummies.Count) { int num3 = 8 + num2 * 6; float num4 = 3f + (float)num2 * 1.6f; float num5 = ((num2 % 2 == 0) ? 1f : (-1f)); int num6 = 0; while (num6 < num3 && num < _dummies.Count) { GameObject val = _dummies[num]; if (!((Object)(object)val == (Object)null)) { float num7 = (float)num6 / (float)num3 * MathF.PI * 2f + num5 * t * 0.25f / (1f + (float)num2 * 0.3f); Vector3 val2 = _centre + new Vector3(Mathf.Cos(num7) * num4, 0f, Mathf.Sin(num7) * num4); Vector3 val3 = new Vector3(0f - Mathf.Sin(num7), 0f, Mathf.Cos(num7)) * num5; val.transform.SetPositionAndRotation(val2, Quaternion.LookRotation(val3, Vector3.up)); } num6++; num++; } num2++; } } private static void Walk() { if (_dummies.Count != 0) { Place(Time.time); } } public static void Despawn() { foreach (GameObject dummy in _dummies) { if ((Object)(object)dummy != (Object)null) { Object.Destroy((Object)(object)dummy); } } if (_dummies.Count > 0) { Log.Info($"[dummies] removed {_dummies.Count} dummy characters"); } _dummies.Clear(); _phase.Clear(); } private static void ResetFps() { _fpsWindowStart = Time.realtimeSinceStartup; _frames = 0; _minFps = float.MaxValue; } private static void TrackFps() { //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) Scene activeScene = SceneManager.GetActiveScene(); if (!(((Scene)(ref activeScene)).name != "Main")) { if (_fpsWindowStart <= 0f) { ResetFps(); } _frames++; float unscaledDeltaTime = Time.unscaledDeltaTime; if (unscaledDeltaTime > 0f) { _minFps = Math.Min(_minFps, 1f / unscaledDeltaTime); } float num = Time.realtimeSinceStartup - _fpsWindowStart; if (!(num < 10f)) { Log.Info($"[dummies] {_dummies.Count} dummies: {(float)_frames / num:0} fps average, {_minFps:0} lowest (last {num:0} s)"); ResetFps(); } } } } public static class HostLobby { public static SteamLobbyService? Service() { try { Lobby instance = Singleton<Lobby>.Instance; object result; if (instance == null) { result = null; } else { ILobbyService lobbyService = instance._lobbyService; result = ((lobbyService != null) ? ((Il2CppObjectBase)lobbyService).TryCast<SteamLobbyService>() : null); } return (SteamLobbyService?)result; } catch { return null; } } public static bool TryGet(out CSteamID lobby) { //IL_0002: 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_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: 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) lobby = default(CSteamID); try { SteamLobbyService val = Service(); if (val == null || !val.IsInLobby) { return false; } lobby = val._lobbySteamID; if (lobby.m_SteamID == 0) { return false; } return SteamMatchmaking.GetLobbyOwner(lobby).m_SteamID == SteamUser.GetSteamID().m_SteamID; } catch (Exception ex) { MelonLogger.Warning("Couldn't read the Steam lobby: " + ex.Message); return false; } } public static int MemberCount() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) try { CSteamID lobby; return TryGet(out lobby) ? SteamMatchmaking.GetNumLobbyMembers(lobby) : 0; } catch { return 0; } } } [HarmonyPatch] public static class LobbyPrivacy { public enum Mode { Private, FriendsOnly, Open } private static MelonPreferences_Entry<string>? _entry; public static Mode Current { get; private set; } = Mode.FriendsOnly; public static void Init(MelonPreferences_Category cat) { _entry = cat.CreateEntry<string>("LobbyPrivacy", "FriendsOnly", "Lobby privacy", "Who can join your lobby: Private (only people you invite), FriendsOnly (Steam friends can use Join Game; the game's default) or Open (anyone who finds it in Steam's lobby search). Can also be changed in Host settings.", false, false, (ValueValidator)null, (string)null); string text = (_entry.Value ?? "").Trim(); if (text.Equals("InviteOnly", StringComparison.OrdinalIgnoreCase)) { text = "Private"; } if (text.Equals("Public", StringComparison.OrdinalIgnoreCase)) { text = "Open"; } if (Enum.TryParse<Mode>(text, ignoreCase: true, out var result) && Enum.IsDefined(typeof(Mode), result)) { Current = result; } else { MelonLogger.Warning("LobbyPrivacy '" + _entry.Value + "' isn't Private, FriendsOnly or Open; using FriendsOnly"); } if (Current != Mode.FriendsOnly) { Log.Info("Lobby privacy: " + Label(Current)); } } public static string Label(Mode m) { if (1 == 0) { } string result = m switch { Mode.Private => "Private", Mode.Open => "Open", _ => "Friends only", }; if (1 == 0) { } return result; } private static ELobbyType SteamType(Mode m) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) if (1 == 0) { } ELobbyType result = (ELobbyType)(m switch { Mode.Private => 0, Mode.Open => 2, _ => 1, }); if (1 == 0) { } return result; } [HarmonyPrefix] [HarmonyPriority(800)] [HarmonyPatch(typeof(SteamMatchmaking), "CreateLobby")] private static void CreateLobby_Prefix(ref ELobbyType eLobbyType) { //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_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between I4 and Unknown //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected I4, but got Unknown ELobbyType val = SteamType(Current); if ((int)eLobbyType != (int)val) { Log.Info($"Lobby privacy: {Label(Current)} (the game asked for {eLobbyType})"); eLobbyType = (ELobbyType)(int)val; } } [HarmonyPrefix] [HarmonyPatch(typeof(PlatformFriends), "IsLocalPlayerFriendsWith")] private static bool IsLocalPlayerFriendsWith_Prefix(ref bool __result) { if (Current != Mode.Open) { return true; } __result = true; return false; } public static string Set(Mode m) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) Current = m; if (_entry != null) { _entry.Value = m.ToString(); MelonPreferences.Save(); } if (!HostLobby.TryGet(out var lobby)) { return "Privacy set to " + Label(m) + ". It applies when you next host."; } bool flag; try { flag = SteamMatchmaking.SetLobbyType(lobby, SteamType(m)); } catch (Exception ex) { MelonLogger.Warning("SetLobbyType failed: " + ex.Message); flag = false; } Log.Info("Lobby privacy changed to " + Label(m) + " on the open lobby: Steam " + (flag ? "accepted" : "refused") + " it"); return flag ? ("Privacy is now " + Label(m)) : "Steam refused the change. It applies when you next host."; } } public static class BanList { public readonly struct Entry { public readonly ulong SteamId; public readonly string Name; public readonly DateTime? Until; public readonly string Reason; public bool Permanent => !Until.HasValue; public Entry(ulong steamId, string name, DateTime? until = null, string reason = "") { SteamId = steamId; Name = name; Until = until; Reason = reason ?? ""; } } private static MelonPreferences_Entry<string>? _entry; private static readonly List<Entry> _bans = new List<Entry>(); private static readonly Dictionary<int, ulong> _verified = new Dictionary<int, ulong>(); private static readonly Dictionary<ulong, float> _lastRefusedLog = new Dictionary<ulong, float>(); public static IReadOnlyList<Entry> Bans { get { Prune(); return _bans; } } public static void Init(MelonPreferences_Category cat) { _entry = cat.CreateEntry<string>("BannedPlayers", "", "Banned players", "Steam IDs that can't join your games, separated by commas. A name in round brackets and a reason in square brackets are just reminders; 'until' and a UTC time make the ban end then. Easiest to manage in Host settings.", false, false, (ValueValidator)null, (string)null); Parse(_entry.Value); MelonPreferences_Entry<string> val = cat.CreateEntry<string>("Blacklist", "", "Blacklist (old)", "Moved to BannedPlayers.", true, false, (ValueValidator)null, (string)null); if (!string.IsNullOrWhiteSpace(val.Value)) { int count = _bans.Count; foreach (Entry e in ParseList(val.Value)) { if (_bans.All((Entry b) => b.SteamId != e.SteamId)) { _bans.Add(e); } } val.Value = ""; Store(); Log.Info($"Moved {_bans.Count - count} ban(s) from the old Blacklist setting to BannedPlayers"); } Prune(); if (_bans.Count > 0) { Log.Info($"Banned players: {_bans.Count}"); } } private static void Parse(string? raw) { _bans.Clear(); _bans.AddRange(ParseList(raw)); } private static List<Entry> ParseList(string? raw) { List<Entry> list = new List<Entry>(); if (string.IsNullOrWhiteSpace(raw)) { return list; } string[] array = raw.Split(','); foreach (string text in array) { string text2 = text.Trim(); int j; for (j = 0; j < text2.Length && char.IsDigit(text2[j]); j++) { } if (j == 0 || !ulong.TryParse(text2.Substring(0, j), out var id) || id == 0) { if (text2.Length > 0) { MelonLogger.Warning("Banned players: '" + text2 + "' doesn't start with a Steam ID; skipped"); } continue; } string text3 = text2.Substring(j).Trim(); DateTime? until = null; string text4 = null; int num = text3.LastIndexOf("until ", StringComparison.OrdinalIgnoreCase); if (num >= 0 && (num == 0 || text3[num - 1] == ' ' || text3[num - 1] == ')')) { text4 = text3.Substring(num + 6); text3 = text3.Substring(0, num); } if (text4 != null) { if (DateTime.TryParse(text4.Trim(), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { until = DateTime.SpecifyKind(result, DateTimeKind.Utc); } else { MelonLogger.Warning($"Banned players: couldn't read the end time '{text4.Trim()}' for {id}; treating the ban as permanent"); } } string reason = ""; text3 = text3.Trim(); int num2 = (text3.StartsWith("(") ? text3.IndexOf(')') : (-1)); int num3 = text3.IndexOf('[', num2 + 1); if (num3 >= 0 && text3.EndsWith("]")) { reason = text3.Substring(num3 + 1, text3.Length - num3 - 2).Trim(); text3 = text3.Substring(0, num3); } string name = text3.Trim().Trim('(', ')').Trim(); if (list.All((Entry b) => b.SteamId != id)) { list.Add(new Entry(id, name, until, reason)); } } return list; } private static void Store() { if (_entry == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (Entry ban in _bans) { if (stringBuilder.Length > 0) { stringBuilder.Append(", "); } stringBuilder.Append(ban.SteamId); string text = Clean(ban.Name); if (text.Length > 0) { stringBuilder.Append(" (").Append(text).Append(')'); } string text2 = CleanReason(ban.Reason); if (text2.Length > 0) { stringBuilder.Append(" [").Append(text2).Append(']'); } if (ban.Until.HasValue) { stringBuilder.Append(" until ").Append(ban.Until.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)); } } _entry.Value = stringBuilder.ToString(); MelonPreferences.Save(); } private static void Prune() { DateTime now = DateTime.UtcNow; List<Entry> list = _bans.Where((Entry b) => b.Until.HasValue && b.Until.Value <= now).ToList(); if (list.Count == 0) { return; } _bans.RemoveAll((Entry b) => b.Until.HasValue && b.Until.Value <= now); foreach (Entry item in list) { Log.Info(string.Format("Ban ended for {0}({1})", (item.Name.Length > 0) ? (item.Name + " ") : "", item.SteamId)); } Store(); } private static string Clean(string? n) { return new string((n ?? "").Where((char c) => c != ',' && c != '(' && c != ')' && !char.IsControl(c)).ToArray()).Replace(" until ", " ").Trim(); } public static string CleanReason(string? r) { string input = new string((r ?? "").Where((char c) => c != ',' && c != '[' && c != ']' && !char.IsControl(c)).ToArray()); input = Regex.Replace(input, "until ", "till ", RegexOptions.IgnoreCase).Trim(); return (input.Length > 40) ? input.Substring(0, 40).Trim() : input; } public static bool IsBanned(ulong id) { if (id == 0) { return false; } Prune(); return _bans.Any((Entry b) => b.SteamId == id); } public static string Remaining(Entry e) { if (!e.Until.HasValue) { return "Permanent"; } TimeSpan timeSpan = e.Until.Value - DateTime.UtcNow; if (timeSpan.TotalMinutes < 1.0) { return "Ending"; } if (timeSpan.TotalMinutes < 60.0) { return $"{Math.Ceiling(timeSpan.TotalMinutes)} min left"; } if (timeSpan.TotalHours < 48.0) { return $"{Math.Ceiling(timeSpan.TotalHours)} h left"; } return $"{Math.Ceiling(timeSpan.TotalDays)} days left"; } public static void RecordLogin(int clientId, ulong steamId) { _verified[clientId] = steamId; } public static ulong VerifiedSteamId(int clientId) { ulong value; return _verified.TryGetValue(clientId, out value) ? value : 0; } public static void ForgetLogin(int clientId) { _verified.Remove(clientId); } public static bool IsConnected(ulong steamId) { if (steamId == 0) { return false; } Dictionary<int, NetworkConnection> val = null; try { ServerManager serverManager = InstanceFinder.ServerManager; val = ((serverManager != null) ? serverManager.Clients : null); } catch { } NetworkConnection val2 = default(NetworkConnection); foreach (KeyValuePair<int, ulong> item in _verified) { if (item.Value != steamId) { continue; } if (val == null) { return true; } try { if (val.TryGetValue(item.Key, ref val2) && val2 != (NetworkConnection)null && val2.IsActive) { return true; } } catch { return true; } } return false; } public static ulong HostSteamId() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) try { return SteamUser.GetSteamID().m_SteamID; } catch { return 0uL; } } public static void Ban(ulong id, string name, TimeSpan? length = null, string reason = "") { if (id == 0) { return; } if (id == HostSteamId()) { MelonLogger.Warning("Refused to ban your own Steam ID"); return; } DateTime? until = (length.HasValue ? new DateTime?(DateTime.UtcNow + length.Value) : ((DateTime?)null)); _bans.RemoveAll((Entry b) => b.SteamId == id); Whitelist.Remove(id); reason = CleanReason(reason); _bans.Add(new Entry(id, Clean(name), until, reason)); Store(); Log.Info(string.Format("Banned {0} ({1}) {2}{3}", Clean(name), id, until.HasValue ? ("until " + until.Value.ToString("u", CultureInfo.InvariantCulture)) : "permanently", (reason.Length > 0) ? (": " + reason) : "")); HostPlayers.DisconnectSteamId(id, length, reason); } public static void Unban(ulong id) { if (_bans.RemoveAll((Entry b) => b.SteamId == id) != 0) { Store(); Log.Info($"Unbanned {id}"); } } public static bool RejectIfBanned(NetworkConnection conn, ulong steamId) { if (!IsBanned(steamId)) { return false; } Entry e = _bans.First((Entry b) => b.SteamId == steamId); float realtimeSinceStartup = Time.realtimeSinceStartup; if (!_lastRefusedLog.TryGetValue(steamId, out var value) || realtimeSinceStartup - value > 60f) { _lastRefusedLog[steamId] = realtimeSinceStartup; Log.Info(string.Format("Refused {0}({1}): banned, {2}", (e.Name.Length > 0) ? (e.Name + " ") : "", steamId, Remaining(e).ToLowerInvariant())); } TimeSpan? length = (e.Until.HasValue ? new TimeSpan?(e.Until.Value - DateTime.UtcNow) : ((TimeSpan?)null)); KickNotice.Remove(conn, steamId, KickNotice.Kind.Ban, length, e.Reason, conn.Authenticated ? 1.5f : 0.5f); return true; } } public static class Whitelist { private static MelonPreferences_Entry<bool>? _enabled; private static readonly List<BanList.Entry> _allowed = new List<BanList.Entry>(); private static readonly Dictionary<ulong, float> _lastRefusedLog = new Dictionary<ulong, float>(); public static bool Enabled => _enabled?.Value ?? false; public static IReadOnlyList<BanList.Entry> Players => _allowed.OrderByDescending((BanList.Entry e) => (int)Friends.StateOf(e.SteamId)).ThenBy<BanList.Entry, string>((BanList.Entry e) => e.Name, StringComparer.OrdinalIgnoreCase).ToList(); public static void Init(MelonPreferences_Category cat) { _enabled = cat.CreateEntry<bool>("WhitelistEnabled", false, "Whitelist", "When true, only players you add to the whitelist in Host settings can join. The list starts empty every time you start the game.", false, false, (ValueValidator)null, (string)null); if (Enabled) { Log.Info("Whitelist is on: only players you add in Host settings can join, and the list starts empty"); } } public static void SetEnabled(bool on) { if (_enabled != null && _enabled.Value != on) { _enabled.Value = on; MelonPreferences.Save(); Log.Info(on ? "Whitelist turned on" : "Whitelist turned off"); } } public static void WarnIfEmpty() { if (Enabled && _allowed.Count == 0) { MelonLogger.Warning("Whitelist is on and empty: nobody else can join until you add players in Host settings (pause menu or F6)"); } } public static bool Contains(ulong id) { return _allowed.Any((BanList.Entry e) => e.SteamId == id); } public static bool Add(ulong id, string name) { if (id == 0L || BanList.IsBanned(id)) { return false; } if (Contains(id)) { return true; } _allowed.Add(new BanList.Entry(id, name ?? "")); Log.Info($"Whitelisted {name} ({id}) for this session"); return true; } public static int Replace(IEnumerable<BanList.Entry> members) { _allowed.Clear(); int num = 0; foreach (BanList.Entry member in members) { if (member.SteamId != 0L && !BanList.IsBanned(member.SteamId) && !Contains(member.SteamId)) { _allowed.Add(new BanList.Entry(member.SteamId, member.Name ?? "")); num++; } } return num; } public static void Remove(ulong id) { if (_allowed.RemoveAll((BanList.Entry e) => e.SteamId == id) > 0) { Log.Info($"Removed {id} from the whitelist"); } } public static bool RejectIfNotListed(NetworkConnection conn, ulong steamId) { if (!Enabled || steamId == 0L || steamId == BanList.HostSteamId() || Contains(steamId)) { return false; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (!_lastRefusedLog.TryGetValue(steamId, out var value) || realtimeSinceStartup - value > 60f) { _lastRefusedLog[steamId] = realtimeSinceStartup; Log.Info($"Refused {Friends.NameOf(steamId)} ({steamId}): not on the whitelist"); } KickNotice.Remove(conn, steamId, KickNotice.Kind.Whitelist, null, "", conn.Authenticated ? 1.5f : 0.5f); return true; } } public static class Crews { public sealed class Crew { public string Name = ""; public List<BanList.Entry> Members = new List<BanList.Entry>(); } private static MelonPreferences_Entry<string>? _entry; private static readonly List<Crew> _crews = new List<Crew>(); public const int MaxCrews = 12; public static IReadOnlyList<Crew> All => _crews; public static void Init(MelonPreferences_Category cat) { _entry = cat.CreateEntry<string>("SavedCrews", "", "Saved crews", "Named whitelists you can load in Host settings. Easiest to manage there.", false, false, (ValueValidator)null, (string)null); Parse(_entry.Value); } private static void Parse(string? raw) { _crews.Clear(); if (string.IsNullOrWhiteSpace(raw)) { return; } string[] array = raw.Split(';'); foreach (string text in array) { int num = text.IndexOf('='); if (num <= 0) { continue; } Crew crew = new Crew { Name = CleanName(text.Substring(0, num)) }; if (crew.Name.Length == 0) { continue; } string[] array2 = text.Substring(num + 1).Split('|'); foreach (string text2 in array2) { string text3 = text2.Trim(); int k; for (k = 0; k < text3.Length && char.IsDigit(text3[k]); k++) { } if (k != 0 && ulong.TryParse(text3.Substring(0, k), out var id) && id != 0) { string name = text3.Substring(k).Trim().Trim('(', ')') .Trim(); if (crew.Members.All((BanList.Entry e) => e.SteamId != id)) { crew.Members.Add(new BanList.Entry(id, name)); } } } if (_crews.All((Crew c) => !c.Name.Equals(crew.Name, StringComparison.OrdinalIgnoreCase))) { _crews.Add(crew); } } } private static void Store() { if (_entry == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (Crew crew in _crews) { if (stringBuilder.Length > 0) { stringBuilder.Append("; "); } stringBuilder.Append(crew.Name).Append('='); stringBuilder.Append(string.Join("|", crew.Members.Select((BanList.Entry m) => $"{m.SteamId} ({CleanMember(m.Name)})"))); } _entry.Value = stringBuilder.ToString(); MelonPreferences.Save(); } public static string CleanName(string? s) { return new string((s ?? "").Where((char c) => c != ';' && c != '=' && c != '|' && c != ',' && !char.IsControl(c)).ToArray()).Trim(); } private static string CleanMember(string? s) { return new string((s ?? "").Where((char c) => c != ';' && c != '=' && c != '|' && c != '(' && c != ')' && !char.IsControl(c)).ToArray()).Trim(); } public static string SaveCurrent(string name) { name = CleanName(name); if (name.Length == 0) { return "Type a name for the crew first"; } List<BanList.Entry> list = Whitelist.Players.ToList(); if (list.Count == 0) { return "Add players to the whitelist first"; } int num = _crews.FindIndex((Crew c) => c.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (num < 0 && _crews.Count >= 12) { return $"You can keep up to {12} crews. Delete one first"; } Crew crew = new Crew { Name = name, Members = list }; if (num >= 0) { _crews[num] = crew; } else { _crews.Add(crew); } Store(); return (num >= 0) ? $"Updated {name} ({list.Count})" : $"Saved {name} ({list.Count})"; } public static string Load(string name) { Crew crew = _crews.FirstOrDefault((Crew c) => c.Name == name); if (crew == null) { return "That crew no longer exists"; } int num = Whitelist.Replace(crew.Members); Whitelist.SetEnabled(on: true); int num2 = crew.Members.Count - num; return (num2 > 0) ? $"Loaded {name}: {num} on the whitelist, {num2} banned and left off" : $"Loaded {name}: whitelist on with {num}"; } public static void Delete(string name) { if (_crews.RemoveAll((Crew c) => c.Name == name) > 0) { Store(); } } } public static class DepartedPlayers { public sealed class Slot { public int Index; public string Json = ""; public string Id = ""; public int Quantity; public ItemInstance? Item; } public sealed class Info { public ulong SteamId; public string Name = ""; public string Folder = ""; public DateTime SavedUtc; public List<Slot> Items = new List<Slot>(); public float Cash; } public sealed class TakenSlot { public int Index; public string Literal = ""; public string Id = ""; public int Quantity; public int Returned; } public sealed class TakeRecord { public string Path = ""; public string Save = ""; public string Name = ""; public string Backup = ""; public ulong SteamId; public DateTime Utc; public List<TakenSlot> Slots = new List<TakenSlot>(); public double Cash; public double CashReturned; public bool AllReturned => Slots.All((TakenSlot s) => s.Returned >= s.Quantity) && Cash - CashReturned < 0.01; } public sealed class Taken { public DateTime Utc; public ulong SteamId; public string Name = ""; public string Summary = ""; public string Save = ""; public TakeRecord? Record; public bool GiveBack; } private sealed class Literal { public int Start; public int End; public string Value = ""; } private sealed class Held { public ItemSlot Slot = null; public int Before; public int Dropped(string id) { try { ItemInstance itemInstance = Slot.ItemInstance; if (itemInstance == null) { return Before; } if (((BaseItemInstance)itemInstance).ID != id) { return 0; } return Math.Max(0, Before - ((BaseItemInstance)itemInstance).Quantity); } catch { return 0; } } } private sealed class Stock { public string Id = ""; public int Usable; public int Asked; public readonly List<Held> Slots = new List<Held>(); } private sealed class Pending { public TakenSlot Slot = null; public string Key = ""; public int Asked; } private static List<Info> _cache = new List<Info>(); private static float _cacheUntil; private const double SettleSeconds = 10.0; private static readonly Regex IdRx = new Regex("\"ID\"\\s*:\\s*\"([^\"]*)\""); private static readonly Regex QtyRx = new Regex("\"Quantity\"\\s*:\\s*(\\d+)"); private static readonly Regex TypeRx = new Regex("\"DataType\"\\s*:\\s*\"([^\"]*)\""); private static readonly Regex CashRx = new Regex("\"CashBalance\"\\s*:\\s*(-?[0-9.eE+-]+)"); private static Dictionary<ulong, string>? _names; private static float _nextNameScan; private static bool _namesWarned; private static List<Taken>? _log; private static DateTime _logStamp; private static bool _givingBack; private static string TakenDir => Path.Combine(MelonEnvironment.UserDataDirectory, "BiggerLobbies", "Taken"); private static string NamesFile => Path.Combine(MelonEnvironment.UserDataDirectory, "BiggerLobbies", "PlayerNames.tsv"); private static string LogFile => Path.Combine(TakenDir, "TakenLog.tsv"); private static string SaveName() { try { LoadManager instance = Singleton<LoadManager>.Instance; return Path.GetFileName((((instance != null) ? instance.LoadedGameFolderPath : null) ?? "").TrimEnd('/', '\\')); } catch { return ""; } } private static Dictionary<ulong, string> Names() { if (_names != null) { return _names; } _names = new Dictionary<ulong, string>(); try { if (File.Exists(NamesFile)) { string[] array = File.ReadAllLines(NamesFile); foreach (string text in array) { string[] array2 = text.Split('\t'); if (array2.Length >= 2 && ulong.TryParse(array2[0], out var result) && result != 0L && array2[1].Trim().Length > 0) { _names[result] = array2[1].Trim(); } } } } catch (Exception ex) { MelonLogger.Warning("Couldn't read saved player names: " + ex.Message); } return _names; } public static void Update() { if (Time.unscaledTime < _nextNameScan) { return; } _nextNameScan = Time.unscaledTime + 5f; try { if (!InstanceFinder.IsServer) { return; } List<Player> playerList = Player.PlayerList; if (playerList == null) { return; } Dictionary<ulong, string> dictionary = Names(); bool flag = false; for (int i = 0; i < playerList.Count; i++) { Player val = playerList[i]; if ((Object)(object)val == (Object)null || val.IsLocalPlayer) { continue; } NetworkConnection connection = val.Connection; if (!(connection == (NetworkConnection)null) && !connection.IsLocalClient) { ulong num = BanList.VerifiedSteamId(connection.ClientId); string text = Tsv(val.PlayerName ?? "").Trim(); if (num != 0L && text.Length != 0 && (!dictionary.TryGetValue(num, out var value) || !(value == text))) { dictionary[num] = text; flag = true; } } } if (flag) { Directory.CreateDirectory(Path.GetDirectoryName(NamesFile)); File.WriteAllLines(NamesFile, dictionary.Select((KeyValuePair<ulong, string> kv) => kv.Key + "\t" + kv.Value)); } } catch (Exception ex) { if (!_namesWarned) { _namesWarned = true; MelonLogger.Warning("Couldn't save player names: " + ex.Message); } } } public static string NameOf(ulong steamId) { try { if (Names().TryGetValue(steamId, out var value) && value.Length > 0) { return value; } } catch { } return Friends.NameOf(steamId); } private static string Tsv(string s) { return (s ?? "").Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); } private static void AppendLog(ulong id, string name, string summary, string record = "") { try { string text = string.Join("\t", DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture), Tsv(SaveName()), id.ToString(), Tsv(name), Tsv(summary), Tsv(record)); File.AppendAllText(LogFile, text + "\n"); _log = null; } catch (Exception ex) { MelonLogger.Warning("Couldn't write the take log: " + ex.Message); } } public static List<Taken> History(int max = 8) { try { if (!File.Exists(LogFile)) { return new List<Taken>(); } DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(LogFile); if (_log == null || lastWriteTimeUtc != _logStamp) { _logStamp = lastWriteTimeUtc; _log = new List<Taken>(); string[] array = File.ReadAllLines(LogFile); foreach (string text in array) { string[] array2 = text.Split('\t'); if (array2.Length >= 5 && ulong.TryParse(array2[2], out var result) && DateTime.TryParse(array2[0], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result2)) { Taken taken = new Taken { Utc = result2.ToUniversalTime(), Save = array2[1], SteamId = result, Name = array2[3], Summary = array2[4] }; taken.GiveBack = taken.Summary.StartsWith("Gave back", StringComparison.Ordinal); if (array2.Length >= 6 && array2[5].Trim().Length > 0) { taken.Record = ReadRecord(Path.Combine(TakenDir, Path.GetFileName(array2[5].Trim()))); } _log.Add(taken); } } } string save = SaveName(); return (from e in _log where e.Save == save orderby e.Utc descending select e).Take(max).ToList(); } catch { return new List<Taken>(); } } private static void WriteRecord(TakeRecord r) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\n"); stringBuilder.Append(" \"version\": 1,\n"); stringBuilder.Append(" \"save\": ").Append(MiniJson.Quote(r.Save)).Append(",\n"); stringBuilder.Append(" \"steamId\": ").Append(MiniJson.Quote(r.SteamId.ToString())).Append(",\n"); stringBuilder.Append(" \"name\": ").Append(MiniJson.Quote(r.Name)).Append(",\n"); stringBuilder.Append(" \"utc\": ").Append(MiniJson.Quote(r.Utc.ToString("o", CultureInfo.InvariantCulture))).Append(",\n"); stringBuilder.Append(" \"backup\": ").Append(MiniJson.Quote(r.Backup)).Append(",\n"); stringBuilder.Append(" \"slots\": ["); for (int i = 0; i < r.Slots.Count; i++) { TakenSlot takenSlot = r.Slots[i]; stringBuilder.Append((i == 0) ? "\n" : ",\n"); stringBuilder.Append(" {\"index\": ").Append(takenSlot.Index).Append(", \"id\": ") .Append(MiniJson.Quote(takenSlot.Id)) .Append(", \"quantity\": ") .Append(takenSlot.Quantity) .Append(", \"returned\": ") .Append(takenSlot.Returned) .Append(", \"literal\": ") .Append(MiniJson.Quote(takenSlot.Literal)) .Append('}'); } stringBuilder.Append((r.Slots.Count > 0) ? "\n ],\n" : "],\n"); stringBuilder.Append(" \"cash\": ").Append(MiniJson.Number(r.Cash)).Append(",\n"); stringBuilder.Append(" \"cashReturned\": ").Append(MiniJson.Number(r.CashReturned)).Append('\n'); stringBuilder.Append("}\n"); File.WriteAllText(r.Path, stringBuilder.ToString()); } private static TakeRecord? ReadRecord(string path) { try { if (!File.Exists(path)) { return null; } if (!(MiniJson.Parse(File.ReadAllText(path)) is Dictionary<string, object> dictionary)) { return null; } if (!ulong.TryParse(MiniJson.Str(dictionary, "steamId"), out var result) || result == 0) { return null; } DateTime.TryParse(MiniJson.Str(dictionary, "utc"), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result2); TakeRecord takeRecord = new TakeRecord { Path = path, Save = MiniJson.Str(dictionary, "save"), SteamId = result, Name = MiniJson.Str(dictionary, "name"), Utc = result2.ToUniversalTime(), Backup = MiniJson.Str(dictionary, "backup"), Cash = MiniJson.Num(dictionary, "cash"), CashReturned = MiniJson.Num(dictionary, "cashReturned") }; if (dictionary.TryGetValue("slots", out var value) && value is List<object> list) { foreach (object item in list) { if (item is Dictionary<string, object> o) { TakenSlot takenSlot = new TakenSlot { Index = (int)MiniJson.Num(o, "index"), Id = MiniJson.Str(o, "id"), Quantity = (int)MiniJson.Num(o, "quantity"), Returned = (int)MiniJson.Num(o, "returned"), Literal = MiniJson.Str(o, "literal") }; if (takenSlot.Literal.Length != 0 && takenSlot.Id.Length != 0 && takenSlot.Quantity > 0) { takeRecord.Slots.Add(takenSlot); } } } } return takeRecord; } catch (Exception ex) { MelonLogger.Warning("Couldn't read the take record " + Path.GetFileName(path) + ": " + ex.Message); return null; } } private static string? PlayersFo