From 3d9018ab885bf1bd6fb6e054e8419ffc0ea53412 Mon Sep 17 00:00:00 2001 From: kmyuhkyuk <72241714+kmyuhkyuk@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:01:51 +0800 Subject: [PATCH] Remove ConnectToHost auto try NetID feature, Reimplemented as server send new NetID --- .../Helpers/LanHandshakeResponseHelper.cs | 59 +++++++ .../Models/ENetLanHandshakeResponse.cs | 15 ++ .../ENetClientConnectionInitializerPatch.cs | 73 -------- .../Patchs/ENetClientPatch.cs | 157 ++++++++++++++++++ .../Patchs/ENetHostPatch.cs | 94 +++++++++-- 5 files changed, 308 insertions(+), 90 deletions(-) create mode 100644 SlayTheSpire2.LAN.Multiplayer/Helpers/LanHandshakeResponseHelper.cs create mode 100644 SlayTheSpire2.LAN.Multiplayer/Models/ENetLanHandshakeResponse.cs delete mode 100644 SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientConnectionInitializerPatch.cs create mode 100644 SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientPatch.cs diff --git a/SlayTheSpire2.LAN.Multiplayer/Helpers/LanHandshakeResponseHelper.cs b/SlayTheSpire2.LAN.Multiplayer/Helpers/LanHandshakeResponseHelper.cs new file mode 100644 index 0000000..83cfb42 --- /dev/null +++ b/SlayTheSpire2.LAN.Multiplayer/Helpers/LanHandshakeResponseHelper.cs @@ -0,0 +1,59 @@ +using System.Buffers.Binary; +using MegaCrit.Sts2.Core.Multiplayer.Transport.ENet; +using SlayTheSpire2.LAN.Multiplayer.Models; + +namespace SlayTheSpire2.LAN.Multiplayer.Helpers +{ + internal class LanHandshakeResponseHelper + { + public static ENetPacket FromLanHandshakeResponse(ENetLanHandshakeResponse response) + { + var array = new byte[] + { + 1, + (byte)response.status, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + }; + var span = array.AsSpan(); + BinaryPrimitives.WriteUInt64BigEndian(span.Slice(2, 8), response.netId); + BinaryPrimitives.WriteUInt64BigEndian(span.Slice(10, 8), response.newNetId); + return new ENetPacket(array); + } + + public static ENetLanHandshakeResponse AsLanHandshakeResponse(ENetPacket eNetPacket) + { + if (eNetPacket.PacketType != ENetPacketType.HandshakeResponse) + { + throw new InvalidOperationException( + $"Attempted to interpret ENet packet of type {eNetPacket.PacketType} as handshake response"); + } + + var span = eNetPacket.AllBytes.AsSpan(); + + var status = (ENetHandshakeStatus)span[1]; + var netId = BinaryPrimitives.ReadUInt64BigEndian(span.Slice(2, 8)); + var newNetId = BinaryPrimitives.ReadUInt64BigEndian(span.Slice(10, 8)); + return new ENetLanHandshakeResponse + { + netId = netId, + newNetId = newNetId, + status = status + }; + } + } +} \ No newline at end of file diff --git a/SlayTheSpire2.LAN.Multiplayer/Models/ENetLanHandshakeResponse.cs b/SlayTheSpire2.LAN.Multiplayer/Models/ENetLanHandshakeResponse.cs new file mode 100644 index 0000000..5060e2a --- /dev/null +++ b/SlayTheSpire2.LAN.Multiplayer/Models/ENetLanHandshakeResponse.cs @@ -0,0 +1,15 @@ +using MegaCrit.Sts2.Core.Multiplayer.Transport.ENet; + +// ReSharper disable InconsistentNaming + +namespace SlayTheSpire2.LAN.Multiplayer.Models +{ + public struct ENetLanHandshakeResponse + { + public ENetHandshakeStatus status; + + public ulong netId; + + public ulong newNetId; + } +} \ No newline at end of file diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientConnectionInitializerPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientConnectionInitializerPatch.cs deleted file mode 100644 index 984052c..0000000 --- a/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientConnectionInitializerPatch.cs +++ /dev/null @@ -1,73 +0,0 @@ -using HarmonyLib; -using MegaCrit.Sts2.Core.Entities.Multiplayer; -using MegaCrit.Sts2.Core.Helpers; -using MegaCrit.Sts2.Core.Logging; -using MegaCrit.Sts2.Core.Multiplayer; -using MegaCrit.Sts2.Core.Multiplayer.Connection; -using MegaCrit.Sts2.Core.Multiplayer.Transport.ENet; -using MegaCrit.Sts2.Core.Platform; - -// ReSharper disable UnusedMember.Global -// ReSharper disable UnusedType.Global - -namespace SlayTheSpire2.LAN.Multiplayer.Patchs -{ - [HarmonyPatch(typeof(ENetClientConnectionInitializer), "Connect")] - internal class ENetClientConnectionInitializerPatch - { - private static bool Prefix(NetClientGameService gameService, - CancellationToken cancelToken, ulong ____netId, string ____ip, ushort ____port, ref Task __result) - { - __result = TaskHelper.RunSafely(Connect(gameService, cancelToken, ____netId, ____ip, ____port)); - - return false; - } - - private static async Task Connect(NetClientGameService gameService, - CancellationToken cancelToken, ulong netId, string ip, ushort port) - { - if (gameService.IsConnected) - { - throw new InvalidOperationException( - "NetClientGameService must not be connected when passed to ENetClientConnectionInitializer!"); - } - - var eNetClient = new ENetClient(gameService); - gameService.Initialize(eNetClient, PlatformType.None); - - var count = 0; - const int tryCount = 10; - NetErrorInfo? netErrorInfo = null; - - while (count < tryCount) - { - if (cancelToken.IsCancellationRequested) - return netErrorInfo; - - netErrorInfo = await eNetClient.ConnectToHost(netId, ip, port, cancelToken); - - if (!netErrorInfo.HasValue) - { - Log.Info($"Connect {ip}:{port} HostGame NetID:{netId}"); - return null; - } - - if (netErrorInfo.Value.GetReason() != NetError.Kicked) - return netErrorInfo; - - var nextNetId = netId + 1000u; - - if (count < tryCount - 1) - { - Log.Warn($"{ip}:{port} HostGame NetID:{netId} already occupied, Next will try NetID:{nextNetId}"); - } - - netId = nextNetId; - - count++; - } - - return netErrorInfo; - } - } -} \ No newline at end of file diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientPatch.cs new file mode 100644 index 0000000..713816e --- /dev/null +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetClientPatch.cs @@ -0,0 +1,157 @@ +using Godot; +using HarmonyLib; +using MegaCrit.Sts2.Core.Entities.Multiplayer; +using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Multiplayer.Transport; +using MegaCrit.Sts2.Core.Multiplayer.Transport.ENet; +using SlayTheSpire2.LAN.Multiplayer.Helpers; +using Logger = MegaCrit.Sts2.Core.Logging.Logger; + +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedType.Global + +namespace SlayTheSpire2.LAN.Multiplayer.Patchs +{ + [HarmonyPatch(typeof(ENetClient), "ConnectToHost")] + internal class ENetClientConnectToHostPatch + { + private static bool Prefix(ENetClient __instance, ulong netId, string ip, ushort port, + CancellationToken cancelToken, Logger ____logger, INetClientHandler ____handler, ref Task __result) + { + __result = TaskHelper.RunSafely(ConnectToHost(__instance, ____logger, ____handler, netId, ip, port, + cancelToken)); + + return false; + } + + private static async Task ConnectToHost(ENetClient eNetClient, Logger logger, INetClientHandler handler, ulong netId, string ip, ushort port, CancellationToken cancelToken) + { + while (true) + { + var connection = new ENetConnection(); + Traverse.Create(eNetClient).Field("_connection").SetValue(connection); + connection.CreateHost(); + var peer = connection.ConnectToHost(ip, port); + Traverse.Create(eNetClient).Field("_peer").SetValue(peer); + var timeoutTimer = 0; + while (!connection.TryService(out var output) || output is not { type: ENetConnection.EventType.Connect }) + { + await Task.Delay(100, cancelToken); + if (cancelToken.IsCancellationRequested) + { + eNetClient.DisconnectFromHost(NetError.CancelledJoin); + logger.Warn("User cancelled join flow"); + return null; + } + + timeoutTimer += 100; + if (timeoutTimer > 10000) + { + peer.Reset(); + logger.Error("Connection timed out!"); + return new NetErrorInfo(NetError.Timeout, selfInitiated: false); + } + } + + if (peer.GetState() != ENetPacketPeer.PeerState.Connected) + { + logger.Error($"Connection to {ip}:{port} failed!"); + return new NetErrorInfo(NetError.UnknownNetworkError, selfInitiated: false); + } + + var bufferedPackets = new List(); + var (result, newNetId) = await SendAndWaitForNetIdAck(eNetClient, logger, peer, connection, netId, bufferedPackets, cancelToken); + if (result.HasValue) + { + peer.PeerDisconnect(); + + if (result.Value.GetReason() == NetError.Kicked) + { + netId = newNetId; + continue; + } + + return result; + } + + Traverse.Create(eNetClient).Field("_netId").SetValue(newNetId); + Traverse.Create(eNetClient).Field("_isConnected").SetValue(true); + handler.OnConnectedToHost(); + foreach (var item in bufferedPackets) + { + Traverse.Create(eNetClient).Method("HandleMessageReceived", item).GetValue(); + } + + return null; + } + } + + private static async Task<(NetErrorInfo?, ulong)> SendAndWaitForNetIdAck(ENetClient eNetClient, Logger logger, + ENetPacketPeer peer, ENetConnection connection, ulong netId, List bufferedPackets, + CancellationToken cancelToken) + { + logger.Info($"Sending handshake with net ID {netId}"); + var eNetPacket = ENetPacket.FromHandshakeRequest(new ENetHandshakeRequest + { + netId = netId + }); + peer.Send(0, eNetPacket.AllBytes, 1); + var receivedAck = false; + var timeoutTimer = 0; + while (!receivedAck) + { + await Task.Delay(100, cancelToken); + if (cancelToken.IsCancellationRequested) + { + logger.Warn("User cancelled join flow"); + eNetClient.DisconnectFromHost(NetError.CancelledJoin); + return (null, netId); + } + + if (connection.TryService(out var output) && output is { type: ENetConnection.EventType.Receive }) + { + var packetData = output.Value.packetData; + var eNetPacket2 = new ENetPacket(packetData); + if (eNetPacket2.PacketType == ENetPacketType.ApplicationMessage) + { + bufferedPackets.Add(output.Value); + continue; + } + + var eNetHandshakeResponse = LanHandshakeResponseHelper.AsLanHandshakeResponse(eNetPacket2); + if (eNetHandshakeResponse.netId != netId) + { + logger.Error( + $"Received net ID ({eNetHandshakeResponse.netId}) during handshake that did not match ours!"); + return (new NetErrorInfo(NetError.InternalError, selfInitiated: false), netId); + } + + if (eNetHandshakeResponse.status == ENetHandshakeStatus.IdCollision) + { + logger.Warn( + $"NetID:{netId} already occupied, Next try server send new NetID:{eNetHandshakeResponse.newNetId}"); + return (new NetErrorInfo(NetError.Kicked, selfInitiated: false), + eNetHandshakeResponse.newNetId); + } + else if (eNetHandshakeResponse.status != ENetHandshakeStatus.Success) + { + logger.Error($"Received non-success code during handshake ({eNetHandshakeResponse.status})!"); + return (new NetErrorInfo(NetError.Kicked, selfInitiated: false), netId); + } + + receivedAck = true; + } + + timeoutTimer += 100; + if (timeoutTimer > 10000) + { + logger.Error("Timed out waiting for handshake ack!"); + eNetClient.DisconnectFromHost(NetError.Timeout); + return (new NetErrorInfo(NetError.Timeout, selfInitiated: false), netId); + } + } + + return (null, netId); + } + } +} \ No newline at end of file diff --git a/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetHostPatch.cs b/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetHostPatch.cs index 41a8028..f12b3f7 100644 --- a/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetHostPatch.cs +++ b/SlayTheSpire2.LAN.Multiplayer/Patchs/ENetHostPatch.cs @@ -1,39 +1,99 @@ -using System.Reflection; -using System.Reflection.Emit; +using System.Collections; using Godot; using HarmonyLib; +using MegaCrit.Sts2.Core.Helpers; +using MegaCrit.Sts2.Core.Multiplayer.Transport; using MegaCrit.Sts2.Core.Multiplayer.Transport.ENet; +using SlayTheSpire2.LAN.Multiplayer.Helpers; +using SlayTheSpire2.LAN.Multiplayer.Models; +using Logger = MegaCrit.Sts2.Core.Logging.Logger; // ReSharper disable UnusedMember.Global // ReSharper disable UnusedType.Global namespace SlayTheSpire2.LAN.Multiplayer.Patchs { - [HarmonyPatch] + [HarmonyPatch(typeof(ENetHost), "DoClientHandshake")] internal class ENetHostDoClientHandshakePatch { - private static MethodInfo TargetMethod() + private static bool Prefix(ENetHost __instance, ENetPacketPeer peer, Logger ____logger, + IList ____receivedHandshakes, IList ____connectedPeers, INetHostHandler ____handler, ref Task __result) { - return AccessTools.AsyncMoveNext(typeof(ENetHost).GetMethod("DoClientHandshake", - BindingFlags.Instance | BindingFlags.NonPublic)); + __result = TaskHelper.RunSafely(DoClientHandshake(__instance, ____logger, ____receivedHandshakes, + ____connectedPeers, ____handler, peer)); + + return false; } - private static IEnumerable Transpiler(IEnumerable instructions) + private static async Task DoClientHandshake(ENetHost eNetHost, Logger logger, IList receivedHandshakes, + IList connectedPeers, INetHostHandler handler, ENetPacketPeer peer) { - //Fixed IdCollision HandshakeResponse will not send - - var peerDisconnectMethod = AccessTools.Method(typeof(ENetPacketPeer), "PeerDisconnect"); - var peerDisconnectLaterMethod = AccessTools.Method(typeof(ENetPacketPeer), "PeerDisconnectLater"); - - foreach (var instruction in instructions) + peer.SetTimeout(24, 20000, 20000); + var timeoutTimer = 0; + object? handshake = null; + while (handshake == null) { - if (instruction.opcode == OpCodes.Callvirt && instruction.operand as MethodInfo == peerDisconnectMethod) + foreach (var receivedHandshake in receivedHandshakes) { - yield return new CodeInstruction(OpCodes.Callvirt, peerDisconnectLaterMethod); - continue; + if (Traverse.Create(receivedHandshake).Field("conn").Field("peer").GetValue() == peer) + { + handshake = receivedHandshake; + break; + } } - yield return instruction; + if (handshake == null) + { + await Task.Delay(100); + timeoutTimer += 100; + if (timeoutTimer >= 10000) + { + logger.Error("Timed out waiting for handshake!"); + peer.Reset(); + return; + } + } + } + + var handshakeConn = Traverse.Create(handshake).Field("conn").GetValue(); + var handshakeNetId = Traverse.Create(handshakeConn).Field("netId").GetValue(); + var handshakePeer = Traverse.Create(handshakeConn).Field("peer").GetValue(); + + var connectedPeerIdHashSet = new HashSet(eNetHost.ConnectedPeerIds); + + if (connectedPeerIdHashSet.Contains(handshakeNetId)) + { + var newNetId = handshakeNetId; + + while (connectedPeerIdHashSet.Contains(newNetId)) + { + newNetId += 1000; + } + + logger.Info( + $"Second client attempted to connect with peer ID {handshakeNetId}, disconnecting them and return new NetId:{newNetId}"); + + var eNetPacket = LanHandshakeResponseHelper.FromLanHandshakeResponse(new ENetLanHandshakeResponse + { + netId = handshakeNetId, + newNetId = newNetId, + status = ENetHandshakeStatus.IdCollision + }); + handshakePeer.Send(0, eNetPacket.AllBytes, 1); + handshakePeer.PeerDisconnectLater(); + } + else + { + logger.Debug($"Acknowledging handshake for peer with ID {handshakeNetId}"); + var eNetPacket2 = LanHandshakeResponseHelper.FromLanHandshakeResponse(new ENetLanHandshakeResponse + { + netId = handshakeNetId, + newNetId = handshakeNetId, + status = ENetHandshakeStatus.Success + }); + handshakePeer.Send(0, eNetPacket2.AllBytes, 1); + connectedPeers.Add(handshakeConn); + handler.OnPeerConnected(handshakeNetId); } } }