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 BoneLib.BoneMenu;
using LabFusion.Data;
using LabFusion.Entities;
using LabFusion.Network;
using LabFusion.Player;
using LabFusion.SDK.Metadata;
using LabLink;
using LabLink.Friends;
using MelonLoader;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(LabLinkMain), "LabLink", "1.0.0", "LabLink Team", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyVersion("0.0.0.0")]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
}
namespace LabLink
{
public class LabLinkMain : MelonMod
{
public static LabLinkMain? Instance;
public static FriendManager? FriendManager;
public static PlayerTargeter? PlayerTargeter;
public static string ModDirectory = string.Empty;
private static Page? _rootPage;
private static Page? _friendsPage;
public override void OnInitializeMelon()
{
Instance = this;
ModDirectory = Path.Combine((string)(typeof(MelonUtils).GetProperty("GameDirectory", BindingFlags.Static | BindingFlags.Public)?.GetValue(null) ?? ""), "LabLink");
Directory.CreateDirectory(ModDirectory);
FriendManager = new FriendManager();
FriendManager.Load();
PlayerTargeter = new PlayerTargeter();
SetupBoneMenu();
((MelonBase)this).LoggerInstance.Msg("LabLink initialized!");
}
private static void SetupBoneMenu()
{
//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_0059: Unknown result type (might be due to invalid IL or missing references)
//IL_008d: Unknown result type (might be due to invalid IL or missing references)
_rootPage = Page.Root.CreatePage("LabLink", Color.cyan, 0, true);
_rootPage.CreateFunction("Refresh Friends", Color.white, (Action)delegate
{
FriendManager?.RefreshNow();
RebuildFriendsPage();
});
_rootPage.CreateFunction("Add Friend (Point + Press A)", Color.green, (Action)delegate
{
if (PlayerTargeter != null && PlayerTargeter.IsTargeting)
{
PlayerTargeter.AddTargetedAsFriend();
}
else
{
FriendManager?.Notify("Point at a player and try again.");
}
});
_friendsPage = _rootPage.CreatePage("Friends List", Color.yellow, 0, true);
RebuildFriendsPage();
}
public static void RebuildFriendsPage()
{
//IL_0023: 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_00a6: Unknown result type (might be due to invalid IL or missing references)
//IL_00da: Unknown result type (might be due to invalid IL or missing references)
//IL_0137: 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_013c: Unknown result type (might be due to invalid IL or missing references)
//IL_017e: Unknown result type (might be due to invalid IL or missing references)
//IL_0196: Unknown result type (might be due to invalid IL or missing references)
//IL_0341: Unknown result type (might be due to invalid IL or missing references)
//IL_0310: Unknown result type (might be due to invalid IL or missing references)
//IL_01f8: Unknown result type (might be due to invalid IL or missing references)
//IL_0239: Unknown result type (might be due to invalid IL or missing references)
//IL_02b2: Unknown result type (might be due to invalid IL or missing references)
//IL_02e3: Unknown result type (might be due to invalid IL or missing references)
if (_friendsPage == null || FriendManager == null)
{
return;
}
_friendsPage.RemoveAll();
_friendsPage.CreateFunction("Refresh", Color.white, (Action)delegate
{
FriendManager.RefreshNow();
RebuildFriendsPage();
});
if (!NetworkLayerManager.LoggedIn)
{
_friendsPage.CreateFunction("Not connected to Fusion", Color.red, (Action)delegate
{
});
return;
}
IReadOnlyList<FriendStatus> friendStatuses = FriendManager.FriendStatuses;
if (friendStatuses.Count == 0)
{
_friendsPage.CreateFunction("No friends added yet", Color.gray, (Action)delegate
{
});
_friendsPage.CreateFunction("Point at a player + press A", Color.gray, (Action)delegate
{
});
return;
}
foreach (FriendStatus status in friendStatuses)
{
Color val = (status.IsOnline ? Color.green : Color.red);
string text = (status.IsOnline ? "ONLINE" : "OFFLINE");
Page val2 = _friendsPage.CreatePage(status.Entry.Username + " [" + text + "]", val, 0, true);
val2.CreateFunction("Status: " + text, val, (Action)delegate
{
});
if (status.IsOnline && !string.IsNullOrEmpty(status.CurrentLobbyCode))
{
val2.CreateFunction("Lobby: " + status.CurrentLobbyName, Color.white, (Action)delegate
{
});
val2.CreateFunction("Level: " + status.CurrentLevel, Color.white, (Action)delegate
{
});
val2.CreateFunction($"Players: {status.LobbyPlayerCount}/{status.LobbyMaxPlayers}", Color.white, (Action)delegate
{
});
val2.CreateFunction("JOIN", Color.green, (Action)delegate
{
FriendManager.JoinFriend(status);
RebuildFriendsPage();
});
}
else if (status.IsOnline)
{
val2.CreateFunction("In main menu (not in lobby)", Color.yellow, (Action)delegate
{
});
}
val2.CreateFunction("Remove Friend", Color.red, (Action)delegate
{
FriendManager.RemoveFriend(status.Entry.PlatformID);
RebuildFriendsPage();
});
}
}
public override void OnUpdate()
{
if (FriendManager != null)
{
if (Input.GetKeyDown((KeyCode)284))
{
RebuildFriendsPage();
Menu.OpenPage(_rootPage);
}
FriendManager.Update();
PlayerTargeter?.Update();
}
}
}
public class PlayerTargeter
{
private float _checkTimer;
private const float CHECK_INTERVAL = 0.15f;
private const float MAX_DISTANCE = 10f;
public PlayerID? TargetedPlayer { get; private set; }
public bool IsTargeting => TargetedPlayer != null;
public void Update()
{
//IL_0052: Unknown result type (might be due to invalid IL or missing references)
//IL_005d: Unknown result type (might be due to invalid IL or missing references)
//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
//IL_00df: 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)
//IL_00e8: 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_00f4: Unknown result type (might be due to invalid IL or missing references)
//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
//IL_0106: Unknown result type (might be due to invalid IL or missing references)
//IL_010b: Unknown result type (might be due to invalid IL or missing references)
//IL_010d: Unknown result type (might be due to invalid IL or missing references)
//IL_0112: Unknown result type (might be due to invalid IL or missing references)
_checkTimer += Time.unscaledDeltaTime;
if (_checkTimer < 0.15f)
{
return;
}
_checkTimer = 0f;
TargetedPlayer = null;
if (!NetworkInfo.HasServer)
{
return;
}
Camera main = Camera.main;
if ((Object)(object)main == (Object)null)
{
return;
}
Ray val = default(Ray);
((Ray)(ref val))..ctor(((Component)main).transform.position, ((Component)main).transform.forward);
float num = 10f;
PlayerID targetedPlayer = null;
NetworkPlayer val2 = default(NetworkPlayer);
foreach (PlayerID playerID in PlayerIDManager.PlayerIDs)
{
if (!playerID.IsMe && NetworkPlayerManager.TryGetPlayer(playerID.SmallID, ref val2) && val2 != null && val2.RigRefs != null && !((Object)(object)val2.RigRefs.Head == (Object)null))
{
Vector3 position = val2.RigRefs.Head.position;
Vector3 val3 = Vector3.Project(position - ((Ray)(ref val)).origin, ((Ray)(ref val)).direction);
float magnitude = ((Vector3)(ref val3)).magnitude;
float num2 = Vector3.Distance(((Ray)(ref val)).origin + val3, position);
if (magnitude < num && num2 < 0.5f)
{
num = magnitude;
targetedPlayer = playerID;
}
}
}
TargetedPlayer = targetedPlayer;
if (TargetedPlayer != null && Input.GetKeyDown((KeyCode)331))
{
AddTargetedAsFriend();
}
}
public void AddTargetedAsFriend()
{
if (TargetedPlayer == null)
{
return;
}
string platformId = TargetedPlayer.PlatformID.ToString();
PlayerMetadata metadata = TargetedPlayer.Metadata;
object obj;
if (metadata == null)
{
obj = null;
}
else
{
MetadataVariable username = metadata.Username;
obj = ((username != null) ? username.GetValueOrEmpty() : null);
}
if (obj == null)
{
obj = "Unknown";
}
string text = (string)obj;
if (LabLinkMain.FriendManager != null)
{
if (LabLinkMain.FriendManager.IsFriend(platformId))
{
LabLinkMain.FriendManager.Notify(text + " is already your friend!");
}
else
{
LabLinkMain.FriendManager.AddFriend(platformId, text);
}
}
}
}
}
namespace LabLink.UI
{
public static class FriendListUI
{
}
}
namespace LabLink.Friends
{
public class FriendEntry
{
public string Username { get; set; } = string.Empty;
public string PlatformID { get; set; } = string.Empty;
public DateTime AddedDate { get; set; } = DateTime.UtcNow;
public string Notes { get; set; } = string.Empty;
}
public class FriendListData
{
public List<FriendEntry> Friends { get; set; } = new List<FriendEntry>();
}
public class FriendStatus
{
public FriendEntry Entry;
public bool IsOnline;
public string CurrentLobbyName = string.Empty;
public string CurrentLobbyCode = string.Empty;
public int LobbyPlayerCount;
public int LobbyMaxPlayers;
public string CurrentLevel = string.Empty;
public FriendStatus(FriendEntry entry)
{
Entry = entry;
}
}
public struct LobbyScanResult
{
public LobbyMetadataInfo Metadata;
public INetworkLobby Lobby;
}
public class FriendManager
{
private FriendListData _data = new FriendListData();
private List<FriendStatus> _statuses = new List<FriendStatus>();
private float _checkTimer;
private const float CHECK_INTERVAL = 3f;
private float _lobbyScanTimer;
private const float LOBBY_SCAN_INTERVAL = 10f;
private List<LobbyScanResult> _cachedLobbies = new List<LobbyScanResult>();
private Action<string>? _onNotification;
private Action? _onFriendsListChanged;
private Dictionary<ulong, bool> _previousOnlineState = new Dictionary<ulong, bool>();
private string SavePath => Path.Combine(LabLinkMain.ModDirectory, "friends.json");
public IReadOnlyList<FriendStatus> FriendStatuses => _statuses;
public IReadOnlyList<FriendEntry> Friends => _data.Friends;
public event Action<string>? OnNotification
{
add
{
_onNotification = (Action<string>)Delegate.Combine(_onNotification, value);
}
remove
{
_onNotification = (Action<string>)Delegate.Remove(_onNotification, value);
}
}
public event Action? OnFriendsListChanged
{
add
{
_onFriendsListChanged = (Action)Delegate.Combine(_onFriendsListChanged, value);
}
remove
{
_onFriendsListChanged = (Action)Delegate.Remove(_onFriendsListChanged, value);
}
}
public void Notify(string message)
{
_onNotification?.Invoke(message);
}
public void Load()
{
try
{
if (File.Exists(SavePath))
{
string text = File.ReadAllText(SavePath);
_data = JsonConvert.DeserializeObject<FriendListData>(text) ?? new FriendListData();
}
else
{
_data = new FriendListData();
}
RebuildStatuses();
MelonLogger.Msg($"[LabLink] Loaded {_data.Friends.Count} friends.");
}
catch (Exception ex)
{
MelonLogger.Error("[LabLink] Failed to load friends: " + ex.Message);
_data = new FriendListData();
}
}
public void Save()
{
try
{
string contents = JsonConvert.SerializeObject((object)_data, (Formatting)1);
File.WriteAllText(SavePath, contents);
}
catch (Exception ex)
{
MelonLogger.Error("[LabLink] Failed to save friends: " + ex.Message);
}
}
public bool AddFriend(string platformId, string username)
{
if (_data.Friends.Any((FriendEntry f) => f.PlatformID == platformId))
{
return false;
}
FriendEntry item = new FriendEntry
{
PlatformID = platformId,
Username = username,
AddedDate = DateTime.UtcNow
};
_data.Friends.Add(item);
Save();
RebuildStatuses();
_onFriendsListChanged?.Invoke();
_onNotification?.Invoke("Added " + username + " as a friend!");
return true;
}
public bool AddFriendByUsername(string username)
{
string platformId = string.Empty;
string username2 = string.Empty;
bool flag = false;
foreach (PlayerID playerID in PlayerIDManager.PlayerIDs)
{
string valueOrEmpty = playerID.Metadata.Username.GetValueOrEmpty();
if (string.Equals(valueOrEmpty, username, StringComparison.OrdinalIgnoreCase))
{
platformId = playerID.PlatformID.ToString();
username2 = valueOrEmpty;
flag = true;
break;
}
}
if (!flag)
{
foreach (LobbyScanResult cachedLobby in _cachedLobbies)
{
LobbyScanResult current2 = cachedLobby;
LobbyInfo lobbyInfo = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo;
object obj;
if (lobbyInfo == null)
{
obj = null;
}
else
{
PlayerList playerList = lobbyInfo.PlayerList;
obj = ((playerList != null) ? playerList.Players : null);
}
if (obj == null)
{
continue;
}
PlayerInfo[] players = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo.PlayerList.Players;
foreach (PlayerInfo val in players)
{
if (string.Equals(val.Username, username, StringComparison.OrdinalIgnoreCase))
{
platformId = val.PlatformID;
username2 = val.Username;
flag = true;
break;
}
}
if (flag)
{
break;
}
}
}
if (!flag)
{
return false;
}
return AddFriend(platformId, username2);
}
public bool RemoveFriend(string platformId)
{
if (_data.Friends.RemoveAll((FriendEntry f) => f.PlatformID == platformId) > 0)
{
Save();
RebuildStatuses();
_onFriendsListChanged?.Invoke();
return true;
}
return false;
}
public bool IsFriend(string platformId)
{
return _data.Friends.Any((FriendEntry f) => f.PlatformID == platformId);
}
private void RebuildStatuses()
{
_statuses.Clear();
foreach (FriendEntry friend in _data.Friends)
{
_statuses.Add(new FriendStatus(friend));
}
UpdateStatuses();
}
public void Update()
{
_checkTimer += Time.unscaledDeltaTime;
if (_checkTimer >= 3f)
{
_checkTimer = 0f;
UpdateStatuses();
}
_lobbyScanTimer += Time.unscaledDeltaTime;
if (_lobbyScanTimer >= 10f)
{
_lobbyScanTimer = 0f;
ScanLobbies();
}
}
public void RefreshNow()
{
UpdateStatuses();
ScanLobbies();
_onNotification?.Invoke("Friend status refreshed!");
}
private void UpdateStatuses()
{
if (!NetworkLayerManager.LoggedIn)
{
return;
}
foreach (FriendStatus status in _statuses)
{
bool isOnline = status.IsOnline;
status.IsOnline = false;
status.CurrentLobbyName = string.Empty;
status.CurrentLobbyCode = string.Empty;
status.LobbyPlayerCount = 0;
status.CurrentLevel = string.Empty;
foreach (PlayerID playerID in PlayerIDManager.PlayerIDs)
{
if (!(playerID.PlatformID.ToString() == status.Entry.PlatformID))
{
continue;
}
status.IsOnline = true;
if (!NetworkInfo.HasServer || LobbyInfoManager.LobbyInfo == null)
{
break;
}
LobbyInfo lobbyInfo = LobbyInfoManager.LobbyInfo;
PlayerList playerList = lobbyInfo.PlayerList;
PlayerInfo[] array = ((playerList != null) ? playerList.Players : null);
if (array == null)
{
break;
}
PlayerInfo[] array2 = array;
for (int i = 0; i < array2.Length; i++)
{
if (array2[i].PlatformID == status.Entry.PlatformID)
{
status.CurrentLobbyName = lobbyInfo.LobbyName ?? "Unknown";
status.CurrentLobbyCode = lobbyInfo.LobbyCode ?? string.Empty;
status.LobbyPlayerCount = lobbyInfo.PlayerCount;
status.LobbyMaxPlayers = lobbyInfo.MaxPlayers;
status.CurrentLevel = lobbyInfo.LevelTitle ?? "Unknown";
break;
}
}
break;
}
if (!status.IsOnline)
{
foreach (LobbyScanResult cachedLobby in _cachedLobbies)
{
LobbyScanResult current2 = cachedLobby;
LobbyInfo lobbyInfo2 = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo;
object obj;
if (lobbyInfo2 == null)
{
obj = null;
}
else
{
PlayerList playerList2 = lobbyInfo2.PlayerList;
obj = ((playerList2 != null) ? playerList2.Players : null);
}
if (obj == null)
{
continue;
}
PlayerInfo[] array2 = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo.PlayerList.Players;
for (int i = 0; i < array2.Length; i++)
{
if (array2[i].PlatformID == status.Entry.PlatformID)
{
status.IsOnline = true;
status.CurrentLobbyName = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo.LobbyName ?? "Unknown";
status.CurrentLobbyCode = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo.LobbyCode ?? string.Empty;
status.LobbyPlayerCount = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo.PlayerCount;
status.LobbyMaxPlayers = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo.MaxPlayers;
status.CurrentLevel = ((LobbyMetadataInfo)(ref current2.Metadata)).LobbyInfo.LevelTitle ?? "Unknown";
break;
}
}
if (status.IsOnline)
{
break;
}
}
}
if (isOnline != status.IsOnline)
{
if (status.IsOnline)
{
_onNotification?.Invoke(status.Entry.Username + " is now online!");
}
else
{
_onNotification?.Invoke(status.Entry.Username + " went offline.");
}
}
}
}
private void ScanLobbies()
{
if (!NetworkLayerManager.LoggedIn)
{
return;
}
try
{
IMatchmaker matchmaker = NetworkLayerManager.Layer.Matchmaker;
if (matchmaker == null)
{
return;
}
matchmaker.RequestLobbies((Action<MatchmakerCallbackInfo>)delegate(MatchmakerCallbackInfo callback)
{
//IL_000b: Unknown result type (might be due to invalid IL or missing references)
//IL_0014: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Unknown result type (might be due to invalid IL or missing references)
//IL_0026: Unknown result type (might be due to invalid IL or missing references)
//IL_0037: 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)
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
//IL_0044: Unknown result type (might be due to invalid IL or missing references)
_cachedLobbies.Clear();
if (callback.Lobbies != null)
{
LobbyInfo[] lobbies = callback.Lobbies;
foreach (LobbyInfo val in lobbies)
{
_cachedLobbies.Add(new LobbyScanResult
{
Metadata = val.Metadata,
Lobby = val.Lobby
});
}
UpdateStatuses();
}
});
}
catch (Exception ex)
{
MelonLogger.Warning("[LabLink] Lobby scan failed: " + ex.Message);
}
}
public void JoinFriend(FriendStatus friend)
{
if (!friend.IsOnline || string.IsNullOrEmpty(friend.CurrentLobbyCode))
{
_onNotification?.Invoke(friend.Entry.Username + " is not in a joinable lobby.");
return;
}
try
{
NetworkHelper.JoinServerByCode(friend.CurrentLobbyCode);
_onNotification?.Invoke("Joining " + friend.Entry.Username + "'s lobby...");
}
catch (Exception ex)
{
MelonLogger.Error("[LabLink] Failed to join lobby: " + ex.Message);
_onNotification?.Invoke("Failed to join lobby.");
}
}
public List<LobbyScanResult> GetCachedLobbies()
{
return _cachedLobbies;
}
}
}