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<MultiplayerManager> 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<bool> _enabled;
private ConfigEntry<float> _intervalMinutes;
private ConfigEntry<bool> _colorConsole;
private ConfigEntry<bool> _manageLobbyName;
private ConfigEntry<string> _lobbyName;
private ConfigEntry<bool> _refreshExistingData;
private ConfigEntry<string> _tagFocus;
private ConfigEntry<string> _tagMature;
private ConfigEntry<string> _tagChill;
private ConfigEntry<string> _tagBreak;
private ConfigEntry<string> _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<bool>("General", "LobbyHeartbeatEnabled", true, "Periodically refresh Steam lobby data so the listing does not expire during long sessions.");
_intervalMinutes = ((BaseUnityPlugin)this).Config.Bind<float>("General", "LobbyHeartbeatIntervalMinutes", 2f, new ConfigDescription("Minutes between lobby heartbeats. Use 1-2 for 12+ hour runs.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 10f), Array.Empty<object>()));
_colorConsole = ((BaseUnityPlugin)this).Config.Bind<bool>("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<bool>("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<bool>("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<string>("Lobby", "LobbyName", "", "Custom lobby name to enforce when 'ManageLobbyName' is true.");
_tagFocus = ((BaseUnityPlugin)this).Config.Bind<string>("Lobby", "TagFocus", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set.");
_tagMature = ((BaseUnityPlugin)this).Config.Bind<string>("Lobby", "TagMature", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set.");
_tagChill = ((BaseUnityPlugin)this).Config.Bind<string>("Lobby", "TagChill", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set.");
_tagBreak = ((BaseUnityPlugin)this).Config.Bind<string>("Lobby", "TagBreak", "", "Override value for this lobby tag. Leave empty to preserve whatever the game already set.");
_tagModded = ((BaseUnityPlugin)this).Config.Bind<string>("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);
}
}