using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Reflection;
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 BoneLib.BoneMenu;
using FusionPresence;
using FusionPresence.Config;
using FusionPresence.Menu;
using FusionPresence.Net;
using FusionPresence.Runtime;
using LabFusion.Data;
using LabFusion.Network;
using LabFusion.Player;
using LabFusion.Representation;
using LabFusion.SDK.Metadata;
using LabFusion.Scene;
using LabFusion.Utilities;
using MelonLoader;
using MelonLoader.Preferences;
using Microsoft.CodeAnalysis;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(Main), "FusionPresence", "1.0.1", "Quartz", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: MelonAdditionalDependencies(new string[] { "BoneLib" })]
[assembly: MelonOptionalDependencies(new string[] { "LabFusion" })]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("Quartz")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyInformationalVersion("1.0.1")]
[assembly: AssemblyProduct("FusionPresence")]
[assembly: AssemblyTitle("FusionPresence")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.1.0")]
[module: UnverifiableCode]
[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 FusionPresence
{
internal static class Log
{
private static Instance _logger;
public static bool Verbose { get; set; }
public static void Bind(Instance logger)
{
_logger = logger;
}
public static void Msg(string message)
{
Instance logger = _logger;
if (logger != null)
{
logger.Msg(message);
}
}
public static void Warn(string message)
{
Instance logger = _logger;
if (logger != null)
{
logger.Warning(message);
}
}
public static void Error(string message)
{
Instance logger = _logger;
if (logger != null)
{
logger.Error(message);
}
}
public static void Debug(string message)
{
if (Verbose)
{
Instance logger = _logger;
if (logger != null)
{
logger.Msg("[debug] " + message);
}
}
}
}
public class Main : MelonMod
{
private bool _hooked;
public override void OnInitializeMelon()
{
Log.Bind(((MelonBase)this).LoggerInstance);
Settings.Register();
WebhookSender.Start();
PresenceMenu.Create();
if (FusionBridge.IsFusionPresent())
{
PresenceTracker.Start();
_hooked = true;
Log.Msg("Ready. Configure the webhook under BoneMenu → Fusion Presence.");
}
else
{
Log.Warn("LabFusion not found. Presence reporting is idle until it loads.");
}
}
public override void OnSceneWasLoaded(int buildIndex, string sceneName)
{
if (!_hooked && FusionBridge.IsFusionPresent())
{
PresenceTracker.Start();
_hooked = true;
Log.Msg("LabFusion detected on scene load, presence reporting is now active.");
}
}
public override void OnUpdate()
{
if (_hooked)
{
PresenceTracker.Tick(Time.unscaledDeltaTime);
}
}
public override void OnApplicationQuit()
{
Shutdown();
}
public override void OnDeinitializeMelon()
{
Shutdown();
}
private void Shutdown()
{
if (_hooked)
{
PresenceTracker.Stop();
_hooked = false;
}
WebhookSender.Stop();
PresenceMenu.Destroy();
}
}
}
namespace FusionPresence.Runtime
{
internal static class FusionBridge
{
private static ServerEvent _onJoined;
private static ServerEvent _onStarted;
private static ServerEvent _onDisconnected;
private static PlayerUpdate _onPlayerJoined;
private static PlayerUpdate _onPlayerLeft;
private static UpdateEvent _onSceneInit;
public static bool Subscribed { get; private set; }
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsFusionPresent()
{
try
{
_ = NetworkInfo.HasServer;
return true;
}
catch (Exception ex)
{
Log.Debug("Fusion not present: " + ex.GetType().Name);
return false;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Subscribe(Action onJoined, Action onStarted, Action onDisconnected, Action<string> onPlayerJoined, Action<string> onPlayerLeft, Action onSceneInit)
{
//IL_0042: Unknown result type (might be due to invalid IL or missing references)
//IL_004c: Expected O, but got Unknown
//IL_0053: Unknown result type (might be due to invalid IL or missing references)
//IL_005d: Expected O, but got Unknown
//IL_0064: Unknown result type (might be due to invalid IL or missing references)
//IL_006e: Expected O, but got Unknown
//IL_0075: Unknown result type (might be due to invalid IL or missing references)
//IL_007f: Expected O, but got Unknown
//IL_0086: Unknown result type (might be due to invalid IL or missing references)
//IL_0090: Expected O, but got Unknown
//IL_0097: Unknown result type (might be due to invalid IL or missing references)
//IL_00a1: Expected O, but got Unknown
if (Subscribed)
{
return;
}
try
{
_onJoined = (ServerEvent)delegate
{
onJoined();
};
_onStarted = (ServerEvent)delegate
{
onStarted();
};
_onDisconnected = (ServerEvent)delegate
{
onDisconnected();
};
_onPlayerJoined = (PlayerUpdate)delegate(PlayerID id)
{
onPlayerJoined(SafeName(id));
};
_onPlayerLeft = (PlayerUpdate)delegate(PlayerID id)
{
onPlayerLeft(SafeName(id));
};
_onSceneInit = (UpdateEvent)delegate
{
onSceneInit();
};
MultiplayerHooking.OnJoinedServer += _onJoined;
MultiplayerHooking.OnStartedServer += _onStarted;
MultiplayerHooking.OnDisconnected += _onDisconnected;
MultiplayerHooking.OnPlayerJoined += _onPlayerJoined;
MultiplayerHooking.OnPlayerLeft += _onPlayerLeft;
MultiplayerHooking.OnMainSceneInitialized += _onSceneInit;
Subscribed = true;
Log.Msg("Hooked into Fusion multiplayer events.");
}
catch (Exception value)
{
Log.Error($"Failed to hook Fusion events: {value}");
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Unsubscribe()
{
if (!Subscribed)
{
return;
}
try
{
MultiplayerHooking.OnJoinedServer -= _onJoined;
MultiplayerHooking.OnStartedServer -= _onStarted;
MultiplayerHooking.OnDisconnected -= _onDisconnected;
MultiplayerHooking.OnPlayerJoined -= _onPlayerJoined;
MultiplayerHooking.OnPlayerLeft -= _onPlayerLeft;
MultiplayerHooking.OnMainSceneInitialized -= _onSceneInit;
}
catch (Exception value)
{
Log.Error($"Failed to unhook Fusion events: {value}");
}
finally
{
_onJoined = null;
_onStarted = null;
_onDisconnected = null;
_onPlayerJoined = null;
_onPlayerLeft = null;
_onSceneInit = null;
Subscribed = false;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool HasServer()
{
try
{
return NetworkInfo.HasServer;
}
catch
{
return false;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool LobbyInfoReady()
{
try
{
LobbyInfo lobbyInfo = LobbyInfoManager.LobbyInfo;
return lobbyInfo != null && lobbyInfo.LobbyID != 0;
}
catch
{
return false;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static string SafeName(PlayerID id)
{
try
{
object obj;
if (id == null)
{
obj = null;
}
else
{
PlayerMetadata metadata = id.Metadata;
if (metadata == null)
{
obj = null;
}
else
{
MetadataVariable username = metadata.Username;
obj = ((username != null) ? username.GetValue() : null);
}
}
string text = (string)obj;
if (!string.IsNullOrEmpty(text))
{
return text;
}
object obj2;
if (id == null)
{
obj2 = null;
}
else
{
PlayerMetadata metadata2 = id.Metadata;
if (metadata2 == null)
{
obj2 = null;
}
else
{
MetadataVariable nickname = metadata2.Nickname;
obj2 = ((nickname != null) ? nickname.GetValue() : null);
}
}
string text2 = (string)obj2;
if (!string.IsNullOrEmpty(text2))
{
return text2;
}
return (id != null) ? id.PlatformID.ToString() : "unknown";
}
catch
{
return "unknown";
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetPlayerCount()
{
try
{
return PlayerIDManager.PlayerCount;
}
catch
{
return 0;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public unsafe static SessionSnapshot BuildSnapshot(PresenceEvent kind, string changeSummary = "")
{
//IL_00a4: 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)
SessionSnapshot sessionSnapshot = new SessionSnapshot
{
Event = kind,
ChangeSummary = (changeSummary ?? "")
};
try
{
if (Settings.IncludeOwnName.Value)
{
sessionSnapshot.LocalName = LocalPlayer.Username ?? "";
}
sessionSnapshot.IsHost = NetworkInfo.IsHost;
}
catch (Exception ex)
{
Log.Debug("local player read failed: " + ex.Message);
}
try
{
LobbyInfo lobbyInfo = LobbyInfoManager.LobbyInfo;
if (lobbyInfo != null)
{
sessionSnapshot.LobbyName = lobbyInfo.LobbyName ?? "";
sessionSnapshot.LobbyHostName = lobbyInfo.LobbyHostName ?? "";
sessionSnapshot.MaxPlayers = lobbyInfo.MaxPlayers;
sessionSnapshot.Privacy = ((object)lobbyInfo.Privacy/*cast due to .constrained prefix*/).ToString();
sessionSnapshot.LevelTitle = lobbyInfo.LevelTitle ?? "";
sessionSnapshot.LevelBarcode = lobbyInfo.LevelBarcode ?? "";
}
}
catch (Exception ex2)
{
Log.Debug("lobby info read failed: " + ex2.Message);
}
try
{
string title = FusionSceneManager.Title;
if (!string.IsNullOrEmpty(title))
{
sessionSnapshot.LevelTitle = title;
sessionSnapshot.LevelBarcode = FusionSceneManager.Barcode ?? sessionSnapshot.LevelBarcode;
}
}
catch (Exception ex3)
{
Log.Debug("scene read failed: " + ex3.Message);
}
if (Settings.IncludeServerCode.Value)
{
try
{
sessionSnapshot.LobbyCode = NetworkHelper.GetServerCode() ?? "";
}
catch (Exception ex4)
{
Log.Debug("server code read failed: " + ex4.Message);
}
}
try
{
sessionSnapshot.PlayerCount = PlayerIDManager.PlayerCount;
if (Settings.IncludePlayerList.Value)
{
PermissionLevel val = default(PermissionLevel);
foreach (PlayerID playerID in PlayerIDManager.PlayerIDs)
{
if (playerID != null && playerID.IsValid)
{
SessionSnapshot.PlayerEntry playerEntry = new SessionSnapshot.PlayerEntry();
PlayerMetadata metadata = playerID.Metadata;
object obj;
if (metadata == null)
{
obj = null;
}
else
{
MetadataVariable username = metadata.Username;
obj = ((username != null) ? username.GetValue() : null);
}
if (obj == null)
{
obj = "";
}
playerEntry.Username = (string)obj;
PlayerMetadata metadata2 = playerID.Metadata;
object obj2;
if (metadata2 == null)
{
obj2 = null;
}
else
{
MetadataVariable nickname = metadata2.Nickname;
obj2 = ((nickname != null) ? nickname.GetValue() : null);
}
if (obj2 == null)
{
obj2 = "";
}
playerEntry.Nickname = (string)obj2;
playerEntry.IsHost = playerID.IsHost;
playerEntry.IsMe = playerID.IsMe;
SessionSnapshot.PlayerEntry playerEntry2 = playerEntry;
if (Settings.IncludePermissions.Value && MetadataHelper.TryGetPermissionLevel(playerID, ref val))
{
playerEntry2.Permission = ((object)(*(PermissionLevel*)(&val))/*cast due to .constrained prefix*/).ToString();
}
sessionSnapshot.Players.Add(playerEntry2);
}
}
}
}
catch (Exception ex5)
{
Log.Debug("roster read failed: " + ex5.Message);
}
return sessionSnapshot;
}
}
internal static class PresenceTracker
{
private static bool _inSession;
private static float _joinTimer = -1f;
private static float _joinElapsed;
private static PresenceEvent _pendingJoinKind;
private static float _rosterTimer = -1f;
private static readonly List<string> _joinedSince = new List<string>();
private static readonly List<string> _leftSince = new List<string>();
private const float JoinHardTimeout = 15f;
public static void Start()
{
FusionBridge.Subscribe(delegate
{
BeginSession(PresenceEvent.Joined);
}, delegate
{
BeginSession(PresenceEvent.Hosting);
}, OnDisconnected, OnPlayerJoined, OnPlayerLeft, OnSceneInitialized);
}
public static void Stop()
{
FusionBridge.Unsubscribe();
Reset();
}
public static void Tick(float deltaTime)
{
if (_joinTimer >= 0f)
{
_joinTimer -= deltaTime;
_joinElapsed += deltaTime;
bool num = FusionBridge.LobbyInfoReady() && _joinTimer <= 0f;
bool flag = _joinElapsed >= 15f;
if (num || flag)
{
_joinTimer = -1f;
if (flag && !FusionBridge.LobbyInfoReady())
{
Log.Warn("Lobby info never arrived; posting with whatever we could read.");
}
Dispatch(_pendingJoinKind);
}
else if (_joinTimer <= 0f)
{
_joinTimer = 0f;
}
}
if (_rosterTimer >= 0f)
{
_rosterTimer -= deltaTime;
if (_rosterTimer <= 0f)
{
_rosterTimer = -1f;
FlushRoster();
}
}
}
private static void BeginSession(PresenceEvent kind)
{
_inSession = true;
_rosterTimer = -1f;
_joinedSince.Clear();
_leftSince.Clear();
if (!((kind == PresenceEvent.Hosting) ? Settings.ReportHost.Value : Settings.ReportJoin.Value))
{
Log.Debug($"{kind} reporting is disabled.");
return;
}
_pendingJoinKind = kind;
_joinElapsed = 0f;
_joinTimer = Math.Max(0f, Settings.JoinDelay.Value);
Log.Debug($"{kind} detected, posting in up to {_joinTimer:0.#}s.");
}
private static void OnDisconnected()
{
_joinTimer = -1f;
_rosterTimer = -1f;
bool inSession = _inSession;
_inSession = false;
if (inSession && Settings.ReportLeave.Value)
{
Dispatch(PresenceEvent.Left);
}
_joinedSince.Clear();
_leftSince.Clear();
}
private static void OnPlayerJoined(string name)
{
if (_inSession && Settings.ReportRoster.Value && !(_joinTimer >= 0f))
{
_leftSince.Remove(name);
if (!_joinedSince.Contains(name))
{
_joinedSince.Add(name);
}
_rosterTimer = Math.Max(0.5f, Settings.RosterDebounce.Value);
}
}
private static void OnPlayerLeft(string name)
{
if (_inSession && Settings.ReportRoster.Value && !(_joinTimer >= 0f))
{
_joinedSince.Remove(name);
if (!_leftSince.Contains(name))
{
_leftSince.Add(name);
}
_rosterTimer = Math.Max(0.5f, Settings.RosterDebounce.Value);
}
}
private static void OnSceneInitialized()
{
if (_inSession && Settings.ReportLevel.Value && !(_joinTimer >= 0f))
{
Dispatch(PresenceEvent.LevelChanged);
}
}
private static void FlushRoster()
{
if (_joinedSince.Count != 0 || _leftSince.Count != 0)
{
List<string> list = new List<string>(2);
if (_joinedSince.Count > 0)
{
list.Add("joined: " + string.Join(", ", _joinedSince));
}
if (_leftSince.Count > 0)
{
list.Add("left: " + string.Join(", ", _leftSince));
}
string summary = string.Join(" • ", list);
_joinedSince.Clear();
_leftSince.Clear();
Dispatch(PresenceEvent.RosterChanged, summary);
}
}
private static void Dispatch(PresenceEvent kind, string summary = "")
{
try
{
WebhookSender.Enqueue(PayloadBuilder.Build(FusionBridge.BuildSnapshot(kind, summary)));
}
catch (Exception value)
{
Log.Error($"Failed to build the {kind} payload: {value}");
}
}
public static void SendTest()
{
try
{
SessionSnapshot snap = ((!FusionBridge.IsFusionPresent() || !FusionBridge.HasServer()) ? new SessionSnapshot
{
Event = PresenceEvent.Test,
LocalName = "Local player",
LevelTitle = "Not in a Fusion session",
PlayerCount = 0
} : FusionBridge.BuildSnapshot(PresenceEvent.Test));
WebhookSender.Enqueue(PayloadBuilder.Build(snap));
}
catch (Exception value)
{
Log.Error($"Test post failed: {value}");
}
}
private static void Reset()
{
_inSession = false;
_joinTimer = -1f;
_rosterTimer = -1f;
_joinedSince.Clear();
_leftSince.Clear();
}
}
internal enum PresenceEvent
{
Joined,
Hosting,
Left,
RosterChanged,
LevelChanged,
Test
}
internal sealed class SessionSnapshot
{
public sealed class PlayerEntry
{
public string Username = "";
public string Nickname = "";
public string Permission = "";
public bool IsHost;
public bool IsMe;
}
public PresenceEvent Event;
public DateTime TimestampUtc = DateTime.UtcNow;
public string LocalName = "";
public bool IsHost;
public string LobbyName = "";
public string LobbyHostName = "";
public string LobbyCode = "";
public string Privacy = "";
public string LevelTitle = "";
public string LevelBarcode = "";
public int PlayerCount;
public int MaxPlayers;
public List<PlayerEntry> Players = new List<PlayerEntry>();
public string ChangeSummary = "";
}
}
namespace FusionPresence.Net
{
internal static class Json
{
public static string Quote(string value)
{
StringBuilder stringBuilder = new StringBuilder((value?.Length ?? 0) + 16);
stringBuilder.Append('"');
Escape(stringBuilder, value);
stringBuilder.Append('"');
return stringBuilder.ToString();
}
public static void Escape(StringBuilder sb, string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
foreach (char c in value)
{
switch (c)
{
case '"':
sb.Append("\\\"");
continue;
case '\\':
sb.Append("\\\\");
continue;
case '\b':
sb.Append("\\b");
continue;
case '\f':
sb.Append("\\f");
continue;
case '\n':
sb.Append("\\n");
continue;
case '\r':
sb.Append("\\r");
continue;
case '\t':
sb.Append("\\t");
continue;
}
if (c < ' ' || c == '\u007f')
{
StringBuilder stringBuilder = sb.Append("\\u");
int num = c;
stringBuilder.Append(num.ToString("x4"));
}
else
{
sb.Append(c);
}
}
}
}
internal static class PayloadBuilder
{
private const int FieldValueLimit = 1024;
private const int TitleLimit = 256;
private static readonly Regex RichText = new Regex("<[^>]*>", RegexOptions.Compiled);
public static string Build(SessionSnapshot snap)
{
if (!Settings.DiscordEmbed.Value)
{
return BuildFlat(snap);
}
return BuildDiscord(snap);
}
private static string BuildDiscord(SessionSnapshot snap)
{
StringBuilder stringBuilder = new StringBuilder(768);
stringBuilder.Append('{');
stringBuilder.Append("\"username\":").Append(Json.Quote(Trim(Settings.BotName.Value, 80)));
stringBuilder.Append(",\"embeds\":[{");
stringBuilder.Append("\"title\":").Append(Json.Quote(Trim(Title(snap), 256)));
stringBuilder.Append(",\"color\":").Append(Color(snap.Event));
stringBuilder.Append(",\"timestamp\":").Append(Json.Quote(snap.TimestampUtc.ToString("yyyy-MM-ddTHH:mm:ssZ")));
List<(string, string, bool)> list = new List<(string, string, bool)>();
if (snap.Event != PresenceEvent.Left)
{
string item = (string.IsNullOrEmpty(snap.LevelTitle) ? "Unknown" : Clean(snap.LevelTitle));
list.Add(("Map", item, true));
list.Add(("Players", PlayerCountText(snap), true));
if (!string.IsNullOrEmpty(snap.Privacy))
{
list.Add(("Privacy", Clean(snap.Privacy), true));
}
if (!string.IsNullOrEmpty(snap.LobbyHostName))
{
list.Add(("Host", Clean(snap.LobbyHostName), true));
}
if (Settings.IncludeLevelBarcode.Value && !string.IsNullOrEmpty(snap.LevelBarcode))
{
list.Add(("Barcode", "`" + Clean(snap.LevelBarcode) + "`", false));
}
if (Settings.IncludeServerCode.Value && !string.IsNullOrEmpty(snap.LobbyCode))
{
list.Add(("Join Code", "`" + Clean(snap.LobbyCode) + "`", true));
}
if (!string.IsNullOrEmpty(snap.ChangeSummary))
{
list.Add(("Change", Clean(snap.ChangeSummary), false));
}
if (Settings.IncludePlayerList.Value && snap.Players.Count > 0)
{
list.Add(("In Lobby", PlayerListText(snap), false));
}
}
else if (!string.IsNullOrEmpty(snap.LevelTitle))
{
list.Add(("Last Map", Clean(snap.LevelTitle), true));
}
if (list.Count > 0)
{
stringBuilder.Append(",\"fields\":[");
for (int i = 0; i < list.Count; i++)
{
if (i > 0)
{
stringBuilder.Append(',');
}
stringBuilder.Append('{');
stringBuilder.Append("\"name\":").Append(Json.Quote(list[i].Item1));
stringBuilder.Append(",\"value\":").Append(Json.Quote(Trim(list[i].Item2, 1024)));
stringBuilder.Append(",\"inline\":").Append(list[i].Item3 ? "true" : "false");
stringBuilder.Append('}');
}
stringBuilder.Append(']');
}
string value = (string.IsNullOrEmpty(snap.LobbyName) ? "Fusion" : Clean(snap.LobbyName));
stringBuilder.Append(",\"footer\":{\"text\":").Append(Json.Quote(Trim(value, 2048))).Append('}');
stringBuilder.Append("}]}");
return stringBuilder.ToString();
}
private static string Title(SessionSnapshot snap)
{
string text = (string.IsNullOrEmpty(snap.LocalName) ? "Someone" : Clean(snap.LocalName));
return snap.Event switch
{
PresenceEvent.Joined => text + " joined a Fusion lobby",
PresenceEvent.Hosting => text + " is hosting a Fusion lobby",
PresenceEvent.Left => text + " left the lobby",
PresenceEvent.RosterChanged => "Lobby roster changed",
PresenceEvent.LevelChanged => "Lobby moved to a new map",
PresenceEvent.Test => "Fusion Presence test message",
_ => "Fusion Presence",
};
}
private static int Color(PresenceEvent kind)
{
return kind switch
{
PresenceEvent.Joined => 3908957,
PresenceEvent.Hosting => 5793266,
PresenceEvent.Left => 10070709,
PresenceEvent.RosterChanged => 16426522,
PresenceEvent.LevelChanged => 45300,
_ => 3092790,
};
}
private static string BuildFlat(SessionSnapshot snap)
{
StringBuilder stringBuilder = new StringBuilder(512);
stringBuilder.Append('{');
stringBuilder.Append("\"event\":").Append(Json.Quote(snap.Event.ToString().ToLowerInvariant()));
stringBuilder.Append(",\"timestamp\":").Append(Json.Quote(snap.TimestampUtc.ToString("o")));
stringBuilder.Append(",\"localPlayer\":").Append(Json.Quote(Clean(snap.LocalName)));
stringBuilder.Append(",\"isHost\":").Append(snap.IsHost ? "true" : "false");
stringBuilder.Append(",\"lobbyName\":").Append(Json.Quote(Clean(snap.LobbyName)));
stringBuilder.Append(",\"lobbyHost\":").Append(Json.Quote(Clean(snap.LobbyHostName)));
stringBuilder.Append(",\"privacy\":").Append(Json.Quote(snap.Privacy));
stringBuilder.Append(",\"levelTitle\":").Append(Json.Quote(Clean(snap.LevelTitle)));
stringBuilder.Append(",\"levelBarcode\":").Append(Json.Quote(snap.LevelBarcode));
stringBuilder.Append(",\"playerCount\":").Append(snap.PlayerCount);
stringBuilder.Append(",\"maxPlayers\":").Append(snap.MaxPlayers);
if (Settings.IncludeServerCode.Value)
{
stringBuilder.Append(",\"lobbyCode\":").Append(Json.Quote(snap.LobbyCode));
}
if (!string.IsNullOrEmpty(snap.ChangeSummary))
{
stringBuilder.Append(",\"change\":").Append(Json.Quote(Clean(snap.ChangeSummary)));
}
if (Settings.IncludePlayerList.Value)
{
stringBuilder.Append(",\"players\":[");
for (int i = 0; i < snap.Players.Count; i++)
{
SessionSnapshot.PlayerEntry playerEntry = snap.Players[i];
if (i > 0)
{
stringBuilder.Append(',');
}
stringBuilder.Append('{');
stringBuilder.Append("\"username\":").Append(Json.Quote(Clean(playerEntry.Username)));
if (Settings.IncludeNicknames.Value)
{
stringBuilder.Append(",\"nickname\":").Append(Json.Quote(Clean(playerEntry.Nickname)));
}
if (Settings.IncludePermissions.Value)
{
stringBuilder.Append(",\"permission\":").Append(Json.Quote(playerEntry.Permission));
}
stringBuilder.Append(",\"isHost\":").Append(playerEntry.IsHost ? "true" : "false");
stringBuilder.Append(",\"isYou\":").Append(playerEntry.IsMe ? "true" : "false");
stringBuilder.Append('}');
}
stringBuilder.Append(']');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
private static string PlayerListText(SessionSnapshot snap)
{
StringBuilder stringBuilder = new StringBuilder(256);
foreach (SessionSnapshot.PlayerEntry player in snap.Players)
{
string value = Clean(player.Username);
if (string.IsNullOrEmpty(value))
{
value = "(unnamed)";
}
stringBuilder.Append("• ").Append(value);
if (Settings.IncludeNicknames.Value)
{
string text = Clean(player.Nickname);
if (!string.IsNullOrEmpty(text) && !text.Equals(value, StringComparison.OrdinalIgnoreCase))
{
stringBuilder.Append(" *(").Append(text).Append(")*");
}
}
if (player.IsHost)
{
stringBuilder.Append(" — host");
}
if (player.IsMe)
{
stringBuilder.Append(" — you");
}
if (Settings.IncludePermissions.Value && !string.IsNullOrEmpty(player.Permission))
{
stringBuilder.Append(" `").Append(player.Permission).Append('`');
}
stringBuilder.Append('\n');
if (stringBuilder.Length > 960)
{
stringBuilder.Append("… list truncated");
break;
}
}
return stringBuilder.ToString().TrimEnd('\n');
}
private static string PlayerCountText(SessionSnapshot snap)
{
if (snap.MaxPlayers <= 0)
{
return snap.PlayerCount.ToString();
}
return $"{snap.PlayerCount} / {snap.MaxPlayers}";
}
private static string Clean(string value)
{
if (string.IsNullOrEmpty(value))
{
return "";
}
string text = RichText.Replace(value, "");
StringBuilder stringBuilder = new StringBuilder(text.Length + 8);
string text2 = text;
foreach (char c in text2)
{
if (c >= ' ')
{
bool flag;
switch (c)
{
case '\u007f':
break;
case '#':
case '*':
case '>':
case '@':
case '\\':
case '_':
case '`':
case '|':
case '~':
flag = true;
goto IL_00c0;
default:
{
flag = false;
goto IL_00c0;
}
IL_00c0:
if (flag)
{
stringBuilder.Append('\\');
}
stringBuilder.Append(c);
continue;
}
}
if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != ' ')
{
stringBuilder.Append(' ');
}
}
return stringBuilder.ToString().Trim();
}
private static string Trim(string value, int max)
{
if (string.IsNullOrEmpty(value))
{
return "";
}
if (value.Length > max)
{
return value.Substring(0, max - 1) + "…";
}
return value;
}
}
internal static class WebhookSender
{
private const int QueueCapacity = 24;
private static readonly HttpClient Client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(10.0)
};
private static BlockingCollection<string> _queue;
private static Thread _worker;
private static volatile bool _running;
public static string LastStatus { get; private set; } = "Nothing sent yet";
public static void Start()
{
if (!_running)
{
_queue = new BlockingCollection<string>(24);
_running = true;
_worker = new Thread(WorkerLoop)
{
Name = "FusionPresence.Webhook",
IsBackground = true
};
_worker.Start();
}
}
public static void Enqueue(string json)
{
if (_running && _queue != null)
{
if (!Settings.Enabled.Value)
{
LastStatus = "Skipped — mod disabled";
}
else if (!Settings.HasWebhook)
{
LastStatus = "Skipped — no webhook URL set";
Log.Warn("A presence event fired but no webhook URL is configured.");
}
else if (!_queue.TryAdd(json))
{
LastStatus = "Dropped — send queue full";
Log.Warn("Webhook queue is full, dropping this event.");
}
}
}
public static void Stop(int flushMilliseconds = 2000)
{
if (_running)
{
_running = false;
try
{
_queue?.CompleteAdding();
}
catch
{
}
try
{
_worker?.Join(flushMilliseconds);
}
catch
{
}
_worker = null;
_queue = null;
}
}
private static void WorkerLoop()
{
string empty = string.Empty;
try
{
foreach (string item in _queue.GetConsumingEnumerable())
{
empty = Settings.WebhookUrl.Value;
if (!string.IsNullOrWhiteSpace(empty))
{
Send(empty, item);
Thread.Sleep((int)(Math.Max(0.2f, Settings.MinSendSpacing.Value) * 1000f));
}
}
}
catch (InvalidOperationException)
{
}
catch (Exception ex2)
{
Log.Error($"Webhook worker died: {ex2}");
LastStatus = "Worker error — " + ex2.GetType().Name;
}
}
private static void Send(string url, string payload, int attempt = 0)
{
try
{
using StringContent content = new StringContent(payload, Encoding.UTF8, "application/json");
using HttpResponseMessage httpResponseMessage = Client.PostAsync(url, content).GetAwaiter().GetResult();
int statusCode = (int)httpResponseMessage.StatusCode;
if (statusCode == 429 && attempt < 3)
{
double num = 2.0;
if (httpResponseMessage.Headers.TryGetValues("Retry-After", out IEnumerable<string> values))
{
foreach (string item in values)
{
if (double.TryParse(item, out var result))
{
num = result;
break;
}
}
}
LastStatus = $"Rate limited, retrying in {num:0.#}s";
Log.Warn(LastStatus);
Thread.Sleep((int)(Math.Min(num, 30.0) * 1000.0));
Send(url, payload, attempt + 1);
}
else if (httpResponseMessage.IsSuccessStatusCode)
{
LastStatus = $"Sent OK ({statusCode}) at {DateTime.Now:HH:mm:ss}";
Log.Debug(LastStatus);
}
else
{
string value = SafeRead(httpResponseMessage);
LastStatus = $"Failed — HTTP {statusCode}";
Log.Error($"Webhook rejected the post: HTTP {statusCode}. {value}");
}
}
catch (Exception ex)
{
LastStatus = "Failed — " + ex.GetType().Name;
Log.Error("Webhook send failed: " + ex.Message);
}
}
private static string SafeRead(HttpResponseMessage response)
{
try
{
string result = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return (result.Length > 400) ? result.Substring(0, 400) : result;
}
catch
{
return "(no body)";
}
}
}
}
namespace FusionPresence.Menu
{
internal static class PresenceMenu
{
private static Page _root;
private static FunctionElement _statusElement;
private static FunctionElement _urlElement;
private static StringElement _urlInput;
public static void Create()
{
//IL_000a: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_0072: Unknown result type (might be due to invalid IL or missing references)
//IL_0084: Unknown result type (might be due to invalid IL or missing references)
//IL_00b8: 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_0152: Unknown result type (might be due to invalid IL or missing references)
//IL_0182: Unknown result type (might be due to invalid IL or missing references)
//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
//IL_01eb: Unknown result type (might be due to invalid IL or missing references)
//IL_022e: Unknown result type (might be due to invalid IL or missing references)
//IL_0240: Unknown result type (might be due to invalid IL or missing references)
//IL_027a: Unknown result type (might be due to invalid IL or missing references)
//IL_02b4: Unknown result type (might be due to invalid IL or missing references)
//IL_02ee: Unknown result type (might be due to invalid IL or missing references)
//IL_0327: Unknown result type (might be due to invalid IL or missing references)
//IL_0374: Unknown result type (might be due to invalid IL or missing references)
//IL_0386: Unknown result type (might be due to invalid IL or missing references)
//IL_03c0: Unknown result type (might be due to invalid IL or missing references)
//IL_03fa: Unknown result type (might be due to invalid IL or missing references)
//IL_0434: Unknown result type (might be due to invalid IL or missing references)
//IL_046e: Unknown result type (might be due to invalid IL or missing references)
//IL_04b6: Unknown result type (might be due to invalid IL or missing references)
//IL_0503: Unknown result type (might be due to invalid IL or missing references)
//IL_0515: Unknown result type (might be due to invalid IL or missing references)
//IL_055e: Unknown result type (might be due to invalid IL or missing references)
//IL_05a6: Unknown result type (might be due to invalid IL or missing references)
//IL_0602: Unknown result type (might be due to invalid IL or missing references)
//IL_0614: Unknown result type (might be due to invalid IL or missing references)
//IL_0649: Unknown result type (might be due to invalid IL or missing references)
//IL_0679: Unknown result type (might be due to invalid IL or missing references)
//IL_06a8: Unknown result type (might be due to invalid IL or missing references)
_root = Page.Root.CreatePage("Fusion Presence", Color.cyan, 0, true);
_root.CreateBool("Enabled", Color.white, Settings.Enabled.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.Enabled, v);
});
Page obj = _root.CreatePage("Webhook", new Color(0.4f, 0.8f, 1f), 0, true);
_urlElement = obj.CreateFunction(UrlLabel(), Color.grey, (Action)delegate
{
});
_urlInput = obj.CreateString("Set URL", Color.white, string.Empty, (Action<string>)delegate
{
CommitUrl();
});
StringElement urlInput = _urlInput;
((Element)urlInput).OnElementChanged = (Action)Delegate.Combine(((Element)urlInput).OnElementChanged, new Action(CommitUrl));
obj.CreateFunction("Save URL", Color.green, (Action)CommitUrl);
obj.CreateFunction("Clear URL", Color.red, (Action)delegate
{
Settings.WebhookUrl.Value = string.Empty;
Settings.Cat.SaveToFile(false);
if (_urlInput != null)
{
_urlInput.Value = string.Empty;
}
if (_urlElement != null)
{
((Element)_urlElement).ElementName = UrlLabel();
}
RefreshStatus();
});
obj.CreateBool("Discord embed format", Color.white, Settings.DiscordEmbed.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.DiscordEmbed, v);
});
obj.CreateFunction("Send test message", Color.green, (Action)delegate
{
PresenceTracker.SendTest();
RefreshStatus();
});
_statusElement = obj.CreateFunction(StatusLabel(), Color.grey, (Action)RefreshStatus);
Page obj2 = _root.CreatePage("Events", new Color(0.6f, 1f, 0.6f), 0, true);
obj2.CreateBool("On joining a lobby", Color.white, Settings.ReportJoin.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.ReportJoin, v);
});
obj2.CreateBool("On hosting", Color.white, Settings.ReportHost.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.ReportHost, v);
});
obj2.CreateBool("On leaving", Color.white, Settings.ReportLeave.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.ReportLeave, v);
});
obj2.CreateBool("On player join / leave", Color.white, Settings.ReportRoster.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.ReportRoster, v);
});
obj2.CreateBool("On map change", Color.white, Settings.ReportLevel.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.ReportLevel, v);
});
Page obj3 = _root.CreatePage("Message Contents", new Color(1f, 0.85f, 0.4f), 0, true);
obj3.CreateBool("Include your own name", Color.white, Settings.IncludeOwnName.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.IncludeOwnName, v);
});
obj3.CreateBool("Player list", Color.white, Settings.IncludePlayerList.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.IncludePlayerList, v);
});
obj3.CreateBool("Nicknames", Color.white, Settings.IncludeNicknames.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.IncludeNicknames, v);
});
obj3.CreateBool("Permission levels", Color.white, Settings.IncludePermissions.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.IncludePermissions, v);
});
obj3.CreateBool("Level barcode", Color.white, Settings.IncludeLevelBarcode.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.IncludeLevelBarcode, v);
});
obj3.CreateBool("[!] Lobby join code", new Color(1f, 0.5f, 0.5f), Settings.IncludeServerCode.Value, (Action<bool>)delegate(bool v)
{
Set(Settings.IncludeServerCode, v);
if (v)
{
Log.Warn("Join code will now be posted. Anyone who can read that webhook target can join your lobby.");
}
});
Page obj4 = _root.CreatePage("Timing", new Color(0.8f, 0.8f, 0.8f), 0, true);
obj4.CreateFloat("Join delay (s)", Color.white, Settings.JoinDelay.Value, 0.5f, 0f, 15f, (Action<float>)delegate(float v)
{
Set(Settings.JoinDelay, v);
});
obj4.CreateFloat("Roster debounce (s)", Color.white, Settings.RosterDebounce.Value, 0.5f, 0.5f, 30f, (Action<float>)delegate(float v)
{
Set(Settings.RosterDebounce, v);
});
obj4.CreateFloat("Min send gap (s)", Color.white, Settings.MinSendSpacing.Value, 0.1f, 0.2f, 10f, (Action<float>)delegate(float v)
{
Set(Settings.MinSendSpacing, v);
});
Page obj5 = _root.CreatePage("Diagnostics", new Color(1f, 0.6f, 0.2f), 0, true);
obj5.CreateBool("Verbose logging", Color.white, Log.Verbose, (Action<bool>)delegate(bool v)
{
Log.Verbose = v;
});
obj5.CreateFunction("Check Fusion link", Color.white, (Action)delegate
{
bool flag = FusionBridge.IsFusionPresent();
bool subscribed = FusionBridge.Subscribed;
bool value = flag && FusionBridge.HasServer();
bool value2 = flag && FusionBridge.LobbyInfoReady();
Log.Msg($"Fusion loaded: {flag} | hooks attached: {subscribed} | in a session: {value} | lobby info received: {value2}");
});
obj5.CreateFunction("Save settings now", Color.white, (Action)delegate
{
Settings.Cat.SaveToFile(false);
});
obj5.CreateFunction("Dump settings to log", Color.white, (Action)delegate
{
Log.Msg("--- FusionPresence live settings ---");
foreach (MelonPreferences_Entry entry in Settings.Cat.Entries)
{
if (!(entry.Identifier == ((MelonPreferences_Entry)Settings.WebhookUrl).Identifier))
{
Log.Msg(" " + entry.Identifier + " = " + entry.GetValueAsString());
}
}
Log.Msg($" (webhook set: {Settings.HasWebhook})");
Log.Msg("------------------------------------");
});
}
public static void Destroy()
{
if (_root != null)
{
try
{
Page.Root.RemovePage(_root);
}
catch
{
}
_root = null;
_statusElement = null;
_urlElement = null;
_urlInput = null;
}
}
private static void CommitUrl()
{
if (_urlInput == null)
{
return;
}
string text = (_urlInput.Value ?? string.Empty).Trim();
if (!(text == Settings.WebhookUrl.Value))
{
Settings.WebhookUrl.Value = text;
Settings.Cat.SaveToFile(false);
if (_urlElement != null)
{
((Element)_urlElement).ElementName = UrlLabel();
}
RefreshStatus();
Log.Msg(string.IsNullOrEmpty(text) ? "Webhook URL cleared." : "Webhook URL saved.");
}
}
private static void Set<T>(MelonPreferences_Entry<T> entry, T value)
{
entry.Value = value;
Settings.Cat.SaveToFile(false);
Log.Debug($"{((MelonPreferences_Entry)entry).Identifier} = {value}");
}
private static void RefreshStatus()
{
if (_statusElement != null)
{
((Element)_statusElement).ElementName = StatusLabel();
}
}
private static string StatusLabel()
{
return "Status: " + WebhookSender.LastStatus;
}
private static string UrlLabel()
{
string value = Settings.WebhookUrl.Value;
if (string.IsNullOrWhiteSpace(value))
{
return "URL: (not set)";
}
string text = ((value.Length > 6) ? value.Substring(value.Length - 6) : value);
return "URL: set (…" + text + ")";
}
}
}
namespace FusionPresence.Config
{
internal static class Settings
{
private const string Category = "FusionPresence";
public static MelonPreferences_Category Cat { get; private set; }
public static MelonPreferences_Entry<bool> Enabled { get; private set; }
public static MelonPreferences_Entry<string> WebhookUrl { get; private set; }
public static MelonPreferences_Entry<bool> DiscordEmbed { get; private set; }
public static MelonPreferences_Entry<string> BotName { get; private set; }
public static MelonPreferences_Entry<bool> ReportJoin { get; private set; }
public static MelonPreferences_Entry<bool> ReportHost { get; private set; }
public static MelonPreferences_Entry<bool> ReportLeave { get; private set; }
public static MelonPreferences_Entry<bool> ReportRoster { get; private set; }
public static MelonPreferences_Entry<bool> ReportLevel { get; private set; }
public static MelonPreferences_Entry<bool> IncludeOwnName { get; private set; }
public static MelonPreferences_Entry<bool> IncludePlayerList { get; private set; }
public static MelonPreferences_Entry<bool> IncludeNicknames { get; private set; }
public static MelonPreferences_Entry<bool> IncludePermissions { get; private set; }
public static MelonPreferences_Entry<bool> IncludeLevelBarcode { get; private set; }
public static MelonPreferences_Entry<bool> IncludeServerCode { get; private set; }
public static MelonPreferences_Entry<float> JoinDelay { get; private set; }
public static MelonPreferences_Entry<float> RosterDebounce { get; private set; }
public static MelonPreferences_Entry<float> MinSendSpacing { get; private set; }
public static bool HasWebhook
{
get
{
if (!string.IsNullOrWhiteSpace(WebhookUrl.Value))
{
return WebhookUrl.Value.StartsWith("http", StringComparison.OrdinalIgnoreCase);
}
return false;
}
}
public static void Register()
{
Cat = MelonPreferences.CreateCategory("FusionPresence", "Fusion Presence");
Enabled = Cat.CreateEntry<bool>("Enabled", true, (string)null, "Master switch. When false nothing is ever sent.", false, false, (ValueValidator)null, (string)null);
WebhookUrl = Cat.CreateEntry<string>("WebhookUrl", string.Empty, (string)null, "Full webhook URL. Discord webhooks look like https://discord.com/api/webhooks/<id>/<token>", false, false, (ValueValidator)null, (string)null);
DiscordEmbed = Cat.CreateEntry<bool>("DiscordEmbed", true, (string)null, "true = Discord embed payload. false = flat JSON, for your own endpoint.", false, false, (ValueValidator)null, (string)null);
BotName = Cat.CreateEntry<string>("BotName", "Fusion Presence", (string)null, "Display name on the Discord post. Ignored in flat JSON mode.", false, false, (ValueValidator)null, (string)null);
ReportJoin = Cat.CreateEntry<bool>("ReportJoin", true, (string)null, "Post when you join someone else's lobby.", false, false, (ValueValidator)null, (string)null);
ReportHost = Cat.CreateEntry<bool>("ReportHost", true, (string)null, "Post when you start hosting.", false, false, (ValueValidator)null, (string)null);
ReportLeave = Cat.CreateEntry<bool>("ReportLeave", true, (string)null, "Post when you disconnect.", false, false, (ValueValidator)null, (string)null);
ReportRoster = Cat.CreateEntry<bool>("ReportRoster", false, (string)null, "Post when players join or leave while you're in a lobby.", false, false, (ValueValidator)null, (string)null);
ReportLevel = Cat.CreateEntry<bool>("ReportLevel", true, (string)null, "Post when the lobby changes level.", false, false, (ValueValidator)null, (string)null);
IncludeOwnName = Cat.CreateEntry<bool>("IncludeOwnName", true, (string)null, "Include your own username. Turn off to report anonymously.", false, false, (ValueValidator)null, (string)null);
IncludePlayerList = Cat.CreateEntry<bool>("IncludePlayerList", true, (string)null, "List everyone in the lobby by name.", false, false, (ValueValidator)null, (string)null);
IncludeNicknames = Cat.CreateEntry<bool>("IncludeNicknames", true, (string)null, "Show nicknames alongside usernames when they differ.", false, false, (ValueValidator)null, (string)null);
IncludePermissions = Cat.CreateEntry<bool>("IncludePermissions", false, (string)null, "Tag each player with their permission level.", false, false, (ValueValidator)null, (string)null);
IncludeLevelBarcode = Cat.CreateEntry<bool>("IncludeLevelBarcode", false, (string)null, "Include the raw level barcode, not just the title.", false, false, (ValueValidator)null, (string)null);
IncludeServerCode = Cat.CreateEntry<bool>("IncludeServerCode", false, (string)null, "Include the lobby join code. OFF by default - anyone who can read the webhook target could join your lobby.", false, false, (ValueValidator)null, (string)null);
JoinDelay = Cat.CreateEntry<float>("JoinDelaySeconds", 4f, (string)null, "Wait this long after joining before posting, so the host's lobby info has arrived.", false, false, (ValueValidator)null, (string)null);
RosterDebounce = Cat.CreateEntry<float>("RosterDebounceSeconds", 6f, (string)null, "Coalesce a burst of joins/leaves into one post.", false, false, (ValueValidator)null, (string)null);
MinSendSpacing = Cat.CreateEntry<float>("MinSendSpacingSeconds", 1.5f, (string)null, "Minimum gap between outgoing requests. Keeps you under Discord's webhook rate limit.", false, false, (ValueValidator)null, (string)null);
}
}
}