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.InteropServices; using System.Runtime.Versioning; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using BepInEx.Unity.IL2CPP; using Gear; using Globals; using HarmonyLib; using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppSystem; using Il2CppSystem.Collections.Generic; using Microsoft.CodeAnalysis; using Player; using PlayerSync.Interop; using PlayerSync.Network; using PlayerSync.Network.Impl; using PlayerSync.Sync.Ammo; using PlayerSync.Sync.Stamina; using SNetwork; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")] [assembly: AssemblyCompany("PlayerSync")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("PlayerSync")] [assembly: AssemblyTitle("PlayerSync")] [assembly: AssemblyVersion("1.0.0.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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 PlayerSync { internal static class ConfigMgr { private static readonly ConfigFile Conf; private static readonly FileSystemWatcher? ConfigWatcher; private static readonly ConfigEntry SyncFrequencyConf; private static readonly ConfigEntry DebugConf; public static bool Processed { get; private set; } public static float SyncFrequency => SyncFrequencyConf.Value; public static float SyncInterval => 1f / SyncFrequency; public static bool Debug => DebugConf.Value; public static void Init() { Logger.Info($"debug={Debug}"); Process(); } public static void Process() { Processed = true; } static ConfigMgr() { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown string text = "PlayerSync.cfg"; string text2 = Path.Combine(Paths.ConfigPath, text); Logger.Info("cfgPath = " + text2); Conf = new ConfigFile(text2, true); ConfigWatcher = new FileSystemWatcher(Paths.ConfigPath, text) { NotifyFilter = NotifyFilters.LastWrite, EnableRaisingEvents = true }; ConfigWatcher.Changed += async delegate { ConfigWatcher.EnableRaisingEvents = false; await Task.Delay(500); Logger.Debug("Reloading config..."); Conf.Reload(); await Task.Delay(250); Process(); await Task.Delay(250); ConfigWatcher.EnableRaisingEvents = true; }; int num = 1; string text3 = $"({num++}) Advanced"; SyncFrequencyConf = Conf.Bind(text3, "Sync Frequency (Hz)", 20f, new ConfigDescription("Frequency in Hz to send player sync data", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 60f), Array.Empty())); text3 = "(Z) Dev"; DebugConf = Conf.Bind(text3, "Enable Debug Logs", false, "debug logging"); } } [HarmonyPatch] public static class Events { public static event Action? OnSessionEnd; public static event Action? OnSessionStart; public static event Action? OnAnyPlayerDeath; public static event Action? OnCheckpointReload; public static event Action? OnWeaponFire; public static event Action? OnWeaponReload; public static event Action? OnPlayerJoinLobby; [HarmonyPatch(typeof(GS_ReadyToStopElevatorRide), "Enter")] [HarmonyPostfix] private static void GS_ReadyToStopElevatorRide_Enter() { Events.OnSessionStart?.Invoke(); } [HarmonyPatch(typeof(PLOC_Downed), "SyncEnter")] [HarmonyPostfix] private static void PLOC_Downed_SyncEnter() { Events.OnAnyPlayerDeath?.Invoke(); } [HarmonyPatch(typeof(RundownManager), "EndGameSession")] [HarmonyPrefix] private static void EndGameSession() { Events.OnSessionEnd?.Invoke(); } [HarmonyPatch(typeof(SNet_SessionHub), "LeaveHub")] [HarmonyPrefix] private static void LeaveHub() { Events.OnSessionEnd?.Invoke(); } [HarmonyPatch(typeof(SNet_SessionHub), "OnJoinedLobby")] [HarmonyPrefix] private static void PlayerJoin(SNet_Player player) { Events.OnPlayerJoinLobby?.Invoke(player); } [HarmonyPatch(typeof(CheckpointManager), "OnStateChange")] [HarmonyPrefix] private static void CheckpointStateChange(pCheckpointState oldState, pCheckpointState newState) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 if ((int)newState.lastInteraction == 2) { Events.OnCheckpointReload?.Invoke(); } } [HarmonyPatch(typeof(BulletWeapon), "Fire")] [HarmonyPostfix] private static void BulletWeaponFire() { Events.OnWeaponFire?.Invoke(); } [HarmonyPatch(typeof(PlayerInventoryLocal), "DoReload")] [HarmonyPostfix] private static void PlayerReload() { Events.OnWeaponReload?.Invoke(); } [HarmonyPatch(typeof(Shotgun), "Fire")] [HarmonyPostfix] private static void ShotgunFire() { Events.OnWeaponFire?.Invoke(); } } internal static class Logger { private static ManualLogSource _mLogSource; public static bool Ready => _mLogSource != null; public static void Setup() { _mLogSource = Logger.CreateLogSource("io.takina.gtfo.PlayerSync"); } public static void SetupFromInit(ManualLogSource logSource) { _mLogSource = logSource; } private static string Format(object data) { return data.ToString(); } public static void Debug(object msg) { if (ConfigMgr.Debug) { _mLogSource.LogInfo((object)(" [DEBUG] " + Format(msg))); } } public static void Debug(string fmt, params object[] args) { if (ConfigMgr.Debug) { _mLogSource.LogInfo((object)("[DEBUG] " + Format(string.Format(fmt, args)))); } } public static void Info(object msg) { _mLogSource.LogInfo((object)Format(msg)); } public static void Info(string fmt, params object[] args) { _mLogSource.LogInfo((object)Format(string.Format(fmt, args))); } public static void Warn(object msg) { _mLogSource.LogWarning((object)Format(msg)); } public static void Warn(string fmt, params object[] args) { _mLogSource.LogWarning((object)Format(string.Format(fmt, args))); } public static void Error(object msg) { _mLogSource.LogError((object)Format(msg)); } public static void Error(string fmt, params object[] args) { _mLogSource.LogError((object)Format(string.Format(fmt, args))); } public static void Fatal(object msg) { _mLogSource.LogFatal((object)Format(msg)); } public static void Fatal(string fmt, params object[] args) { _mLogSource.LogFatal((object)Format(string.Format(fmt, args))); } } [BepInPlugin("io.takina.gtfo.PlayerSync", "PlayerSync", "1.0.2")] public class Plugin : BasePlugin { public const string NAME = "PlayerSync"; public const string GUID = "io.takina.gtfo.PlayerSync"; public const string VERSION = "1.0.2"; public static readonly PlugVersion PlugVersion = new PlugVersion("1.0.2"); public static GameObject? PluginObject; public event Action? OnManagersSetup; public override void Load() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown Logger.Setup(); Logger.Info("PlayerSync [io.takina.gtfo.PlayerSync @ 1.0.2]"); Harmony val = new Harmony("io.takina.gtfo.PlayerSync"); ConfigMgr.Init(); RegisterIl2CppTypes(); OnManagersSetup += Initialize; Global.OnAllManagersSetup += Action.op_Implicit(this.OnManagersSetup); Logger.Info("Patching..."); PatchAll(val); Logger.Info("Finished Patching"); } private static void RegisterIl2CppTypes() { ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); ClassInjector.RegisterTypeInIl2Cpp(); } private static void PatchAll(Harmony h) { h.PatchAll(typeof(Net)); h.PatchAll(typeof(Events)); h.PatchAll(typeof(AmmoSync)); } private void Initialize() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Expected O, but got Unknown PluginObject = new GameObject("io.takina.gtfo.PlayerSync"); Object.DontDestroyOnLoad((Object)(object)PluginObject); PluginObject.AddComponent(); PluginObject.AddComponent(); PluginObject.AddComponent(); } public override bool Unload() { return true; } } public static class Util { public static void OffsetDevFloat32(ref DevFloat32 val, float offset) { val.internalValue += offset; } } } namespace PlayerSync.Sync.Stamina { public struct StaminaInfo { public float Stamina; public byte[] Serialize() { byte[] array = new byte[Unsafe.SizeOf()]; MemoryMarshal.Write(array, ref this); return array; } public static StaminaInfo Deserialize(byte[] data) { return MemoryMarshal.Read(data); } } [RegisterIl2Cpp] public class StaminaSync : MonoBehaviour { public const byte PacketId = 83; private static float _nextSyncTime = 0f; private static readonly Dictionary StaminaInfos = new Dictionary(); private void Awake() { Net.RegisterHandler(83, HandlePacket); } private void OnDestroy() { Net.UnregisterHandler(83); } private void Update() { if (Clock.Time < _nextSyncTime) { return; } _nextSyncTime = Clock.Time + ConfigMgr.SyncInterval; SNet_SessionHub sessionHub = SNet.SessionHub; ulong[] array = StaminaInfos.Keys.ToArray(); foreach (ulong num in array) { if (!sessionHub.IsPlayerInHub(NetHelper.GetPlayerByID(num))) { StaminaInfos.Remove(num); } } NetHelper.InvokeWithAllPlayers(SendStaminaInfo); } public static bool GetStaminaInfo(SNet_Player player, out StaminaInfo info) { return StaminaInfos.TryGetValue(player.Lookup, out info); } private static void SendStaminaInfo(SNet_Player target) { if (PeerInfoManager.Supported(target) && GetLocalStaminaInfo(out var info)) { Net.SendBytes(info.Serialize(), 83, target); } } private static bool GetLocalStaminaInfo(out StaminaInfo info) { info = default(StaminaInfo); PlayerAgent localPlayerAgent = PlayerManager.GetLocalPlayerAgent(); PlayerStamina val = ((localPlayerAgent != null) ? localPlayerAgent.Stamina : null); if ((Object)(object)val == (Object)null) { return false; } info.Stamina = val.Stamina; return true; } private static void HandlePacket(byte[] data, SNet_Player? sender) { StaminaInfo value = StaminaInfo.Deserialize(data); StaminaInfos[sender.Lookup] = value; } } } namespace PlayerSync.Sync.Ammo { public struct AmmoClipInfo { public ulong Lookup; public int StandardClipAmmo; public int SpecialClipAmmo; public int ClassClipAmmo; public int ResourcePackClipAmmo; public int ConsumableClipAmmo; public int GetClip(InventorySlot slot) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected I4, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) return (slot - 1) switch { 0 => StandardClipAmmo, 1 => SpecialClipAmmo, 2 => ClassClipAmmo, 3 => ResourcePackClipAmmo, 4 => ConsumableClipAmmo, _ => throw new ArgumentException($"Invalid inventory slot: {slot}"), }; } public byte[] Serialize() { byte[] array = new byte[Unsafe.SizeOf()]; MemoryMarshal.Write(array, ref this); return array; } public static AmmoClipInfo Deserialize(byte[] data) { return MemoryMarshal.Read(data); } } [RegisterIl2Cpp] [HarmonyPatch] public class AmmoSync : MonoBehaviour { public const byte PacketId = 65; private static float _nextSyncTime = 0f; private static readonly List SyncedSlots = new List { (InventorySlot)1, (InventorySlot)2, (InventorySlot)3, (InventorySlot)4, (InventorySlot)5 }; private void Awake() { Net.RegisterHandler(65, HandlePacket); Events.OnWeaponFire += SendAmmoUpdateToAll; Events.OnWeaponReload += SendAmmoUpdateToAll; } private void OnDestroy() { Net.UnregisterHandler(65); Events.OnWeaponFire -= SendAmmoUpdateToAll; Events.OnWeaponReload -= SendAmmoUpdateToAll; } [HarmonyPatch(typeof(PlayerBackpackManager), "SendLocalAmmoData")] [HarmonyPrefix] [HarmonyPriority(600)] private static bool Prefix__PlayerBackpackManager_SendLocalAmmoData(PlayerBackpackManager __instance) { return !NetHelper.InvokeWithAllPlayers(SendAmmoData); } private static void SendAmmoUpdateToAll() { NetHelper.InvokeWithAllPlayers(SendAmmoData); } private static void SendAmmoData(SNet_Player target) { //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_022a: Unknown result type (might be due to invalid IL or missing references) if (_nextSyncTime > Clock.Time) { return; } _nextSyncTime = Clock.Time + ConfigMgr.SyncInterval; if (!PlayerBackpackManager.m_hasLocalPlayerBackpack) { return; } Logger.Debug($"Sending ammo data for player '{target.GetName()}' ({target.Lookup}) clipSupport={PeerInfoManager.Supported(target)}"); bool flag = PeerInfoManager.Supported(target); PlayerBackpackManager.m_ammoSyncTimer = Clock.Time + PlayerBackpackManager.m_ammoSyncDelay; PlayerBackpackManager.m_ammoSyncTimerConstant = Clock.Time + PlayerBackpackManager.m_ammoSyncDelayConstant; Enumerator enumerator = PlayerBackpackManager.Current.m_backpacks.GetEnumerator(); while (enumerator.MoveNext()) { KeyValuePair current = enumerator.Current; if (current.Value.Owner.Lookup == SNet.LocalPlayer.Lookup || (SNet.IsMaster && current.Value.Owner.IsBot)) { PlayerAmmoStorage ammoStorage = current.Value.AmmoStorage; pAmmoStorageData storageData = ammoStorage.GetStorageData(); if (SNet.IsMaster && current.Value.Owner.IsBot) { current.Value.OnStorageUpdatedCallback?.Invoke(current.Value); } if (flag) { AmmoClipInfo clipInfo = GetClipInfo(current.Value); Util.OffsetDevFloat32(ref storageData.standardAmmo, (float)(-clipInfo.StandardClipAmmo) * ammoStorage.StandardAmmo.CostOfBullet); Util.OffsetDevFloat32(ref storageData.specialAmmo, (float)(-clipInfo.SpecialClipAmmo) * ammoStorage.SpecialAmmo.CostOfBullet); Util.OffsetDevFloat32(ref storageData.classAmmo, (float)(-clipInfo.ClassClipAmmo) * ammoStorage.ClassAmmo.CostOfBullet); Util.OffsetDevFloat32(ref storageData.resourcePackAmmo, (float)(-clipInfo.ResourcePackClipAmmo) * ammoStorage.ResourcePackAmmo.CostOfBullet); Util.OffsetDevFloat32(ref storageData.consumableAmmo, (float)(-clipInfo.ConsumableClipAmmo) * ammoStorage.ConsumableAmmo.CostOfBullet); Net.SendBytes(clipInfo.Serialize(), 65, target); } PlayerBackpackManager.Current.m_ammoStoragePacket.Send(storageData, (SNet_ChannelType)2, target); } } } private static void HandlePacket(byte[] data, SNet_Player? sender) { if (!((Object)(object)sender == (Object)null)) { ApplyClipInfo(AmmoClipInfo.Deserialize(data)); } } private static AmmoClipInfo GetClipInfo(PlayerBackpack backpack) { AmmoClipInfo result = default(AmmoClipInfo); PlayerAmmoStorage ammoStore = backpack.AmmoStorage; result.Lookup = backpack.Owner.Lookup; result.StandardClipAmmo = GetClip((InventorySlot)1); result.SpecialClipAmmo = GetClip((InventorySlot)2); result.ClassClipAmmo = GetClip((InventorySlot)3); result.ResourcePackClipAmmo = GetClip((InventorySlot)4); result.ConsumableClipAmmo = GetClip((InventorySlot)5); return result; int GetClip(InventorySlot slot) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return Mathf.RoundToInt(ammoStore.GetClipAmmoFromSlot(slot)); } } private static void ApplyClipInfo(AmmoClipInfo clipInfo) { //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: 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_0115: Unknown result type (might be due to invalid IL or missing references) Logger.Debug($"Applying clip info for player with lookup {clipInfo.Lookup}, with data: standard={clipInfo.StandardClipAmmo}, special={clipInfo.SpecialClipAmmo}, class={clipInfo.ClassClipAmmo}, resourcePack={clipInfo.ResourcePackClipAmmo}, consumable={clipInfo.ConsumableClipAmmo}"); PlayerBackpack val = default(PlayerBackpack); if (!PlayerBackpackManager.TryGetBackpack(clipInfo.Lookup, ref val)) { return; } BackpackItem val2 = default(BackpackItem); foreach (InventorySlot syncedSlot in SyncedSlots) { if (val.TryGetBackpackItem(syncedSlot, ref val2)) { ItemEquippable val3 = ((Il2CppObjectBase)val2.Instance).TryCast(); if ((Object)(object)val3 != (Object)null) { val3.SetCurrentClip(clipInfo.GetClip(syncedSlot)); } } } } } } namespace PlayerSync.Network { [HarmonyPatch] public class Net { private const ushort NetKeyBytes = 20556; private const int SNetChannel = 1; private static readonly Dictionary> MessageHandlers = new Dictionary>(); public static bool RegisterHandler(byte packetId, Action handler) { if (MessageHandlers.TryAdd(packetId, handler)) { return true; } Logger.Error($"Handler for packetId={packetId} is already registered!"); return false; } public static bool UnregisterHandler(byte packetId) { return MessageHandlers.Remove(packetId); } [HarmonyPatch(typeof(SNet_Replication), "RecieveBytes")] [HarmonyPrefix] private static bool OnReceive(Il2CppStructArray bytes, uint size, ulong messagerID) { if (!IsNetMessage(bytes)) { return true; } byte b = ((Il2CppArrayBase)(object)bytes)[2]; if (MessageHandlers.TryGetValue(b, out Action value)) { byte[] array = new byte[size - 3]; Array.Copy(Il2CppArrayBase.op_Implicit((Il2CppArrayBase)(object)bytes), 3L, array, 0L, size - 3); SNet_Player playerByID = NetHelper.GetPlayerByID(messagerID); value(array, playerByID); } else { Logger.Error($"Net (key={20556:X}) received message for packetId={b} with no handler, are we on the same version?"); } return false; } public static void SendBytes(byte[] data, byte packetId, SNet_Player target) { SendBytes(PrepareData(data, packetId), target); } public static void SendBytes(byte[] data, byte packetId, List targets) { SendBytes(PrepareData(data, packetId), targets); } private static void SendBytes(byte[] bytes, SNet_Player target) { Il2CppStructArray val = new Il2CppStructArray(bytes); SNet.Core.SendBytes(val, (SNet_SendQuality)2, 1, target); } private static void SendBytes(byte[] bytes, List targets) { Il2CppStructArray val = new Il2CppStructArray(bytes); SNet.Core.SendBytes(val, (SNet_SendQuality)2, 1, targets); } private static byte[] PrepareData(byte[] data, byte packetId = 0) { byte[] array = new byte[3 + data.Length]; ((Il2CppArrayBase)(object)BitConverter.GetBytes((ushort)20556)).CopyTo(array, 0); array[2] = packetId; data.CopyTo(array, 3); return array; } private static bool IsNetMessage(Il2CppStructArray bytes) { if (((Il2CppArrayBase)(object)bytes).Length < 2) { return false; } return BitConverter.ToUInt16(bytes, 0) == 20556; } } public static class NetHelper { public static SNet_Player? GetPlayerByID(ulong id) { Enumerator enumerator = SNet.LobbyPlayers.GetEnumerator(); while (enumerator.MoveNext()) { SNet_Player current = enumerator.Current; if (current.Lookup == id) { return current; } } return null; } public static bool InvokeWithAllPlayers(Action sendMethod) { if (!SNet.IsInLobby) { return false; } Enumerator enumerator = SNet.Lobby.Players.GetEnumerator(); while (enumerator.MoveNext()) { SNet_Player current = enumerator.Current; if (!current.IsBot && !current.IsLocal) { sendMethod(current); } } return true; } } public readonly record struct PlugVersion { public readonly byte Major; public readonly byte Minor; public readonly byte Patch; public PlugVersion() { Major = 0; Minor = 0; Patch = 0; } public PlugVersion(byte major, byte minor, byte patch) { Major = 0; Minor = 0; Patch = 0; Major = major; Minor = minor; Patch = patch; } public PlugVersion(string version) { Major = 0; Minor = 0; Patch = 0; string[] array = version.Split('.'); byte result; byte result2; byte result3; if (array.Length != 3) { Logger.Error("Invalid version string '" + version + "', expected format 'major.minor.patch'."); } else if (!byte.TryParse(array[0], out result)) { Logger.Error($"Invalid major version '{array[0]}' in version string '{version}'."); } else if (!byte.TryParse(array[1], out result2)) { Logger.Error($"Invalid minor version '{array[1]}' in version string '{version}'."); } else if (!byte.TryParse(array[2], out result3)) { Logger.Error($"Invalid patch version '{array[2]}' in version string '{version}'."); } else { Major = result; Minor = result2; Patch = result3; } } public PlugVersion(byte[] version) { Major = 0; Minor = 0; Patch = 0; if (version.Length < 3) { Logger.Error($"Invalid version byte array of length {version.Length}, expected at least 3."); } else { Major = version[0]; Minor = version[1]; Patch = version[2]; } } public PlugVersion(byte[] data, int offset) { Major = 0; Minor = 0; Patch = 0; if (data.Length < offset + 3) { Logger.Error($"Invalid version byte array of length {data.Length} with offset {offset}, expected at least {offset + 3}."); } else { Major = data[offset]; Minor = data[offset + 1]; Patch = data[offset + 2]; } } public byte[] ToByteArray() { return new byte[3] { Major, Minor, Patch }; } public static bool operator >(PlugVersion a, PlugVersion b) { if (a.Major != b.Major) { return a.Major > b.Major; } if (a.Minor != b.Minor) { return a.Minor > b.Minor; } return a.Patch > b.Patch; } public static bool operator <(PlugVersion a, PlugVersion b) { if (a.Major != b.Major) { return a.Major < b.Major; } if (a.Minor != b.Minor) { return a.Minor < b.Minor; } return a.Patch < b.Patch; } public override string ToString() { return $"{Major}.{Minor}.{Patch}"; } } } namespace PlayerSync.Network.Impl { public static class NetImpl { } [RegisterIl2Cpp] public class PeerInfoManager : MonoBehaviour { public enum PeerSupport { Unknown, Supported, NotSupported } public class PeerInfo { public PeerSupport Support; public PlugVersion PlugVersion = new PlugVersion(); public SNet_Player? Player; public byte RequestCount { get; private set; } public bool MaxRequestsReached => RequestCount >= 7; public void IncrementRequestCount() { RequestCount++; } } public const byte PacketId = byte.MaxValue; public const byte KeyRequest = 105; public const byte KeyResponse = 103; public const int MaxRequestCount = 7; private const float PeerInfoUpdateInterval = 4f; private float _timeSinceLastUpdate; public static PeerInfoManager? Instance { get; private set; } private Dictionary PeerInfos { get; } = new Dictionary(); private void Awake() { if ((Object)(object)Instance == (Object)null) { Instance = this; } else if ((Object)(object)Instance != (Object)(object)this) { Object.Destroy((Object)(object)this); Logger.Warn("Multiple instances of LobbyInfo detected, this should not happen!"); return; } Net.RegisterHandler(byte.MaxValue, HandlePacket); Events.OnPlayerJoinLobby += SendPeerInfoRequestSafe; } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } Net.UnregisterHandler(byte.MaxValue); Events.OnPlayerJoinLobby -= SendPeerInfoRequestSafe; } private void Update() { _timeSinceLastUpdate += Time.deltaTime; if (_timeSinceLastUpdate >= 4f) { UpdateLobbyInfo(); _timeSinceLastUpdate = 0f; } } public static bool Supported(SNet_Player player) { if ((Object)(object)Instance == (Object)null) { return false; } if (Instance.PeerInfos.TryGetValue(player.Lookup, out PeerInfo value)) { return value.Support == PeerSupport.Supported; } return false; } public static bool SupportUnknown(SNet_Player player) { if ((Object)(object)Instance == (Object)null) { return false; } if (Instance.PeerInfos.TryGetValue(player.Lookup, out PeerInfo value)) { return value.Support == PeerSupport.Unknown; } return false; } public static bool Unsupported(SNet_Player player) { if ((Object)(object)Instance == (Object)null) { return false; } if (Instance.PeerInfos.TryGetValue(player.Lookup, out PeerInfo value)) { return value.Support == PeerSupport.NotSupported; } return false; } private void UpdateLobbyInfo() { SNet_SessionHub sessionHub = SNet.SessionHub; foreach (ulong item in PeerInfos.Keys.ToList()) { if (!sessionHub.IsPlayerInHub(PeerInfos[item].Player)) { DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(58, 2); defaultInterpolatedStringHandler.AppendLiteral("Player '"); SNet_Player? player = PeerInfos[item].Player; defaultInterpolatedStringHandler.AppendFormatted(((player != null) ? player.NickName : null) ?? "???"); defaultInterpolatedStringHandler.AppendLiteral("' ("); defaultInterpolatedStringHandler.AppendFormatted(item); defaultInterpolatedStringHandler.AppendLiteral(") is not in lobby anymore, removing their info."); Logger.Debug(defaultInterpolatedStringHandler.ToStringAndClear()); PeerInfos.Remove(item); } } if (SNet.IsInLobby) { NetHelper.InvokeWithAllPlayers(RequestInfoFromPlayer); } } private void RequestInfoFromPlayer(SNet_Player player) { if (PeerInfos.TryGetValue(player.Lookup, out PeerInfo value)) { if (value.Support != 0) { return; } if (value.MaxRequestsReached) { value.Support = PeerSupport.NotSupported; Logger.Info($"Player '{player.GetName()}' ({player.Lookup}) reached max request count without response, marking as not supported."); return; } } SendPeerInfoRequestSafe(player); } private void SendPeerInfoRequestSafe(SNet_Player player) { if (!player.IsBot && !player.IsLocal) { if (!PeerInfos.TryGetValue(player.Lookup, out PeerInfo value)) { value = new PeerInfo(); PeerInfos[player.Lookup] = value; } SendPeerInfoRequest(player); value.IncrementRequestCount(); PeerInfo peerInfo = value; if (peerInfo.Player == null) { peerInfo.Player = player; } Logger.Debug($"Sent request for info to player '{player.GetName()}' ({player.Lookup}), RequestCount({value.RequestCount} of {7})"); } } private static void SendPeerInfoRequest(SNet_Player player) { if (!((Object)(object)player == (Object)null) && !player.IsBot) { Net.SendBytes(new byte[1] { 105 }, byte.MaxValue, player); } } private static void SendPeerInfoResponse(SNet_Player player) { if (!((Object)(object)player == (Object)null) && !player.IsBot) { byte[] array = new byte[4] { 103, 0, 0, 0 }; Array.Copy(Plugin.PlugVersion.ToByteArray(), 0, array, 1, 3); Net.SendBytes(array, byte.MaxValue, player); } } private void HandlePacket(byte[] data, SNet_Player? sender) { if ((Object)(object)sender == (Object)null) { Logger.Error("Received client info exchange packet with null sender, ignoring!"); return; } switch (data[0]) { case 105: SendPeerInfoResponse(sender); break; case 103: { if (data.Length < 4) { Logger.Error($"Received peer info response of invalid length {data.Length}, expected 4."); break; } if (!PeerInfos.TryGetValue(sender.Lookup, out PeerInfo value)) { value = new PeerInfo(); PeerInfos[sender.Lookup] = value; } value.Support = PeerSupport.Supported; value.PlugVersion = new PlugVersion(data, 1); value.Player = sender; Logger.Info($"Received peer info response from player '{sender.GetName()}' ({sender.Lookup}) version={value.PlugVersion}"); break; } } } } } namespace PlayerSync.Interop { [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class RegisterIl2CppAttribute : Attribute { } }