Decompiled source of Heartbeat v1.2.0

BepInEx\plugins\Heartbeat\LobbyHeartbeat.dll

Decompiled a month ago
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);
	}
}