using System; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("LobbyHeartbeat")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: AssemblyInformationalVersion("1.2.0")] [assembly: AssemblyProduct("Heartbeat")] [assembly: AssemblyTitle("LobbyHeartbeat")] [assembly: AssemblyVersion("1.2.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 LobbyHeartbeat { internal static class GameBridge { private delegate object MemberGetter(object target = null); private static bool _resolved; private static MemberGetter _singletonInstance; private static MemberGetter _lobbyStatus; private static MemberGetter _lobbyCode; private static Type _mmType; private static Type _cSteamIdType; private static FieldInfo _cSteamIdValue; private static ConstructorInfo _cSteamIdCtor; private static MethodInfo _setLobbyData; private static MethodInfo _getLobbyData; private static MethodInfo _getLobbyOwner; private static MethodInfo _getMySteamId; public static bool TryResolve(out string error) { if (_resolved) { error = null; return true; } error = ResolveGame() ?? ResolveSteam(); _resolved = error == null; return _resolved; } private static string ResolveGame() { _mmType = FindType("MultiplayerManager"); if (_mmType == null) { return "MultiplayerManager not loaded"; } Type type = FindType("MonoSingleton`1"); if (type == null) { return "MonoSingleton not loaded"; } Type type2; try { type2 = type.MakeGenericType(_mmType); } catch (Exception ex) { return "MonoSingleton error: " + ex.Message; } _singletonInstance = ResolveStaticGetter(type2, "I"); if (_singletonInstance == null) { return "MonoSingleton.I not found"; } _lobbyStatus = ResolveInstanceGetter(_mmType, "LobbyStatus"); if (_lobbyStatus == null) { return "MultiplayerManager.LobbyStatus not found"; } _lobbyCode = ResolveInstanceGetter(_mmType, "LobbyCode"); if (_lobbyCode == null) { return "MultiplayerManager.LobbyCode not found"; } return null; } private static string ResolveSteam() { _cSteamIdType = FindType("Steamworks.CSteamID") ?? FindType("CSteamID"); if (_cSteamIdType == null) { return "Steamworks.CSteamID not loaded"; } _cSteamIdCtor = _cSteamIdType.GetConstructor(new Type[1] { typeof(ulong) }); if (_cSteamIdCtor == null) { return "CSteamID(ulong) constructor not found"; } _cSteamIdValue = _cSteamIdType.GetField("m_SteamID"); if (_cSteamIdValue == null) { return "CSteamID.m_SteamID field not found"; } Type type = FindType("Steamworks.SteamMatchmaking") ?? FindType("SteamMatchmaking"); if (type == null) { return "Steamworks.SteamMatchmaking not loaded"; } _setLobbyData = type.GetMethod("SetLobbyData", new Type[3] { _cSteamIdType, typeof(string), typeof(string) }); if (_setLobbyData == null) { return "SteamMatchmaking.SetLobbyData not found"; } _getLobbyData = type.GetMethod("GetLobbyData", new Type[2] { _cSteamIdType, typeof(string) }); if (_getLobbyData == null) { return "SteamMatchmaking.GetLobbyData not found"; } _getLobbyOwner = type.GetMethod("GetLobbyOwner", new Type[1] { _cSteamIdType }); if (_getLobbyOwner == null) { return "SteamMatchmaking.GetLobbyOwner not found"; } Type type2 = FindType("Steamworks.SteamUser") ?? FindType("SteamUser"); if (type2 == null) { return "Steamworks.SteamUser not loaded"; } _getMySteamId = type2.GetMethod("GetSteamID", Type.EmptyTypes); if (_getMySteamId == null) { return "SteamUser.GetSteamID not found"; } return null; } public static bool TryGetActiveLobby(out ulong lobbyId) { lobbyId = 0uL; object obj = _singletonInstance(); if (obj == null) { return false; } object obj2 = _lobbyStatus(obj); if (!(obj2 is bool) || !(bool)obj2) { return false; } string text = _lobbyCode(obj) as string; if (string.IsNullOrEmpty(text)) { return false; } if (ulong.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out lobbyId)) { return lobbyId != 0; } return false; } public static bool IsLobbyOwner(ulong lobbyId) { object obj = NewCSteamId(lobbyId); object? cSteamId = _getLobbyOwner.Invoke(null, new object[1] { obj }); object cSteamId2 = _getMySteamId.Invoke(null, null); return SteamIdValue(cSteamId) == SteamIdValue(cSteamId2); } public static void SetLobbyData(ulong lobbyId, string key, string value) { _setLobbyData.Invoke(null, new object[3] { NewCSteamId(lobbyId), key, value }); } public static string GetLobbyData(ulong lobbyId, string key) { return _getLobbyData.Invoke(null, new object[2] { NewCSteamId(lobbyId), key }) as string; } private static object NewCSteamId(ulong value) { return _cSteamIdCtor.Invoke(new object[1] { value }); } private static ulong SteamIdValue(object cSteamId) { if (cSteamId != null) { return (ulong)_cSteamIdValue.GetValue(cSteamId); } return 0uL; } private static MemberGetter ResolveStaticGetter(Type type, string name) { PropertyInfo prop = type.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (prop != null && prop.CanRead) { return (object _) => prop.GetValue(null, null); } FieldInfo field = type.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (field != null) { return (object _) => field.GetValue(null); } return null; } private static MemberGetter ResolveInstanceGetter(Type type, string name) { PropertyInfo prop = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (prop != null && prop.CanRead) { return (object target) => prop.GetValue(target, null); } FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if (field != null) { return (object target) => field.GetValue(target); } return null; } private static Type FindType(string name) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type = assemblies[i].GetType(name, throwOnError: false); if (type != null) { return type; } } assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { Type[] types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types; } catch { continue; } if (types == null) { continue; } Type[] array = types; foreach (Type type2 in array) { if (type2 != null && (type2.FullName == name || type2.Name == name)) { return type2; } } } return null; } } [BepInPlugin("com.ontogether.lobbyheartbeat", "Heartbeat", "1.2.0")] public class LobbyHeartbeatPlugin : BaseUnityPlugin { public const string PluginGuid = "com.ontogether.lobbyheartbeat"; public const string PluginName = "Heartbeat"; public const string PluginVersion = "1.2.0"; private const float MinIntervalMinutes = 0.25f; private const float MaxIntervalMinutes = 10f; private const string ColorPrefix = "\u001b[38;5;141m"; private const string ColorReset = "\u001b[0m"; private ConfigEntry _enabled; private ConfigEntry _intervalMinutes; private ConfigEntry _colorConsole; private ConfigEntry _manageLobbyName; private ConfigEntry _lobbyName; private ConfigEntry _refreshExistingData; private ConfigEntry _tagFocus; private ConfigEntry _tagMature; private ConfigEntry _tagChill; private ConfigEntry _tagBreak; private ConfigEntry _tagModded; private float _timer; private ulong _currentLobby; private int _beatsThisLobby; private bool _warnedNotOwner; private bool _warnedReflection; private const int StdOutputHandle = -11; private const uint EnableVirtualTerminalProcessing = 4u; private void Awake() { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown _enabled = ((BaseUnityPlugin)this).Config.Bind("General", "LobbyHeartbeatEnabled", true, "Periodically refresh Steam lobby data so the listing does not expire during long sessions."); _intervalMinutes = ((BaseUnityPlugin)this).Config.Bind("General", "LobbyHeartbeatIntervalMinutes", 2f, new ConfigDescription("Minutes between lobby heartbeats. Use 1-2 for 12+ hour runs.", (AcceptableValueBase)(object)new AcceptableValueRange(0.25f, 10f), Array.Empty())); _colorConsole = ((BaseUnityPlugin)this).Config.Bind("General", "ColorConsole", true, "Show Heartbeat's console messages as white-on-purple. Turn off if your console shows raw color codes instead of colors."); if (_colorConsole.Value) { TryEnableAnsiColors(); } _refreshExistingData = ((BaseUnityPlugin)this).Config.Bind("Lobby", "RefreshExistingData", true, "Re-write the lobby's current name and tags on every heartbeat so Steam keeps every field fresh, not just the internal heartbeat key."); _manageLobbyName = ((BaseUnityPlugin)this).Config.Bind("Lobby", "ManageLobbyName", false, "If true, force the lobby name to 'LobbyName' on every heartbeat instead of preserving the in-game name."); _lobbyName = ((BaseUnityPlugin)this).Config.Bind("Lobby", "LobbyName", "", "Custom lobby name to enforce when 'ManageLobbyName' is true."); _tagFocus = ((BaseUnityPlugin)this).Config.Bind("Lobby", "TagFocus", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set."); _tagMature = ((BaseUnityPlugin)this).Config.Bind("Lobby", "TagMature", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set."); _tagChill = ((BaseUnityPlugin)this).Config.Bind("Lobby", "TagChill", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set."); _tagBreak = ((BaseUnityPlugin)this).Config.Bind("Lobby", "TagBreak", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set."); _tagModded = ((BaseUnityPlugin)this).Config.Bind("Lobby", "TagModded", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set."); _timer = float.MaxValue; InfoLine("Heartbeat v1.2.0 loaded (heartbeat " + (_enabled.Value ? "enabled" : "disabled") + ", " + $"interval {ClampInterval(_intervalMinutes.Value):0.##} min)"); } private void Update() { if (_enabled.Value) { _timer += Time.deltaTime; if (!(_timer < ClampInterval(_intervalMinutes.Value) * 60f)) { _timer = 0f; SendLobbyHeartbeat(); } } } private float ClampInterval(float minutes) { if (minutes < 0.25f) { return 0.25f; } if (minutes > 10f) { return 10f; } return minutes; } private void SendLobbyHeartbeat() { try { if (!GameBridge.TryResolve(out var error)) { if (!_warnedReflection) { _warnedReflection = true; DebugLine("Heartbeat not active yet (" + error + "). Will retry."); } return; } _warnedReflection = false; if (!GameBridge.TryGetActiveLobby(out var lobbyId)) { EndCurrentLobby(); return; } if (lobbyId != _currentLobby) { _currentLobby = lobbyId; _beatsThisLobby = 0; _warnedNotOwner = false; InfoLine($"Lobby detected ({lobbyId}); keeping the Steam listing alive."); } if (!GameBridge.IsLobbyOwner(lobbyId)) { if (!_warnedNotOwner) { _warnedNotOwner = true; InfoLine("Not the lobby owner; only the host can refresh the listing, so heartbeat is idle here."); } return; } GameBridge.SetLobbyData(lobbyId, "heartbeat", DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture)); ApplyName(lobbyId); ApplyTag(lobbyId, "focus", _tagFocus.Value); ApplyTag(lobbyId, "mature", _tagMature.Value); ApplyTag(lobbyId, "chill", _tagChill.Value); ApplyTag(lobbyId, "break", _tagBreak.Value); ApplyTag(lobbyId, "modded", _tagModded.Value); _beatsThisLobby++; if (_beatsThisLobby == 1) { InfoLine("First lobby heartbeat sent (Steam listing kept alive)."); } else { DebugLine($"Lobby heartbeat sent (#{_beatsThisLobby}, Steam listing kept alive)."); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Lobby heartbeat failed: " + ex.GetType().Name + ": " + ex.Message)); } } private void ApplyName(ulong lobbyId) { if (_manageLobbyName.Value && !string.IsNullOrEmpty(_lobbyName.Value)) { GameBridge.SetLobbyData(lobbyId, "Name", _lobbyName.Value); } else if (_refreshExistingData.Value) { RefreshExisting(lobbyId, "Name"); } } private void ApplyTag(ulong lobbyId, string key, string overrideValue) { if (!string.IsNullOrEmpty(overrideValue)) { GameBridge.SetLobbyData(lobbyId, key, overrideValue); } else if (_refreshExistingData.Value) { RefreshExisting(lobbyId, key); } } private void RefreshExisting(ulong lobbyId, string key) { string lobbyData = GameBridge.GetLobbyData(lobbyId, key); if (!string.IsNullOrEmpty(lobbyData)) { GameBridge.SetLobbyData(lobbyId, key, lobbyData); } } private void EndCurrentLobby() { if (_currentLobby != 0L) { InfoLine("Lobby ended; heartbeat idle until the next lobby starts."); _currentLobby = 0uL; _beatsThisLobby = 0; _warnedNotOwner = false; } } private void InfoLine(string message) { ((BaseUnityPlugin)this).Logger.LogInfo((object)Colorize(message)); } private void DebugLine(string message) { ((BaseUnityPlugin)this).Logger.LogDebug((object)Colorize(message)); } private string Colorize(string message) { if (_colorConsole == null || !_colorConsole.Value) { return message; } return "\u001b[38;5;141m" + message + "\u001b[0m"; } private void TryEnableAnsiColors() { try { IntPtr stdHandle = GetStdHandle(-11); if (!(stdHandle == IntPtr.Zero) && !(stdHandle == new IntPtr(-1)) && GetConsoleMode(stdHandle, out var lpMode)) { SetConsoleMode(stdHandle, lpMode | 4); } } catch { } } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GetStdHandle(int nStdHandle); [DllImport("kernel32.dll")] private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); [DllImport("kernel32.dll")] private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); } }