From 9b06cf4bbfe3fac05e4a166b8154314cdd0fe0e2 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 10 Sep 2025 03:22:32 +0200 Subject: [PATCH 1/7] Extract Steam P2P logic into a dedicated class --- Source/Client/Networking/HostUtil.cs | 2 +- Source/Client/Networking/NetworkingSteam.cs | 112 ++++++++++++++++++- Source/Client/Networking/SteamIntegration.cs | 97 +--------------- Source/Client/OnMainThread.cs | 2 +- 4 files changed, 112 insertions(+), 101 deletions(-) diff --git a/Source/Client/Networking/HostUtil.cs b/Source/Client/Networking/HostUtil.cs index 9e4c2bd9b..16b0ec342 100644 --- a/Source/Client/Networking/HostUtil.cs +++ b/Source/Client/Networking/HostUtil.cs @@ -71,7 +71,7 @@ private static void PrepareLocalServer(ServerSettings settings, bool fromReplay) localServer.worldData.spectatorFactionId = Multiplayer.WorldComp.spectatorFaction.loadID; if (settings.steam) - localServer.TickEvent += SteamIntegration.ServerSteamNetTick; + localServer.TickEvent += SteamP2PIntegration.ServerSteamNetTick; if (fromReplay) { diff --git a/Source/Client/Networking/NetworkingSteam.cs b/Source/Client/Networking/NetworkingSteam.cs index f07dd1da1..b6bae7565 100644 --- a/Source/Client/Networking/NetworkingSteam.cs +++ b/Source/Client/Networking/NetworkingSteam.cs @@ -1,7 +1,9 @@ -using Multiplayer.Common; -using Steamworks; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using Multiplayer.Common; +using Steamworks; using Verse; namespace Multiplayer.Client.Networking @@ -75,8 +77,9 @@ protected override void HandleReceiveMsg(int msgId, int fragState, ByteReader re public override void OnError(EP2PSessionError error) { - Multiplayer.session.disconnectInfo.titleTranslated = - error == EP2PSessionError.k_EP2PSessionErrorTimeout ? "MpSteamTimedOut".Translate() : "MpSteamGenericError".Translate(); + Multiplayer.session.disconnectInfo.titleTranslated = error == EP2PSessionError.k_EP2PSessionErrorTimeout + ? "MpSteamTimedOut".Translate() + : "MpSteamGenericError".Translate(); OnDisconnect(); } @@ -128,4 +131,105 @@ private void OnDisconnect() serverPlayer.Server.playerManager.SetDisconnected(this, MpDisconnectReason.ClientLeft); } } + + public static class SteamP2PIntegration + { + private static Callback p2pFail; + + public static void InitCallbacks() + { + p2pFail = Callback.Create(fail => + { + var session = Multiplayer.session; + if (session == null) return; + + var remoteId = fail.m_steamIDRemote; + var error = (EP2PSessionError)fail.m_eP2PSessionError; + + if (Multiplayer.Client is SteamBaseConn clientConn && clientConn.remoteId == remoteId) + clientConn.OnError(error); + + var server = Multiplayer.LocalServer; + if (server == null) return; + + server.Enqueue(() => + { + var conn = server.playerManager.Players.Select(p => p.conn).OfType() + .FirstOrDefault(c => c.remoteId == remoteId); + conn?.OnError(error); + }); + }); + } + + public struct SteamPacket + { + public CSteamID remote; + public ByteReader data; + public bool joinPacket; + public bool reliable; + public ushort channel; + } + + public static IEnumerable ReadPackets(int recvChannel) + { + while (SteamNetworking.IsP2PPacketAvailable(out uint size, recvChannel)) + { + byte[] data = new byte[size]; + + if (!SteamNetworking.ReadP2PPacket(data, size, out uint sizeRead, out CSteamID remote, recvChannel)) + continue; + if (data.Length <= 0) continue; + + var reader = new ByteReader(data); + byte flags = reader.ReadByte(); + bool joinPacket = (flags & 1) > 0; + bool reliable = (flags & 2) > 0; + bool hasChannel = (flags & 4) > 0; + ushort channel = hasChannel ? reader.ReadUShort() : (ushort)0; + + yield return new SteamPacket() + { + remote = remote, data = reader, joinPacket = joinPacket, reliable = reliable, channel = channel + }; + } + } + + public static void ServerSteamNetTick(MultiplayerServer server) + { + foreach (var packet in ReadPackets(0)) + { + var playerManager = server.playerManager; + var player = GenCollection.FirstOrDefault(playerManager.Players, + p => p.conn is SteamBaseConn conn && conn.remoteId == packet.remote); + + if (packet.joinPacket && player == null) + { + ConnectionBase conn = new SteamServerConn(packet.remote, packet.channel); + + var preConnect = playerManager.OnPreConnect(packet.remote); + if (preConnect != null) + { + conn.Close(preConnect.Value); + continue; + } + + conn.ChangeState(ConnectionStateEnum.ServerJoining); + player = playerManager.OnConnected(conn); + player.type = PlayerType.Steam; + + player.steamId = (ulong)packet.remote; + player.steamPersonaName = SteamFriends.GetFriendPersonaName(packet.remote); + if (player.steamPersonaName.Length == 0) + player.steamPersonaName = "[unknown]"; + + conn.Send(Packets.Server_SteamAccept); + } + + if (!packet.joinPacket && player != null) + { + player.HandleReceive(packet.data, packet.reliable); + } + } + } + } } diff --git a/Source/Client/Networking/SteamIntegration.cs b/Source/Client/Networking/SteamIntegration.cs index f9436187b..49e483273 100644 --- a/Source/Client/Networking/SteamIntegration.cs +++ b/Source/Client/Networking/SteamIntegration.cs @@ -1,11 +1,9 @@ using System; -using Multiplayer.Client.Networking; -using Multiplayer.Common; -using Steamworks; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; +using Multiplayer.Client.Networking; using RimWorld; +using Steamworks; using UnityEngine; using Verse; @@ -15,7 +13,6 @@ public static class SteamIntegration { // Callbacks stored in static fields so they don't get garbage collected private static Callback sessionReq; - private static Callback p2pFail; private static Callback friendRchpUpdate; private static Callback gameJoinReq; private static Callback personaChange; @@ -65,87 +62,6 @@ public static void InitCallbacks() personaChange = Callback.Create(change => { }); - - p2pFail = Callback.Create(fail => - { - var session = Multiplayer.session; - if (session == null) return; - - var remoteId = fail.m_steamIDRemote; - var error = (EP2PSessionError)fail.m_eP2PSessionError; - - if (Multiplayer.Client is SteamBaseConn clientConn && clientConn.remoteId == remoteId) - clientConn.OnError(error); - - var server = Multiplayer.LocalServer; - if (server == null) return; - - server.Enqueue(() => - { - var conn = server.playerManager.Players.Select(p => p.conn).OfType().FirstOrDefault(c => c.remoteId == remoteId); - if (conn != null) - conn.OnError(error); - }); - }); - } - - public static IEnumerable ReadPackets(int recvChannel) - { - while (SteamNetworking.IsP2PPacketAvailable(out uint size, recvChannel)) - { - byte[] data = new byte[size]; - - if (!SteamNetworking.ReadP2PPacket(data, size, out uint sizeRead, out CSteamID remote, recvChannel)) continue; - if (data.Length <= 0) continue; - - var reader = new ByteReader(data); - byte flags = reader.ReadByte(); - bool joinPacket = (flags & 1) > 0; - bool reliable = (flags & 2) > 0; - bool hasChannel = (flags & 4) > 0; - ushort channel = hasChannel ? reader.ReadUShort() : (ushort)0; - - yield return new SteamPacket() { - remote = remote, data = reader, joinPacket = joinPacket, reliable = reliable, channel = channel - }; - } - } - - public static void ServerSteamNetTick(MultiplayerServer server) - { - foreach (var packet in ReadPackets(0)) - { - var playerManager = server.playerManager; - var player = playerManager.Players.FirstOrDefault(p => p.conn is SteamBaseConn conn && conn.remoteId == packet.remote); - - if (packet.joinPacket && player == null) - { - ConnectionBase conn = new SteamServerConn(packet.remote, packet.channel); - - var preConnect = playerManager.OnPreConnect(packet.remote); - if (preConnect != null) - { - conn.Close(preConnect.Value); - continue; - } - - conn.ChangeState(ConnectionStateEnum.ServerJoining); - player = playerManager.OnConnected(conn); - player.type = PlayerType.Steam; - - player.steamId = (ulong)packet.remote; - player.steamPersonaName = SteamFriends.GetFriendPersonaName(packet.remote); - if (player.steamPersonaName.Length == 0) - player.steamPersonaName = "[unknown]"; - - conn.Send(Packets.Server_SteamAccept); - } - - if (!packet.joinPacket && player != null) - { - player.HandleReceive(packet.data, packet.reliable); - } - } } private static Stopwatch lastSteamUpdate = Stopwatch.StartNew(); @@ -189,15 +105,6 @@ public static CSteamID GetConnectHostId(CSteamID friend) } } - public struct SteamPacket - { - public CSteamID remote; - public ByteReader data; - public bool joinPacket; - public bool reliable; - public ushort channel; - } - public static class SteamImages { public static Dictionary cache = new(); diff --git a/Source/Client/OnMainThread.cs b/Source/Client/OnMainThread.cs index 0841fa8a4..1de1518eb 100644 --- a/Source/Client/OnMainThread.cs +++ b/Source/Client/OnMainThread.cs @@ -53,7 +53,7 @@ public void Update() Multiplayer.session.playerCursors.SendVisuals(); if (Multiplayer.Client is SteamBaseConn steamConn && SteamManager.Initialized) - foreach (var packet in SteamIntegration.ReadPackets(steamConn.recvChannel)) + foreach (var packet in SteamP2PIntegration.ReadPackets(steamConn.recvChannel)) // Note: receive can lead to disconnection if (steamConn.remoteId == packet.remote && Multiplayer.Client != null) ClientUtil.HandleReceive(packet.data, packet.reliable); From 0a32347933f1e9955c3fe9e7727ae3d988db2300 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 10 Sep 2025 03:28:49 +0200 Subject: [PATCH 2/7] Encapsulate SteamP2P networking --- Source/Client/MultiplayerStatic.cs | 8 +++- Source/Client/Networking/ClientUtil.cs | 7 +++- Source/Client/Networking/NetworkingSteam.cs | 42 +++++++++++---------- Source/Client/OnMainThread.cs | 7 +--- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 87fc76303..1d8482613 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using HarmonyLib; using LiteNetLib; +using Multiplayer.Client.Networking; using Multiplayer.Client.Patches; using Multiplayer.Client.Util; using Multiplayer.Common; @@ -65,7 +66,10 @@ static MultiplayerStatic() SetUsername(); if (SteamManager.Initialized) + { SteamIntegration.InitCallbacks(); + SteamP2PIntegration.InitCallbacks(); + } Log.Message($"Multiplayer version {MpVersion.Version}"); Log.Message($"Player's username: {Multiplayer.username}"); @@ -305,7 +309,7 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po Assembly.GetCallingAssembly().GetTypes().Do(type => { // EarlyPatches are handled in MultiplayerMod.EarlyPatches if (type.IsDefined(typeof(EarlyPatchAttribute))) return; - + var harmonyAttributes = HarmonyMethodExtensions.GetFromType(type); if (harmonyAttributes is null || harmonyAttributes.Count == 0) return; @@ -333,7 +337,7 @@ void TryPatch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod po foreach (Type t in typeof(Designator).AllSubtypesAndSelf() // Designator_MechControlGroup Opens float menu, sync that instead // Designator_Plan_CopySelection creates the placement gizmo, this shouldn't be synced - .Except([typeof(Designator_MechControlGroup), typeof(Designator_Plan_CopySelection)])) + .Except([typeof(Designator_MechControlGroup), typeof(Designator_Plan_CopySelection)])) { foreach ((string m, Type[] args) in designatorMethods) { diff --git a/Source/Client/Networking/ClientUtil.cs b/Source/Client/Networking/ClientUtil.cs index ef4ed6c62..c36e37a58 100644 --- a/Source/Client/Networking/ClientUtil.cs +++ b/Source/Client/Networking/ClientUtil.cs @@ -49,11 +49,11 @@ public static void TrySteamConnectWithWindow(CSteamID user, bool returnToServerB Multiplayer.Client.ChangeState(ConnectionStateEnum.ClientSteam); } - public static void HandleReceive(ByteReader data, bool reliable) + public static void DisconnectOnException(Action action) { try { - Multiplayer.Client.HandleReceiveRaw(data, reliable); + action(); } catch (Exception e) { @@ -65,6 +65,9 @@ public static void HandleReceive(ByteReader data, bool reliable) Multiplayer.StopMultiplayer(); } } + + public static void HandleReceive(ByteReader data, bool reliable) => + DisconnectOnException(() => Multiplayer.Client.HandleReceiveRaw(data, reliable)); } } diff --git a/Source/Client/Networking/NetworkingSteam.cs b/Source/Client/Networking/NetworkingSteam.cs index b6bae7565..18e4bd420 100644 --- a/Source/Client/Networking/NetworkingSteam.cs +++ b/Source/Client/Networking/NetworkingSteam.cs @@ -8,21 +8,14 @@ namespace Multiplayer.Client.Networking { - public abstract class SteamBaseConn : ConnectionBase + public abstract class SteamBaseConn(CSteamID remoteId, ushort recvChannel, ushort sendChannel) : ConnectionBase { - public readonly CSteamID remoteId; + public readonly CSteamID remoteId = remoteId; - public readonly ushort recvChannel; // currently only for client - public readonly ushort sendChannel; // currently only for server + public readonly ushort recvChannel = recvChannel; // currently only for client + public readonly ushort sendChannel = sendChannel; // currently only for server - public SteamBaseConn(CSteamID remoteId, ushort recvChannel, ushort sendChannel) - { - this.remoteId = remoteId; - this.recvChannel = recvChannel; - this.sendChannel = sendChannel; - } - - protected override void SendRaw(byte[] raw, bool reliable) + protected override void SendRaw(byte[] raw, bool reliable = true) { byte[] full = new byte[1 + raw.Length]; full[0] = reliable ? (byte)2 : (byte)0; @@ -42,7 +35,7 @@ public void SendRawSteam(byte[] raw, bool reliable) ); } - public override void Close(MpDisconnectReason reason, byte[] data) + public override void Close(MpDisconnectReason reason, byte[] data = null) { if (State != ConnectionStateEnum.ClientSteam) Send(Packets.Special_Steam_Disconnect, GetDisconnectBytes(reason, data)); @@ -60,6 +53,16 @@ public class SteamClientConn(CSteamID remoteId) : SteamBaseConn(remoteId, Random { static ushort RandomChannelId() => (ushort)new Random().Next(); + public void Tick() + { + foreach (var packet in SteamP2PIntegration.ReadPackets(recvChannel)) + { + // Note: receive can lead to disconnection + if (State == ConnectionStateEnum.Disconnected) return; + if (packet.remote == remoteId) HandleReceiveRaw(packet.data, packet.reliable); + } + } + protected override void HandleReceiveMsg(int msgId, int fragState, ByteReader reader, bool reliable) { if (msgId == (int)Packets.Special_Steam_Disconnect) @@ -161,7 +164,7 @@ public static void InitCallbacks() }); } - public struct SteamPacket + internal struct SteamPacket { public CSteamID remote; public ByteReader data; @@ -170,14 +173,13 @@ public struct SteamPacket public ushort channel; } - public static IEnumerable ReadPackets(int recvChannel) + internal static IEnumerable ReadPackets(int recvChannel) { while (SteamNetworking.IsP2PPacketAvailable(out uint size, recvChannel)) { byte[] data = new byte[size]; - if (!SteamNetworking.ReadP2PPacket(data, size, out uint sizeRead, out CSteamID remote, recvChannel)) - continue; + if (!SteamNetworking.ReadP2PPacket(data, size, out uint sizeRead, out CSteamID remote, recvChannel)) continue; if (data.Length <= 0) continue; var reader = new ByteReader(data); @@ -187,7 +189,7 @@ public static IEnumerable ReadPackets(int recvChannel) bool hasChannel = (flags & 4) > 0; ushort channel = hasChannel ? reader.ReadUShort() : (ushort)0; - yield return new SteamPacket() + yield return new SteamPacket { remote = remote, data = reader, joinPacket = joinPacket, reliable = reliable, channel = channel }; @@ -199,8 +201,8 @@ public static void ServerSteamNetTick(MultiplayerServer server) foreach (var packet in ReadPackets(0)) { var playerManager = server.playerManager; - var player = GenCollection.FirstOrDefault(playerManager.Players, - p => p.conn is SteamBaseConn conn && conn.remoteId == packet.remote); + var player = playerManager.Players + .FirstOrDefault(p => p.conn is SteamBaseConn conn && conn.remoteId == packet.remote); if (packet.joinPacket && player == null) { diff --git a/Source/Client/OnMainThread.cs b/Source/Client/OnMainThread.cs index 1de1518eb..28ed12a66 100644 --- a/Source/Client/OnMainThread.cs +++ b/Source/Client/OnMainThread.cs @@ -52,11 +52,8 @@ public void Update() !Multiplayer.session.desynced && Multiplayer.session.client.State == ConnectionStateEnum.ClientPlaying) Multiplayer.session.playerCursors.SendVisuals(); - if (Multiplayer.Client is SteamBaseConn steamConn && SteamManager.Initialized) - foreach (var packet in SteamP2PIntegration.ReadPackets(steamConn.recvChannel)) - // Note: receive can lead to disconnection - if (steamConn.remoteId == packet.remote && Multiplayer.Client != null) - ClientUtil.HandleReceive(packet.data, packet.reliable); + if (Multiplayer.Client is SteamClientConn steamConn) + ClientUtil.DisconnectOnException(steamConn.Tick); } public void OnApplicationQuit() From 7a1101c8988329de292ec26dc44858d54cd2af07 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 10 Sep 2025 04:39:11 +0200 Subject: [PATCH 3/7] Encapsulate LiteNetLib networking --- Source/Client/Networking/ClientUtil.cs | 21 +-- Source/Client/Networking/NetworkingLiteNet.cs | 124 +++++++++++++----- Source/Client/OnMainThread.cs | 9 +- Source/Client/Session/MultiplayerSession.cs | 4 - Source/Client/Windows/ChatWindow.cs | 6 +- Source/Common/Networking/LiteNetConnection.cs | 14 +- Source/Common/Networking/NetworkingLiteNet.cs | 4 +- Source/Common/Util/Extensions.cs | 5 + 8 files changed, 109 insertions(+), 78 deletions(-) diff --git a/Source/Client/Networking/ClientUtil.cs b/Source/Client/Networking/ClientUtil.cs index c36e37a58..739d02846 100644 --- a/Source/Client/Networking/ClientUtil.cs +++ b/Source/Client/Networking/ClientUtil.cs @@ -1,4 +1,3 @@ -using LiteNetLib; using Multiplayer.Common; using Steamworks; using System; @@ -19,18 +18,10 @@ public static void TryConnectWithWindow(string address, int port, bool returnToS port = port }; - NetManager netClient = new NetManager(new MpClientNetListener()) - { - EnableStatistics = true, - IPv6Enabled = MpUtil.SupportsIPv6() ? IPv6Mode.SeparateSocket : IPv6Mode.Disabled - }; - - netClient.Start(); - netClient.ReconnectDelay = 300; - netClient.MaxConnectAttempts = 8; - - Multiplayer.session.netClient = netClient; - netClient.Connect(address, port, ""); + var conn = ClientLiteNetConnection.Connect(address, port); + conn.username = Multiplayer.username; + Multiplayer.session.client = conn; + Multiplayer.session.ReapplyPrefs(); } public static void TrySteamConnectWithWindow(CSteamID user, bool returnToServerBrowser = true) @@ -65,9 +56,5 @@ public static void DisconnectOnException(Action action) Multiplayer.StopMultiplayer(); } } - - public static void HandleReceive(ByteReader data, bool reliable) => - DisconnectOnException(() => Multiplayer.Client.HandleReceiveRaw(data, reliable)); } - } diff --git a/Source/Client/Networking/NetworkingLiteNet.cs b/Source/Client/Networking/NetworkingLiteNet.cs index 3018552e6..9192ce0e1 100644 --- a/Source/Client/Networking/NetworkingLiteNet.cs +++ b/Source/Client/Networking/NetworkingLiteNet.cs @@ -1,68 +1,120 @@ +using System; using LiteNetLib; using Multiplayer.Common; using System.Net; using System.Net.Sockets; using Multiplayer.Client.Util; +using Verse; namespace Multiplayer.Client.Networking { - - public class MpClientNetListener : INetEventListener + public class ClientLiteNetConnection : LiteNetConnection { - public void OnPeerConnected(NetPeer peer) - { - ConnectionBase conn = new LiteNetConnection(peer); - conn.username = Multiplayer.username; - conn.ChangeState(ConnectionStateEnum.ClientJoining); + private readonly NetManager netManager; - Multiplayer.session.client = conn; - Multiplayer.session.ReapplyPrefs(); + private ClientLiteNetConnection(NetPeer peer, NetManager netManager) : base(peer) => + this.netManager = netManager; + + ~ClientLiteNetConnection() + { + if (netManager.IsRunning) + { + Log.Error("[ClientLiteNetConnection] NetManager did not get stopped"); + netManager.Stop(); + } + } - MpLog.Log("Net client connected"); + public static ClientLiteNetConnection Connect(string address, int port) + { + var netClient = new NetManager(new NetListener()) + { + EnableStatistics = true, + IPv6Enabled = MpUtil.SupportsIPv6() ? IPv6Mode.SeparateSocket : IPv6Mode.Disabled, + ReconnectDelay = 300, + MaxConnectAttempts = 8 + }; + netClient.Start(); + var peer = netClient.Connect(address, port, ""); + var conn = new ClientLiteNetConnection(peer, netClient); + peer.SetConnection(conn); + return conn; } - public void OnNetworkError(IPEndPoint endPoint, SocketError error) + public void Tick() => netManager.PollEvents(); + + public void OnDisconnect(MpDisconnectReason reason, ByteReader data) { - MpLog.Warn($"Net client error {error}"); + Multiplayer.session.ProcessDisconnectPacket(reason, data); + ConnectionStatusListeners.TryNotifyAll_Disconnected(); + Multiplayer.StopMultiplayer(); } - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod method) + public override void Close(MpDisconnectReason reason, byte[] data) { - byte[] data = reader.GetRemainingBytes(); - ClientUtil.HandleReceive(new ByteReader(data), method == DeliveryMethod.ReliableOrdered); + base.Close(reason, data); + netManager.Stop(); } - public void OnPeerDisconnected(NetPeer peer, DisconnectInfo info) + private class NetListener : INetEventListener { - MpDisconnectReason reason; - ByteReader reader; + private ClientLiteNetConnection GetConnection(NetPeer peer) => + peer.GetConnection() as ClientLiteNetConnection ?? throw new Exception("Can't get connection"); - if (info.AdditionalData.IsNull) + public void OnPeerConnected(NetPeer peer) { - if (info.Reason is DisconnectReason.DisconnectPeerCalled or DisconnectReason.RemoteConnectionClose) - reason = MpDisconnectReason.Generic; - else if (Multiplayer.Client == null) - reason = MpDisconnectReason.ConnectingFailed; + GetConnection(peer).ChangeState(ConnectionStateEnum.ClientJoining); + MpLog.Log("Net client connected"); + } + + public void OnNetworkError(IPEndPoint endPoint, SocketError error) + { + MpLog.Warn($"Net client error {error}"); + } + + public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod method) + { + byte[] data = reader.GetRemainingBytes(); + GetConnection(peer).HandleReceiveRaw(new ByteReader(data), method == DeliveryMethod.ReliableOrdered); + } + + public void OnPeerDisconnected(NetPeer peer, DisconnectInfo info) + { + MpDisconnectReason reason; + ByteReader reader; + + if (info.AdditionalData.IsNull) + { + if (info.Reason is DisconnectReason.DisconnectPeerCalled or DisconnectReason.RemoteConnectionClose) + reason = MpDisconnectReason.Generic; + else if (Multiplayer.Client == null) + reason = MpDisconnectReason.ConnectingFailed; + else + reason = MpDisconnectReason.NetFailed; + + reader = new ByteReader(ByteWriter.GetBytes(info.Reason)); + } else - reason = MpDisconnectReason.NetFailed; + { + reader = new ByteReader(info.AdditionalData.GetRemainingBytes()); + reason = reader.ReadEnum(); + } - reader = new ByteReader(ByteWriter.GetBytes(info.Reason)); + GetConnection(peer).OnDisconnect(reason, reader); + MpLog.Log($"Net client disconnected {info.Reason}"); } - else + + public void OnConnectionRequest(ConnectionRequest request) { - reader = new ByteReader(info.AdditionalData.GetRemainingBytes()); - reason = reader.ReadEnum(); } - Multiplayer.session.ProcessDisconnectPacket(reason, reader); - ConnectionStatusListeners.TryNotifyAll_Disconnected(); + public void OnNetworkLatencyUpdate(NetPeer peer, int latency) + { + } - Multiplayer.StopMultiplayer(); - MpLog.Log($"Net client disconnected {info.Reason}"); + public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, + UnconnectedMessageType messageType) + { + } } - - public void OnConnectionRequest(ConnectionRequest request) { } - public void OnNetworkLatencyUpdate(NetPeer peer, int latency) { } - public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { } } } diff --git a/Source/Client/OnMainThread.cs b/Source/Client/OnMainThread.cs index 28ed12a66..4406745c5 100644 --- a/Source/Client/OnMainThread.cs +++ b/Source/Client/OnMainThread.cs @@ -23,14 +23,7 @@ public void Update() MpInput.Update(); - try - { - Multiplayer.session?.netClient?.PollEvents(); - } - catch (Exception e) - { - Log.Error($"Exception handling network events: {e}"); - } + if (Multiplayer.Client is ClientLiteNetConnection netConn) ClientUtil.DisconnectOnException(netConn.Tick); queue.RunQueue(Log.Error); RunScheduled(); diff --git a/Source/Client/Session/MultiplayerSession.cs b/Source/Client/Session/MultiplayerSession.cs index 94d8222b0..ccbc020b7 100644 --- a/Source/Client/Session/MultiplayerSession.cs +++ b/Source/Client/Session/MultiplayerSession.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using Multiplayer.Client.Util; using UnityEngine; using Verse; @@ -25,7 +24,6 @@ public class MultiplayerSession : IConnectionStatusListener public int remoteSentCmds; public ConnectionBase client; - public NetManager netClient; public PacketLogWindow writerLog = new(true); public PacketLogWindow readerLog = new(false); public int myFactionId; @@ -72,8 +70,6 @@ public void Stop() client.ChangeState(ConnectionStateEnum.Disconnected); } - netClient?.Stop(); - if (arbiter != null) { arbiter.TryKill(); diff --git a/Source/Client/Windows/ChatWindow.cs b/Source/Client/Windows/ChatWindow.cs index 00b519fc6..c79067f61 100644 --- a/Source/Client/Windows/ChatWindow.cs +++ b/Source/Client/Windows/ChatWindow.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Text; using Multiplayer.Client.Factions; +using Multiplayer.Client.Networking; using Multiplayer.Client.Util; using UnityEngine; using Verse; @@ -384,9 +385,8 @@ void LogNetData(string name, NetStatistics stats) text.AppendLine(); } - var netClient = Multiplayer.session.netClient; - if (netClient != null) - LogNetData("Client", netClient.Statistics); + if (Multiplayer.Client is ClientLiteNetConnection conn) + LogNetData("Client", conn.peer.Statistics); if (Multiplayer.LocalServer != null) { diff --git a/Source/Common/Networking/LiteNetConnection.cs b/Source/Common/Networking/LiteNetConnection.cs index 082589145..d61bbc6e7 100644 --- a/Source/Common/Networking/LiteNetConnection.cs +++ b/Source/Common/Networking/LiteNetConnection.cs @@ -2,18 +2,16 @@ namespace Multiplayer.Common { - public class LiteNetConnection : ConnectionBase + public class LiteNetConnection(NetPeer peer) : ConnectionBase { - public readonly NetPeer peer; - - public LiteNetConnection(NetPeer peer) - { - this.peer = peer; - } + public readonly NetPeer peer = peer; protected override void SendRaw(byte[] raw, bool reliable) { - peer.Send(raw, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable); + if (peer.ConnectionState == ConnectionState.Connected) + peer.Send(raw, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable); + else + ServerLog.Error($"SendRaw() called with invalid connection state ({peer.EndPoint}): {peer.ConnectionState}"); } public override void Close(MpDisconnectReason reason, byte[]? data) diff --git a/Source/Common/Networking/NetworkingLiteNet.cs b/Source/Common/Networking/NetworkingLiteNet.cs index 289b3c627..9eb9a6c54 100644 --- a/Source/Common/Networking/NetworkingLiteNet.cs +++ b/Source/Common/Networking/NetworkingLiteNet.cs @@ -20,9 +20,9 @@ public void OnConnectionRequest(ConnectionRequest req) public void OnPeerConnected(NetPeer peer) { - ConnectionBase conn = new LiteNetConnection(peer); + var conn = new LiteNetConnection(peer); conn.ChangeState(ConnectionStateEnum.ServerJoining); - peer.Tag = conn; + peer.SetConnection(conn); var player = server.playerManager.OnConnected(conn); if (arbiter) diff --git a/Source/Common/Util/Extensions.cs b/Source/Common/Util/Extensions.cs index 384cb7d35..66f46686b 100644 --- a/Source/Common/Util/Extensions.cs +++ b/Source/Common/Util/Extensions.cs @@ -63,6 +63,11 @@ public static T[] SubArray(this T[] data, int index, int length) return result; } + public static void SetConnection(this NetPeer peer, LiteNetConnection conn) + { + peer.Tag = conn; + } + public static LiteNetConnection GetConnection(this NetPeer peer) { return (LiteNetConnection)peer.Tag; From e303270edb711d52788d88eec360a20f509c3663 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 10 Sep 2025 04:41:02 +0200 Subject: [PATCH 4/7] Unify client connection updating --- Source/Client/Networking/ClientUtil.cs | 5 +++++ Source/Client/Networking/NetworkingLiteNet.cs | 2 +- Source/Client/Networking/NetworkingSteam.cs | 2 +- Source/Client/OnMainThread.cs | 7 ++----- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Source/Client/Networking/ClientUtil.cs b/Source/Client/Networking/ClientUtil.cs index 739d02846..1e1c3c95c 100644 --- a/Source/Client/Networking/ClientUtil.cs +++ b/Source/Client/Networking/ClientUtil.cs @@ -6,6 +6,11 @@ namespace Multiplayer.Client { + public interface ITickableConnection + { + public void Tick(); + } + public static class ClientUtil { public static void TryConnectWithWindow(string address, int port, bool returnToServerBrowser = true) diff --git a/Source/Client/Networking/NetworkingLiteNet.cs b/Source/Client/Networking/NetworkingLiteNet.cs index 9192ce0e1..86ddaa13e 100644 --- a/Source/Client/Networking/NetworkingLiteNet.cs +++ b/Source/Client/Networking/NetworkingLiteNet.cs @@ -8,7 +8,7 @@ namespace Multiplayer.Client.Networking { - public class ClientLiteNetConnection : LiteNetConnection + public class ClientLiteNetConnection : LiteNetConnection, ITickableConnection { private readonly NetManager netManager; diff --git a/Source/Client/Networking/NetworkingSteam.cs b/Source/Client/Networking/NetworkingSteam.cs index 18e4bd420..c01c1241f 100644 --- a/Source/Client/Networking/NetworkingSteam.cs +++ b/Source/Client/Networking/NetworkingSteam.cs @@ -49,7 +49,7 @@ public override string ToString() } } - public class SteamClientConn(CSteamID remoteId) : SteamBaseConn(remoteId, RandomChannelId(), 0) + public class SteamClientConn(CSteamID remoteId) : SteamBaseConn(remoteId, RandomChannelId(), 0), ITickableConnection { static ushort RandomChannelId() => (ushort)new Random().Next(); diff --git a/Source/Client/OnMainThread.cs b/Source/Client/OnMainThread.cs index 4406745c5..09a0494a4 100644 --- a/Source/Client/OnMainThread.cs +++ b/Source/Client/OnMainThread.cs @@ -22,9 +22,6 @@ public void Update() return; MpInput.Update(); - - if (Multiplayer.Client is ClientLiteNetConnection netConn) ClientUtil.DisconnectOnException(netConn.Tick); - queue.RunQueue(Log.Error); RunScheduled(); @@ -45,8 +42,8 @@ public void Update() !Multiplayer.session.desynced && Multiplayer.session.client.State == ConnectionStateEnum.ClientPlaying) Multiplayer.session.playerCursors.SendVisuals(); - if (Multiplayer.Client is SteamClientConn steamConn) - ClientUtil.DisconnectOnException(steamConn.Tick); + if (Multiplayer.Client is ITickableConnection conn) + ClientUtil.DisconnectOnException(conn.Tick); } public void OnApplicationQuit() From 2261e33c3e9a23e7b1968c02d8eda081c124735e Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 10 Sep 2025 05:07:31 +0200 Subject: [PATCH 5/7] Encapsulate LiteNetLib logging into LiteNetLogger --- Source/Client/MultiplayerStatic.cs | 3 +-- Source/Client/Networking/NetworkingLiteNet.cs | 13 +++++++++++++ Source/Common/Util/ServerLog.cs | 11 +---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Source/Client/MultiplayerStatic.cs b/Source/Client/MultiplayerStatic.cs index 1d8482613..ffd5f2904 100644 --- a/Source/Client/MultiplayerStatic.cs +++ b/Source/Client/MultiplayerStatic.cs @@ -7,7 +7,6 @@ using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using HarmonyLib; -using LiteNetLib; using Multiplayer.Client.Networking; using Multiplayer.Client.Patches; using Multiplayer.Client.Util; @@ -61,7 +60,7 @@ static MultiplayerStatic() // UnityEngine.Debug.Log instead of Verse.Log.Message because the server runs on its own thread ServerLog.info = str => Debug.Log($"MpServerLog: {str}"); ServerLog.error = str => Debug.Log($"MpServerLog Error: {str}"); - NetDebug.Logger = new ServerLog(); + LiteNetLogger.Install(); SetUsername(); diff --git a/Source/Client/Networking/NetworkingLiteNet.cs b/Source/Client/Networking/NetworkingLiteNet.cs index 86ddaa13e..0f1dbe540 100644 --- a/Source/Client/Networking/NetworkingLiteNet.cs +++ b/Source/Client/Networking/NetworkingLiteNet.cs @@ -117,4 +117,17 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead } } } + + public class LiteNetLogger : INetLogger + { + public static void Install() => NetDebug.Logger = new LiteNetLogger(); + + public void WriteNet(NetLogLevel level, string str, params object[] args) + { + if (level == NetLogLevel.Error) + ServerLog.Error(string.Format(str, args)); + else + ServerLog.Log(string.Format(str, args)); + } + } } diff --git a/Source/Common/Util/ServerLog.cs b/Source/Common/Util/ServerLog.cs index c5e124c77..fa21f035c 100644 --- a/Source/Common/Util/ServerLog.cs +++ b/Source/Common/Util/ServerLog.cs @@ -1,9 +1,8 @@ using System; -using LiteNetLib; namespace Multiplayer.Common { - public class ServerLog : INetLogger + public class ServerLog { public static Action? info; public static Action? error; @@ -37,13 +36,5 @@ public static void Verbose(string s) if (verboseEnabled) Console.WriteLine($"(Verbose) {s}"); } - - public void WriteNet(NetLogLevel level, string str, params object[] args) - { - if (level == NetLogLevel.Error) - Error(string.Format(str, args)); - else - Log(string.Format(str, args)); - } } } From a1369292cff12ad22915afef229fec6e28115f98 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 10 Sep 2025 05:12:46 +0200 Subject: [PATCH 6/7] Encapsulate LAN server discovery logic into LanListener --- Source/Client/Windows/ServerBrowser.cs | 121 ++++++++++++------------- Source/Common/LiteNetManager.cs | 3 +- Source/Common/MultiplayerServer.cs | 2 + 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/Source/Client/Windows/ServerBrowser.cs b/Source/Client/Windows/ServerBrowser.cs index 8f57af49f..8ce3edbfb 100644 --- a/Source/Client/Windows/ServerBrowser.cs +++ b/Source/Client/Windows/ServerBrowser.cs @@ -21,32 +21,13 @@ namespace Multiplayer.Client { public class ServerBrowser : Window { - private NetManager lanListener; - private List servers = new(); + private LanListener lanListener; public override Vector2 InitialSize => new(800f, 500f); public ServerBrowser() { - EventBasedNetListener listener = new EventBasedNetListener(); - listener.NetworkReceiveUnconnectedEvent += (endpoint, data, type) => - { - if (type != UnconnectedMessageType.Broadcast) return; - - string s = Encoding.UTF8.GetString(data.GetRemainingBytes()); - if (s == "mp-server") - AddOrUpdate(endpoint); - }; - - lanListener = new NetManager(listener) - { - BroadcastReceiveEnabled = true, - ReuseAddress = true, - IPv6Enabled = IPv6Mode.Disabled - }; - - lanListener.Start(5100); - + lanListener = new LanListener(expirationMillis: 5000); doCloseX = true; } @@ -562,7 +543,7 @@ private void DrawLan(Rect inRect) float margin = 100; Rect outRect = new Rect(margin, inRect.yMin + 10, inRect.width - 2 * margin, inRect.height - 20); - float height = servers.Count * 40; + float height = lanListener.foundServers.Count * 40; Rect viewRect = new Rect(0, 0, outRect.width - 16f, height); Widgets.BeginScrollView(outRect, ref lanScroll, viewRect, true); @@ -570,7 +551,7 @@ private void DrawLan(Rect inRect) float y = 0; int i = 0; - foreach (LanServer server in servers) + foreach (var server in lanListener.foundServers) { Rect entryRect = new Rect(0, y, viewRect.width, 40); if (i % 2 == 0) @@ -599,24 +580,12 @@ private void DrawLan(Rect inRect) public override void WindowUpdate() { - UpdateLan(); + lanListener.Update(); if (SteamManager.Initialized) UpdateSteam(); } - private void UpdateLan() - { - lanListener.PollEvents(); - - for (int i = servers.Count - 1; i >= 0; i--) - { - LanServer server = servers[i]; - if (Multiplayer.clock.ElapsedMilliseconds - server.lastUpdate > 5000) - servers.RemoveAt(i); - } - } - private long lastFriendUpdate; private void UpdateSteam() @@ -657,7 +626,7 @@ public override void PostClose() public void Cleanup(bool sync) { - void Stop(object s) => lanListener.Stop(); + void Stop(object s) => lanListener.Dispose(); if (sync) Stop(null); @@ -665,30 +634,6 @@ public void Cleanup(bool sync) ThreadPool.QueueUserWorkItem(Stop); } - private void AddOrUpdate(IPEndPoint endpoint) - { - LanServer server = servers.Find(s => s.endpoint.Equals(endpoint)); - - if (server == null) - { - servers.Add(new LanServer() - { - endpoint = endpoint, - lastUpdate = Multiplayer.clock.ElapsedMilliseconds - }); - } - else - { - server.lastUpdate = Multiplayer.clock.ElapsedMilliseconds; - } - } - - class LanServer - { - public IPEndPoint endpoint; - public long lastUpdate; - } - public override void OnAcceptKeyPressed() { if (tab == Tabs.Direct) @@ -709,4 +654,58 @@ public class SteamPersona public CSteamID serverHost = CSteamID.Nil; } + public class LanListener : IDisposable + { + private NetManager netManager; + private int expirationMillis; + public readonly List foundServers = []; + + public class Server + { + public IPEndPoint endpoint; + public long lastUpdate; + } + + public LanListener(int expirationMillis) + { + this.expirationMillis = expirationMillis; + var listener = new EventBasedNetListener(); + listener.NetworkReceiveUnconnectedEvent += (endpoint, data, type) => + { + if (type != UnconnectedMessageType.Broadcast) return; + + string s = Encoding.UTF8.GetString(data.GetRemainingBytes()); + if (s != MultiplayerServer.LanBroadcastName) return; + var server = foundServers.Find(server => Equals(server.endpoint, endpoint)); + if (server == null) + { + server = new Server { endpoint = endpoint }; + foundServers.Add(server); + } + server.lastUpdate = Multiplayer.clock.ElapsedMilliseconds; + }; + + netManager = new NetManager(listener) + { + BroadcastReceiveEnabled = true, + ReuseAddress = true, + IPv6Enabled = IPv6Mode.Disabled + }; + + netManager.Start(MultiplayerServer.LanBroadcastPort); + } + + public void Update() + { + netManager.PollEvents(); + for (var i = foundServers.Count - 1; i >= 0; i--) + { + Server server = foundServers[i]; + if (Multiplayer.clock.ElapsedMilliseconds - server.lastUpdate > expirationMillis) + foundServers.RemoveAt(i); + } + } + + public void Dispose() => netManager.Stop(); + } } diff --git a/Source/Common/LiteNetManager.cs b/Source/Common/LiteNetManager.cs index f0bfbb79f..a1e3b8369 100644 --- a/Source/Common/LiteNetManager.cs +++ b/Source/Common/LiteNetManager.cs @@ -34,7 +34,8 @@ public void Tick() arbiter?.PollEvents(); if (lanManager != null && broadcastTimer % 60 == 0) - lanManager.SendBroadcast(Encoding.UTF8.GetBytes("mp-server"), 5100); + lanManager.SendBroadcast(Encoding.UTF8.GetBytes(MultiplayerServer.LanBroadcastName), + MultiplayerServer.LanBroadcastPort); broadcastTimer++; } diff --git a/Source/Common/MultiplayerServer.cs b/Source/Common/MultiplayerServer.cs index a055c31d5..e64e74cbb 100644 --- a/Source/Common/MultiplayerServer.cs +++ b/Source/Common/MultiplayerServer.cs @@ -21,6 +21,8 @@ static MultiplayerServer() public static MultiplayerServer? instance; public const int DefaultPort = 30502; + public const int LanBroadcastPort = 5100; + public const string LanBroadcastName = "mp-server"; public const int MaxUsernameLength = 15; public const int MinUsernameLength = 3; public const char EndpointSeparator = '&'; From 16886bb03e7cae3a4c68eaa62ec4308b948b8e40 Mon Sep 17 00:00:00 2001 From: mibac138 <5672750+mibac138@users.noreply.github.com> Date: Wed, 10 Sep 2025 05:30:46 +0200 Subject: [PATCH 7/7] Inline single-use helper method --- Source/Client/Networking/ClientUtil.cs | 18 ------------------ Source/Client/OnMainThread.cs | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Source/Client/Networking/ClientUtil.cs b/Source/Client/Networking/ClientUtil.cs index 1e1c3c95c..7e6bb23a6 100644 --- a/Source/Client/Networking/ClientUtil.cs +++ b/Source/Client/Networking/ClientUtil.cs @@ -1,6 +1,5 @@ using Multiplayer.Common; using Steamworks; -using System; using Verse; using Multiplayer.Client.Networking; @@ -44,22 +43,5 @@ public static void TrySteamConnectWithWindow(CSteamID user, bool returnToServerB Multiplayer.session.ReapplyPrefs(); Multiplayer.Client.ChangeState(ConnectionStateEnum.ClientSteam); } - - public static void DisconnectOnException(Action action) - { - try - { - action(); - } - catch (Exception e) - { - Log.Error($"Exception handling packet by {Multiplayer.Client}: {e}"); - - Multiplayer.session.disconnectInfo.titleTranslated = "MpPacketErrorLocal".Translate(); - - ConnectionStatusListeners.TryNotifyAll_Disconnected(); - Multiplayer.StopMultiplayer(); - } - } } } diff --git a/Source/Client/OnMainThread.cs b/Source/Client/OnMainThread.cs index 09a0494a4..9dd57dae0 100644 --- a/Source/Client/OnMainThread.cs +++ b/Source/Client/OnMainThread.cs @@ -43,7 +43,21 @@ public void Update() Multiplayer.session.playerCursors.SendVisuals(); if (Multiplayer.Client is ITickableConnection conn) - ClientUtil.DisconnectOnException(conn.Tick); + { + try + { + conn.Tick(); + } + catch (Exception e) + { + Log.Error($"Exception handling packet by {conn}: {e}"); + + Multiplayer.session.disconnectInfo.titleTranslated = "MpPacketErrorLocal".Translate(); + + ConnectionStatusListeners.TryNotifyAll_Disconnected(); + Multiplayer.StopMultiplayer(); + } + } } public void OnApplicationQuit()