using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text.Json; using BoneLib.BoneMenu; using LabFusion.Network; using LabFusion.Network.Serialization; using LabFusion.Player; using LabFusion.SDK.Modules; using LabFusion.UI.Popups; using LabFusion.Utilities; using LabLink; using LabLink.Data; using LabLink.Net; using LabLink.Runtime; using LabLink.UI; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using Steamworks; using Steamworks.Data; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(ModMain), "LabLink", "1.1.0", "Dynamic Team", null)] [assembly: MelonGame("Stress Level Zero", "BONELAB")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("LabLink")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0")] [assembly: AssemblyProduct("LabLink")] [assembly: AssemblyTitle("LabLink")] [assembly: AssemblyVersion("1.1.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 LabLink { public class ModMain : MelonMod { public static Instance Log { get; private set; } public override void OnInitializeMelon() { Log = ((MelonBase)this).LoggerInstance; Settings.Init(); FriendStore.Load(); Menu.Build(); try { ModuleManager.RegisterModule(); Log.Msg("LabLink Fusion module registered."); } catch (Exception ex) { Log.Warning("Fusion module not registered (LabFusion missing?): " + ex.Message); } LobbyTracker.Hook(); Log.Msg("LabLink loaded."); } } public static class Settings { public const string Category = "LabLink"; public static MelonPreferences_Category Cat { get; private set; } public static MelonPreferences_Entry PresenceToasts { get; private set; } public static MelonPreferences_Entry PopupOnRequest { get; private set; } public static MelonPreferences_Entry PopupOnMessage { get; private set; } public static MelonPreferences_Entry MessagesFromFriendsOnly { get; private set; } public static MelonPreferences_Entry PopupDuration { get; private set; } public static MelonPreferences_Entry ComposedMessage { get; private set; } public static void Init() { Cat = MelonPreferences.CreateCategory("LabLink", "LabLink"); PresenceToasts = Cat.CreateEntry("PresenceToasts", true, "Toast when a friend joins your lobby", (string)null, false, false, (ValueValidator)null, (string)null); PopupOnRequest = Cat.CreateEntry("PopupOnRequest", true, "Popup when a friend request arrives", (string)null, false, false, (ValueValidator)null, (string)null); PopupOnMessage = Cat.CreateEntry("PopupOnMessage", true, "Popup when a message arrives", (string)null, false, false, (ValueValidator)null, (string)null); MessagesFromFriendsOnly = Cat.CreateEntry("MessagesFromFriendsOnly", true, "Only accept messages from friends", (string)null, false, false, (ValueValidator)null, (string)null); PopupDuration = Cat.CreateEntry("PopupDuration", 5f, "Popup duration in seconds", (string)null, false, false, (ValueValidator)null, (string)null); ComposedMessage = Cat.CreateEntry("ComposedMessage", "", "Current composed message", (string)null, false, false, (ValueValidator)null, (string)null); try { if (PopupDuration.Value < 1f || PopupDuration.Value > 30f) { PopupDuration.Value = 5f; } } catch { } Cat.SaveToFile(false); } public static void Save() { MelonPreferences_Category cat = Cat; if (cat != null) { cat.SaveToFile(false); } } } } namespace LabLink.UI { public static class Menu { private static readonly Color _green = new Color(0.3f, 0.85f, 0.4f); private static readonly Color _cyan = new Color(0.2f, 0.8f, 0.9f); private static readonly Color _gold = new Color(1f, 0.84f, 0f); private static readonly Color _gray = new Color(0.6f, 0.6f, 0.6f); private static readonly Color _red = new Color(0.9f, 0.3f, 0.3f); private static readonly Color _blue = new Color(0.3f, 0.6f, 1f); private static Page _root; private static Page _friendsPage; private static Page _lobbyPage; private static Page _requestsPage; private static Page _messagesPage; private static Page _blockedPage; private static Page _recipientPage; private static Page _inboxPage; private static readonly string[] _quick = new string[6] { "gg", "follow me", "help!", "afk", "nice", "lol" }; public static void Build() { //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_0040: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) _root = Page.Root.CreatePage("LabLink", _green, 0, true); _friendsPage = _root.CreatePage("Friends", _green, 0, true); _lobbyPage = _root.CreatePage("Lobby", _blue, 0, true); _requestsPage = _root.CreatePage("Requests", _gold, 0, true); _messagesPage = _root.CreatePage("Messages", _cyan, 0, true); _blockedPage = _root.CreatePage("Blocked", _red, 0, true); BuildSettingsPage(_root); RefreshDynamic(); ModMain.Log.Msg("LabLink menu built."); } public static void RefreshDynamic() { try { RebuildFriends(); RebuildLobby(); RebuildRequests(); RebuildMessages(); RebuildBlocked(); } catch (Exception ex) { Instance log = ModMain.Log; if (log != null) { log.Warning("Menu refresh failed: " + ex.Message); } } } private static void RebuildFriends() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Unknown result type (might be due to invalid IL or missing references) Page friendsPage = _friendsPage; if (friendsPage == null) { return; } friendsPage.RemoveAll(); friendsPage.CreateFunction("Refresh", _green, (Action)RefreshDynamic); IReadOnlyList friends = FriendStore.Friends; if (friends.Count == 0) { friendsPage.CreateFunction("(no friends yet - add from Lobby)", _gray, (Action)delegate { }); return; } List list = new List(friends); list.Sort((Friend a, Friend b) => (a.Favorite != b.Favorite) ? ((!a.Favorite) ? 1 : (-1)) : string.Compare(a.Display(), b.Display(), StringComparison.OrdinalIgnoreCase)); foreach (Friend item in list) { ulong platformId = item.PlatformId; FriendPresence pres = SteamPresence.Get(platformId); bool flag = pres.Status != PresenceStatus.Offline && pres.Status != PresenceStatus.Unknown; string value = (item.Favorite ? "* " : ""); BuildFriendActions(friendsPage.CreatePage($"{value}{item.Display()} [{pres.Label}]", flag ? _green : _gray, 0, true), platformId, pres); } } private static void BuildFriendActions(Page page, ulong id, FriendPresence pres) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00be: 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) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) Friend f = FriendStore.GetFriend(id); if (f == null) { page.CreateFunction("(removed)", _gray, (Action)delegate { }); return; } if (pres.Status == PresenceStatus.InLobby) { page.CreateFunction("Join their lobby", _green, (Action)delegate { string message = SteamPresence.JoinFriend(id); Social.Toast("LabLink", message, (NotificationType)0); }); } bool flag = pres.Status == PresenceStatus.InYourLobby; page.CreateFunction(flag ? "Set as message recipient" : "Message (needs same lobby)", flag ? _cyan : _gray, (Action)delegate { Social.CurrentRecipient = id; Social.Toast("LabLink", "Recipient set to " + f.Display() + ". Open Messages.", (NotificationType)0); }); page.CreateString("Nickname", _gold, f.Nickname ?? "", (Action)delegate(string v) { f.Nickname = v ?? ""; FriendStore.Save(); }); page.CreateString("Note", _gold, f.Note ?? "", (Action)delegate(string v) { f.Note = v ?? ""; FriendStore.Save(); }); page.CreateBool("Favorite", _gold, f.Favorite, (Action)delegate(bool v) { f.Favorite = v; FriendStore.Save(); }); Page obj = page.CreatePage("Remove / Block", _red, 0, true); obj.CreateFunction("Unfriend", _red, (Action)delegate { Social.Unfriend(id); }); obj.CreateFunction("Block", _red, (Action)delegate { Social.Block(id); }); } private static void RebuildLobby() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_01f4: 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_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) Page lobbyPage = _lobbyPage; if (lobbyPage == null) { return; } lobbyPage.RemoveAll(); lobbyPage.CreateFunction("Refresh", _green, (Action)RefreshDynamic); if (!Fusion.HasServer) { lobbyPage.CreateFunction("(not in a lobby)", _gray, (Action)delegate { }); return; } int num = 0; foreach (Fusion.PlayerEntry player in Fusion.GetPlayers()) { if (player.IsLocal) { continue; } num++; ulong id = player.PlatformId; string name = player.Name; if (FriendStore.IsBlocked(id)) { lobbyPage.CreateFunction(name + " [blocked]", _gray, (Action)delegate { }); } else if (FriendStore.IsFriend(id)) { lobbyPage.CreateFunction(name + " [friend]", _green, (Action)delegate { }); } else if (LobbyTracker.HasMod(id)) { lobbyPage.CreateFunction("Add Friend: " + name, _cyan, (Action)delegate { Social.SendFriendRequest(id, name); }); } else { lobbyPage.CreateFunction(name + " (no LabLink)", _gray, (Action)delegate { }); } } if (num == 0) { lobbyPage.CreateFunction("(no other players here)", _gray, (Action)delegate { }); } } private static void RebuildRequests() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_007f: 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_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: Unknown result type (might be due to invalid IL or missing references) //IL_020c: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Unknown result type (might be due to invalid IL or missing references) Page requestsPage = _requestsPage; if (requestsPage == null) { return; } requestsPage.RemoveAll(); requestsPage.CreateFunction("Refresh", _green, (Action)RefreshDynamic); IReadOnlyList incoming = FriendStore.Incoming; IReadOnlyList outgoing = FriendStore.Outgoing; requestsPage.CreateFunction($"-- Incoming ({incoming.Count}) --", _gold, (Action)delegate { }); if (incoming.Count == 0) { requestsPage.CreateFunction("(none)", _gray, (Action)delegate { }); } foreach (PendingRequest item in incoming) { ulong id = item.PlatformId; Page obj = requestsPage.CreatePage("From " + item.Display(), _gold, 0, true); obj.CreateFunction("Accept", _green, (Action)delegate { Social.AcceptRequest(id); }); obj.CreateFunction("Decline", _red, (Action)delegate { Social.DeclineRequest(id); }); obj.CreateFunction("Block", _red, (Action)delegate { Social.Block(id); }); } requestsPage.CreateFunction($"-- Outgoing ({outgoing.Count}) --", _cyan, (Action)delegate { }); if (outgoing.Count == 0) { requestsPage.CreateFunction("(none)", _gray, (Action)delegate { }); } foreach (PendingRequest item2 in outgoing) { string text = (Fusion.IsOnline(item2.PlatformId) ? "pending" : "pending (offline)"); requestsPage.CreateFunction(item2.Display() + " - " + text, _gray, (Action)delegate { }); } } private static void RebuildMessages() { //IL_0016: 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) Page messagesPage = _messagesPage; if (messagesPage != null) { messagesPage.RemoveAll(); messagesPage.CreateFunction("Refresh", _green, (Action)RefreshDynamic); string text = ((Social.CurrentRecipient == 0L) ? "none" : (FriendStore.GetFriend(Social.CurrentRecipient)?.Display() ?? Fusion.NameOf(Social.CurrentRecipient) ?? "unknown")); messagesPage.CreateFunction("To: " + text, _gold, (Action)delegate { }); BuildComposePage(messagesPage); BuildRecipientPage(messagesPage); BuildQuickSendPage(messagesPage); BuildInboxPage(messagesPage); } } private static void BuildComposePage(Page parent) { //IL_0006: 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_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) Page obj = parent.CreatePage("Compose & Send", _cyan, 0, true); obj.CreateString("Type Message (VR keyboard)", _gold, Settings.ComposedMessage.Value, (Action)delegate(string v) { Settings.ComposedMessage.Value = v ?? ""; Settings.Save(); }); obj.CreateFunction("Send", _green, (Action)delegate { Social.SendMessageTo(Social.CurrentRecipient, Settings.ComposedMessage.Value); }); obj.CreateFunction("Clear", _gray, (Action)delegate { Settings.ComposedMessage.Value = ""; Settings.Save(); }); } private static void BuildRecipientPage(Page parent) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) _recipientPage = parent.CreatePage("Choose Recipient", _blue, 0, true); RebuildRecipient(); } private static void RebuildRecipient() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) Page recipientPage = _recipientPage; if (recipientPage == null) { return; } recipientPage.RemoveAll(); recipientPage.CreateFunction("Refresh", _green, (Action)RebuildRecipient); int num = 0; foreach (Friend friend in FriendStore.Friends) { if (Fusion.IsOnline(friend.PlatformId)) { ulong id = friend.PlatformId; string label = friend.Display(); recipientPage.CreateFunction(label, _cyan, (Action)delegate { Social.CurrentRecipient = id; Social.Toast("LabLink", "Recipient: " + label, (NotificationType)0); RebuildMessages(); }); num++; } } if (num == 0) { recipientPage.CreateFunction("(no friends online)", _gray, (Action)delegate { }); } } private static void BuildQuickSendPage(Page parent) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) Page val = parent.CreatePage("Quick Send", _green, 0, true); string[] quick = _quick; foreach (string text in quick) { string m = text; val.CreateFunction(m, _green, (Action)delegate { Social.SendMessageTo(Social.CurrentRecipient, m); }); } } private static void BuildInboxPage(Page parent) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) _inboxPage = parent.CreatePage("Inbox", _gray, 0, true); RebuildInbox(); } private static void RebuildInbox() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) Page inboxPage = _inboxPage; if (inboxPage == null) { return; } inboxPage.RemoveAll(); inboxPage.CreateFunction("Refresh", _green, (Action)RebuildInbox); inboxPage.CreateFunction("Clear", _gray, (Action)delegate { Social.ClearInbox(); RebuildInbox(); }); IReadOnlyList inbox = Social.Inbox; if (inbox.Count == 0) { inboxPage.CreateFunction("(no messages)", _gray, (Action)delegate { }); return; } for (int num = inbox.Count - 1; num >= 0; num--) { Social.InboxEntry inboxEntry = inbox[num]; inboxPage.CreateFunction(inboxEntry.Name + ": " + Trunc(inboxEntry.Body, 28), _gray, (Action)delegate { }); } } private static void RebuildBlocked() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) Page blockedPage = _blockedPage; if (blockedPage == null) { return; } blockedPage.RemoveAll(); blockedPage.CreateFunction("Refresh", _green, (Action)RefreshDynamic); IReadOnlyList blocked = FriendStore.Blocked; if (blocked.Count == 0) { blockedPage.CreateFunction("(no one blocked)", _gray, (Action)delegate { }); return; } foreach (ulong item in blocked) { ulong bid = item; string text = Fusion.NameOf(bid) ?? $"Player {bid}"; blockedPage.CreateFunction("Unblock " + text, _green, (Action)delegate { FriendStore.Unblock(bid); RefreshDynamic(); }); } } private static void BuildSettingsPage(Page parent) { //IL_0006: 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_0052: Unknown result type (might be due to invalid IL or missing references) //IL_008c: 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_00ff: Unknown result type (might be due to invalid IL or missing references) Page obj = parent.CreatePage("Settings", _gray, 0, true); obj.CreateBool("Friend-join toasts", _cyan, Settings.PresenceToasts.Value, (Action)delegate(bool v) { Settings.PresenceToasts.Value = v; Settings.Save(); }); obj.CreateBool("Popup on friend request", _cyan, Settings.PopupOnRequest.Value, (Action)delegate(bool v) { Settings.PopupOnRequest.Value = v; Settings.Save(); }); obj.CreateBool("Popup on message", _cyan, Settings.PopupOnMessage.Value, (Action)delegate(bool v) { Settings.PopupOnMessage.Value = v; Settings.Save(); }); obj.CreateBool("Messages from friends only", _cyan, Settings.MessagesFromFriendsOnly.Value, (Action)delegate(bool v) { Settings.MessagesFromFriendsOnly.Value = v; Settings.Save(); }); obj.CreateFloat("Popup duration (s)", _gold, Settings.PopupDuration.Value, 1f, 1f, 30f, (Action)delegate(float v) { Settings.PopupDuration.Value = v; Settings.Save(); }); } private static string Trunc(string s, int max) { if (string.IsNullOrEmpty(s)) { return "(empty)"; } if (s.Length > max) { return s.Substring(0, max - 1) + "…"; } return s; } } } namespace LabLink.Runtime { public static class Fusion { public struct PlayerEntry { public ulong PlatformId; public byte SmallId; public string Name; public bool IsLocal; public bool IsHost; } public static bool HasServer { get { try { return NetworkInfo.HasServer; } catch { return false; } } } public static bool IsHost { get { try { return NetworkInfo.IsHost; } catch { return false; } } } public static ulong LocalPlatformId { get { try { return PlayerIDManager.LocalPlatformID; } catch { return 0uL; } } } public static string LocalName() { try { return NameOf(PlayerIDManager.LocalID) ?? "You"; } catch { return "You"; } } public static List GetPlayers() { List list = new List(); if (!HasServer) { return list; } try { ulong localPlatformId = LocalPlatformId; foreach (PlayerID playerID in PlayerIDManager.PlayerIDs) { if (playerID != null) { list.Add(new PlayerEntry { PlatformId = playerID.PlatformID, SmallId = playerID.SmallID, Name = (NameOf(playerID) ?? $"Player {playerID.SmallID}"), IsLocal = (playerID.PlatformID == localPlatformId), IsHost = playerID.IsHost }); } } } catch (Exception ex) { ModMain.Log.Warning("Player list failed: " + ex.Message); } return list; } public static string NameOf(PlayerID pid) { if (pid == null) { return null; } try { string valueOrEmpty = pid.Metadata.Nickname.GetValueOrEmpty(); if (!string.IsNullOrEmpty(valueOrEmpty)) { return valueOrEmpty; } string valueOrEmpty2 = pid.Metadata.Username.GetValueOrEmpty(); if (!string.IsNullOrEmpty(valueOrEmpty2)) { return valueOrEmpty2; } } catch { } return null; } public static string NameOf(ulong platformId) { try { return NameOf(PlayerIDManager.GetPlayerID(platformId)); } catch { return null; } } public static bool IsOnline(ulong platformId) { try { return PlayerIDManager.GetPlayerID(platformId) != null; } catch { return false; } } } public static class LobbyTracker { [CompilerGenerated] private static class <>O { public static ServerEvent <0>__HandleSessionStart; public static ServerEvent <1>__HandleSessionEnd; public static PlayerUpdate <2>__HandlePlayerJoined; public static PlayerUpdate <3>__HandlePlayerLeft; } private static readonly HashSet _modUsers = new HashSet(); private static bool _hooked; public static bool HasMod(ulong platformId) { return _modUsers.Contains(platformId); } public static void MarkModUser(ulong platformId, string name) { if (platformId != 0L && platformId != Fusion.LocalPlatformId) { _modUsers.Add(platformId); } } public static void Hook() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown //IL_0039: 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_0044: Expected O, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Expected O, but got Unknown if (_hooked) { return; } try { object obj = <>O.<0>__HandleSessionStart; if (obj == null) { ServerEvent val = HandleSessionStart; <>O.<0>__HandleSessionStart = val; obj = (object)val; } MultiplayerHooking.OnJoinedServer += (ServerEvent)obj; object obj2 = <>O.<0>__HandleSessionStart; if (obj2 == null) { ServerEvent val2 = HandleSessionStart; <>O.<0>__HandleSessionStart = val2; obj2 = (object)val2; } MultiplayerHooking.OnStartedServer += (ServerEvent)obj2; object obj3 = <>O.<1>__HandleSessionEnd; if (obj3 == null) { ServerEvent val3 = HandleSessionEnd; <>O.<1>__HandleSessionEnd = val3; obj3 = (object)val3; } MultiplayerHooking.OnDisconnected += (ServerEvent)obj3; object obj4 = <>O.<2>__HandlePlayerJoined; if (obj4 == null) { PlayerUpdate val4 = HandlePlayerJoined; <>O.<2>__HandlePlayerJoined = val4; obj4 = (object)val4; } MultiplayerHooking.OnPlayerJoined += (PlayerUpdate)obj4; object obj5 = <>O.<3>__HandlePlayerLeft; if (obj5 == null) { PlayerUpdate val5 = HandlePlayerLeft; <>O.<3>__HandlePlayerLeft = val5; obj5 = (object)val5; } MultiplayerHooking.OnPlayerLeft += (PlayerUpdate)obj5; _hooked = true; } catch (Exception ex) { ModMain.Log.Warning("LabLink hooks failed: " + ex.Message); } } private static void HandleSessionStart() { _modUsers.Clear(); LinkNet.Broadcast(LinkKind.CapabilityPing); Menu.RefreshDynamic(); } private static void HandleSessionEnd() { _modUsers.Clear(); Menu.RefreshDynamic(); } private static void HandlePlayerJoined(PlayerID id) { LinkNet.Broadcast(LinkKind.CapabilityPing); try { if (id != null && Settings.PresenceToasts.Value && FriendStore.IsFriend(id.PlatformID)) { string text = FriendStore.GetFriend(id.PlatformID)?.Display() ?? Fusion.NameOf(id) ?? "A friend"; Social.Toast("Friend online", text + " joined the lobby.", (NotificationType)3); } } catch { } Menu.RefreshDynamic(); } private static void HandlePlayerLeft(PlayerID id) { try { if (id != null) { _modUsers.Remove(id.PlatformID); } } catch { } Menu.RefreshDynamic(); } } public static class Social { public struct InboxEntry { public float Time; public ulong FromId; public string Name; public string Body; } private const int InboxCapacity = 20; private static readonly List _inbox = new List(); public static IReadOnlyList Inbox => _inbox; public static ulong CurrentRecipient { get; set; } public static void OnReceive(LinkData data) { if (data == null) { return; } ulong fromId = data.FromId; if (fromId == Fusion.LocalPlatformId || (data.ToId != 0L && data.ToId != Fusion.LocalPlatformId) || FriendStore.IsBlocked(fromId)) { return; } string text = ((!string.IsNullOrEmpty(data.FromName)) ? data.FromName : (Fusion.NameOf(fromId) ?? $"Player {fromId}")); FriendStore.UpdateName(fromId, text); switch ((LinkKind)data.Kind) { case LinkKind.CapabilityPing: LobbyTracker.MarkModUser(fromId, text); LinkNet.Send(LinkKind.CapabilityAck, fromId); Menu.RefreshDynamic(); break; case LinkKind.CapabilityAck: LobbyTracker.MarkModUser(fromId, text); Menu.RefreshDynamic(); break; case LinkKind.FriendRequest: OnFriendRequest(fromId, text); break; case LinkKind.RequestAccept: OnRequestAccepted(fromId, text); break; case LinkKind.RequestDecline: FriendStore.RemoveOutgoing(fromId); Toast("Request declined", text + " declined your friend request.", (NotificationType)0); Menu.RefreshDynamic(); break; case LinkKind.DirectMessage: OnDirectMessage(fromId, text, data.Body); break; case LinkKind.Unfriend: if (FriendStore.IsFriend(fromId)) { FriendStore.RemoveFriend(fromId); Toast("Unfriended", text + " removed you as a friend.", (NotificationType)0); Menu.RefreshDynamic(); } break; } } private static void OnFriendRequest(ulong from, string name) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Expected O, but got Unknown if (FriendStore.IsFriend(from)) { LinkNet.Send(LinkKind.RequestAccept, from); return; } FriendStore.AddIncoming(from, name); Menu.RefreshDynamic(); if (Settings.PopupOnRequest.Value) { ulong who = from; Notify(new Notification { Title = NotificationText.op_Implicit("Friend request"), Message = NotificationText.op_Implicit(name + " wants to add you. Accept?"), Type = (NotificationType)0, PopupLength = Mathf.Clamp(Settings.PopupDuration.Value + 5f, 5f, 30f), ShowPopup = true, SaveToMenu = true, OnAccepted = delegate { AcceptRequest(who); }, OnDeclined = delegate { DeclineRequest(who); } }); } } private static void OnRequestAccepted(ulong from, string name) { bool num = FriendStore.GetOutgoing(from) != null; FriendStore.AddFriend(from, name); if (num) { Toast("Friend added", name + " accepted your friend request.", (NotificationType)3); } Menu.RefreshDynamic(); } private static void OnDirectMessage(ulong from, string name, string body) { if (!Settings.MessagesFromFriendsOnly.Value || FriendStore.IsFriend(from)) { RecordInbox(from, name, body); if (Settings.PopupOnMessage.Value) { Toast(name, string.IsNullOrEmpty(body) ? "(empty)" : body, (NotificationType)0); } Menu.RefreshDynamic(); } } public static void SendFriendRequest(ulong toId, string name) { if (!Fusion.HasServer) { Toast("LabLink", "You're not in a lobby.", (NotificationType)1); } else if (toId != 0L && toId != Fusion.LocalPlatformId) { if (FriendStore.IsFriend(toId)) { Toast("LabLink", "Already friends.", (NotificationType)0); return; } FriendStore.AddOutgoing(toId, name); LinkNet.Send(LinkKind.FriendRequest, toId); Toast("Request sent", "Friend request sent to " + name + ".", (NotificationType)3); Menu.RefreshDynamic(); } } public static void AcceptRequest(ulong fromId) { PendingRequest incoming = FriendStore.GetIncoming(fromId); string text = ((incoming != null) ? incoming.Display() : (Fusion.NameOf(fromId) ?? $"Player {fromId}")); FriendStore.AddFriend(fromId, text); LinkNet.Send(LinkKind.RequestAccept, fromId); Toast("Friend added", "You and " + text + " are now friends.", (NotificationType)3); Menu.RefreshDynamic(); } public static void DeclineRequest(ulong fromId) { FriendStore.RemoveIncoming(fromId); LinkNet.Send(LinkKind.RequestDecline, fromId); Menu.RefreshDynamic(); } public static void Unfriend(ulong id) { string text = FriendStore.GetFriend(id)?.Display() ?? "friend"; LinkNet.Send(LinkKind.Unfriend, id); FriendStore.RemoveFriend(id); Toast("Unfriended", "Removed " + text + ".", (NotificationType)0); Menu.RefreshDynamic(); } public static void Block(ulong id) { LinkNet.Send(LinkKind.Unfriend, id); FriendStore.Block(id); Toast("Blocked", "They can no longer reach you.", (NotificationType)0); Menu.RefreshDynamic(); } public static void SendMessageTo(ulong toId, string body) { if (string.IsNullOrWhiteSpace(body)) { Toast("LabLink", "Type a message first.", (NotificationType)1); return; } if (!Fusion.HasServer) { Toast("LabLink", "You're not in a lobby.", (NotificationType)1); return; } if (toId == 0L) { Toast("LabLink", "Pick a recipient first.", (NotificationType)1); return; } if (!Fusion.IsOnline(toId)) { Toast("LabLink", "That friend isn't in this lobby.", (NotificationType)1); return; } LinkNet.Send(LinkKind.DirectMessage, toId, body); string text = FriendStore.GetFriend(toId)?.Display() ?? Fusion.NameOf(toId) ?? "them"; Toast("Sent", "Message sent to " + text + ".", (NotificationType)3); } private static void RecordInbox(ulong from, string name, string body) { _inbox.Add(new InboxEntry { Time = Time.unscaledTime, FromId = from, Name = name, Body = (body ?? "") }); while (_inbox.Count > 20) { _inbox.RemoveAt(0); } } public static void ClearInbox() { _inbox.Clear(); } public static void Toast(string title, string message, NotificationType type) { //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) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0043: 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_0056: Expected O, but got Unknown Notify(new Notification { Title = NotificationText.op_Implicit(title), Message = NotificationText.op_Implicit(message), Type = type, PopupLength = Mathf.Clamp(Settings.PopupDuration.Value, 1f, 30f), ShowPopup = true, SaveToMenu = false }); } private static void Notify(Notification n) { try { Notifier.Send(n); } catch (Exception ex) { ModMain.Log.Warning("Notifier failed: " + ex.Message); } } } public enum PresenceStatus { Unknown, Offline, Online, InGame, InLobby, InYourLobby } public struct FriendPresence { public PresenceStatus Status; public bool Joinable; public ulong HostId; public string Label; } public static class SteamPresence { public static bool SteamAvailable { get { try { return SteamClient.IsValid; } catch { return false; } } } public static FriendPresence Get(ulong platformId) { //IL_004f: 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_0089: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: 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) if (Fusion.IsOnline(platformId)) { return new FriendPresence { Status = PresenceStatus.InYourLobby, Label = "in your lobby" }; } if (!SteamAvailable) { return new FriendPresence { Status = PresenceStatus.Unknown, Label = "unknown" }; } try { Friend val = default(Friend); ((Friend)(ref val))..ctor(SteamId.op_Implicit(platformId)); if (((Friend)(ref val)).IsPlayingThisGame) { FriendGameInfo? gameInfo = ((Friend)(ref val)).GameInfo; Lobby? obj; if (!gameInfo.HasValue) { obj = null; } else { FriendGameInfo valueOrDefault = gameInfo.GetValueOrDefault(); obj = ((FriendGameInfo)(ref valueOrDefault)).Lobby; } Lobby? val2 = obj; if (val2.HasValue) { ulong num = 0uL; try { Lobby value = val2.Value; num = SteamId.op_Implicit(((Lobby)(ref value)).Owner.Id); } catch { } if (num != 0L) { return new FriendPresence { Status = PresenceStatus.InLobby, Joinable = true, HostId = num, Label = "in a lobby" }; } return new FriendPresence { Status = PresenceStatus.InLobby, Joinable = false, Label = "in a lobby" }; } return new FriendPresence { Status = PresenceStatus.InGame, Label = "in BONELAB" }; } if (((Friend)(ref val)).IsOnline) { return new FriendPresence { Status = PresenceStatus.Online, Label = "online" }; } if (((Friend)(ref val)).IsFriend) { return new FriendPresence { Status = PresenceStatus.Offline, Label = "offline" }; } return new FriendPresence { Status = PresenceStatus.Unknown, Label = "not a Steam friend" }; } catch (Exception ex) { ModMain.Log.Warning("[LabLink] presence read failed: " + ex.Message); return new FriendPresence { Status = PresenceStatus.Unknown, Label = "unknown" }; } } public static string JoinFriend(ulong platformId) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) if (!SteamAvailable) { return "Joining only works on the Steam version."; } try { Friend val = default(Friend); ((Friend)(ref val))..ctor(SteamId.op_Implicit(platformId)); if (!((Friend)(ref val)).IsPlayingThisGame) { return "They're not in BONELAB right now."; } FriendGameInfo? gameInfo = ((Friend)(ref val)).GameInfo; Lobby? obj; if (!gameInfo.HasValue) { obj = null; } else { FriendGameInfo valueOrDefault = gameInfo.GetValueOrDefault(); obj = ((FriendGameInfo)(ref valueOrDefault)).Lobby; } Lobby? val2 = obj; if (!val2.HasValue) { return "They're not in a joinable lobby."; } Lobby value = val2.Value; ulong num = 0uL; try { num = SteamId.op_Implicit(((Lobby)(ref value)).Owner.Id); } catch { } if (num == 0L) { try { ((Lobby)(ref value)).Refresh(); } catch { } return "Fetching their lobby... press Join again in a moment."; } NetworkLayer layer = NetworkLayerManager.Layer; SteamNetworkLayer val3 = (SteamNetworkLayer)(object)((layer is SteamNetworkLayer) ? layer : null); if (val3 != null) { val3.JoinServer(SteamId.op_Implicit(num)); return "Joining " + ((Friend)(ref val)).Name + "..."; } return "Joining requires the Steam network layer."; } catch (Exception ex) { ModMain.Log.Warning("[LabLink] join failed: " + ex.Message); return "Couldn't join (see console)."; } } } } namespace LabLink.Net { public class LinkData : INetSerializable { public byte Kind; public ulong FromId; public string FromName; public ulong ToId; public string Body; public LinkData() { FromName = ""; Body = ""; } public int? GetSize() { return 17 + SizeExtensions.GetSize(FromName) + SizeExtensions.GetSize(Body); } public void Serialize(INetSerializer serializer) { serializer.SerializeValue(ref Kind); serializer.SerializeValue(ref FromId); serializer.SerializeValue(ref FromName); serializer.SerializeValue(ref ToId); serializer.SerializeValue(ref Body); } } public class LinkHandler : ModuleMessageHandler { protected override void OnHandleMessage(ReceivedMessage received) { try { LinkData linkData = ((ReceivedMessage)(ref received)).ReadData(); if (linkData != null) { Social.OnReceive(linkData); } } catch (Exception value) { ModMain.Log.Error($"[LabLink] failed to handle packet: {value}"); } } } public enum LinkKind : byte { CapabilityPing, CapabilityAck, FriendRequest, RequestAccept, RequestDecline, DirectMessage, Unfriend } public class LinkModule : Module { public override string Name => "LabLink"; public override string Author => "Dynamic Team"; public override Version Version => new Version(1, 0, 0); public override ConsoleColor Color => ConsoleColor.Green; protected override void OnModuleRegistered() { ModuleMessageManager.RegisterHandler(); ModMain.Log.Msg("LabLink message handler registered."); } } public static class LinkNet { public static void Send(LinkKind kind, ulong toId, string body = "") { //IL_004b: Unknown result type (might be due to invalid IL or missing references) if (!Fusion.HasServer) { return; } try { LinkData obj = new LinkData { Kind = (byte)kind, FromId = Fusion.LocalPlatformId, FromName = Fusion.LocalName(), ToId = toId, Body = (body ?? "") }; MessageRoute val = default(MessageRoute); ((MessageRoute)(ref val))..ctor((RelayType)2, (NetworkChannel)0); MessageRelay.RelayModule(obj, val); } catch (Exception ex) { ModMain.Log.Warning($"[LabLink] send {kind} failed: {ex.Message}"); } } public static void Broadcast(LinkKind kind, string body = "") { Send(kind, 0uL, body); } } } namespace LabLink.Data { public class Friend { public ulong PlatformId { get; set; } public string LastName { get; set; } = ""; public string Nickname { get; set; } = ""; public string Note { get; set; } = ""; public bool Favorite { get; set; } public long AddedUnix { get; set; } public string Display() { if (!string.IsNullOrEmpty(Nickname)) { return Nickname; } if (!string.IsNullOrEmpty(LastName)) { return LastName; } return $"Player {PlatformId}"; } } public class PendingRequest { public ulong PlatformId { get; set; } public string LastName { get; set; } = ""; public long Unix { get; set; } public string Display() { if (!string.IsNullOrEmpty(LastName)) { return LastName; } return $"Player {PlatformId}"; } } public static class FriendStore { public sealed class StoreData { public List Friends { get; set; } = new List(); public List Blocked { get; set; } = new List(); public List Incoming { get; set; } = new List(); public List Outgoing { get; set; } = new List(); } private static StoreData _data = new StoreData(); public static IReadOnlyList Friends => _data.Friends; public static IReadOnlyList Blocked => _data.Blocked; public static IReadOnlyList Incoming => _data.Incoming; public static IReadOnlyList Outgoing => _data.Outgoing; private static string FilePath() { string text; try { text = Path.Combine(MelonEnvironment.UserDataDirectory, "LabLink"); } catch { text = Path.Combine("UserData", "LabLink"); } Directory.CreateDirectory(text); return Path.Combine(text, "labllink.json"); } public static void Load() { try { string path = FilePath(); if (File.Exists(path)) { _data = JsonSerializer.Deserialize(File.ReadAllText(path)) ?? new StoreData(); } } catch (Exception ex) { ModMain.Log.Warning("Failed to load friends: " + ex.Message); _data = new StoreData(); } if (_data.Friends == null) { _data.Friends = new List(); } if (_data.Blocked == null) { _data.Blocked = new List(); } if (_data.Incoming == null) { _data.Incoming = new List(); } if (_data.Outgoing == null) { _data.Outgoing = new List(); } } public static void Save() { try { string contents = JsonSerializer.Serialize(_data, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(FilePath(), contents); } catch (Exception ex) { ModMain.Log.Warning("Failed to save friends: " + ex.Message); } } private static long Now() { return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); } public static Friend GetFriend(ulong id) { return _data.Friends.FirstOrDefault((Friend f) => f.PlatformId == id); } public static bool IsFriend(ulong id) { return GetFriend(id) != null; } public static bool IsBlocked(ulong id) { return _data.Blocked.Contains(id); } public static PendingRequest GetIncoming(ulong id) { return _data.Incoming.FirstOrDefault((PendingRequest r) => r.PlatformId == id); } public static PendingRequest GetOutgoing(ulong id) { return _data.Outgoing.FirstOrDefault((PendingRequest r) => r.PlatformId == id); } public static Friend AddFriend(ulong id, string name) { Friend friend = GetFriend(id); if (friend == null) { friend = new Friend { PlatformId = id, LastName = (name ?? ""), AddedUnix = Now() }; _data.Friends.Add(friend); } else if (!string.IsNullOrEmpty(name)) { friend.LastName = name; } _data.Incoming.RemoveAll((PendingRequest r) => r.PlatformId == id); _data.Outgoing.RemoveAll((PendingRequest r) => r.PlatformId == id); Save(); return friend; } public static void RemoveFriend(ulong id) { _data.Friends.RemoveAll((Friend f) => f.PlatformId == id); Save(); } public static void Block(ulong id) { _data.Friends.RemoveAll((Friend f) => f.PlatformId == id); _data.Incoming.RemoveAll((PendingRequest r) => r.PlatformId == id); _data.Outgoing.RemoveAll((PendingRequest r) => r.PlatformId == id); if (!_data.Blocked.Contains(id)) { _data.Blocked.Add(id); } Save(); } public static void Unblock(ulong id) { _data.Blocked.RemoveAll((ulong b) => b == id); Save(); } public static void AddIncoming(ulong id, string name) { PendingRequest incoming = GetIncoming(id); if (incoming == null) { _data.Incoming.Add(new PendingRequest { PlatformId = id, LastName = (name ?? ""), Unix = Now() }); } else if (!string.IsNullOrEmpty(name)) { incoming.LastName = name; } Save(); } public static void RemoveIncoming(ulong id) { _data.Incoming.RemoveAll((PendingRequest r) => r.PlatformId == id); Save(); } public static void AddOutgoing(ulong id, string name) { if (GetOutgoing(id) == null) { _data.Outgoing.Add(new PendingRequest { PlatformId = id, LastName = (name ?? ""), Unix = Now() }); } Save(); } public static void RemoveOutgoing(ulong id) { _data.Outgoing.RemoveAll((PendingRequest r) => r.PlatformId == id); Save(); } public static void UpdateName(ulong id, string name) { if (!string.IsNullOrEmpty(name)) { Friend friend = GetFriend(id); if (friend != null && friend.LastName != name) { friend.LastName = name; Save(); } } } } }