Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Source/Client/Networking/ClientUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@ public static void HandleReceive(ByteReader data, bool reliable)
{
Log.Error($"Exception handling packet by {Multiplayer.Client}: {e}");

Multiplayer.session.disconnectInfo.titleTranslated = "MpPacketErrorLocal".Translate();
var info = new SessionDisconnectInfo
{
titleTranslated = "MpPacketErrorLocal".Translate()
};

ConnectionStatusListeners.TryNotifyAll_Disconnected();
ConnectionStatusListeners.TryNotifyAll_Disconnected(info);
Multiplayer.StopMultiplayer();
}
}
Expand Down
6 changes: 3 additions & 3 deletions Source/Client/Networking/ConnectionStatusListeners.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Multiplayer.Client.Networking
public interface IConnectionStatusListener
{
void Connected();
void Disconnected();
void Disconnected(SessionDisconnectInfo info);
}

public static class ConnectionStatusListeners
Expand Down Expand Up @@ -45,13 +45,13 @@ public static void TryNotifyAll_Connected()
}
}

public static void TryNotifyAll_Disconnected()
public static void TryNotifyAll_Disconnected(SessionDisconnectInfo info)
{
foreach (var listener in All)
{
try
{
listener.Disconnected();
listener.Disconnected(info);
}
catch (Exception e)
{
Expand Down
4 changes: 2 additions & 2 deletions Source/Client/Networking/NetworkingInMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected override void SendRaw(byte[] raw, bool reliable)
});
}

public override void Close(MpDisconnectReason reason, byte[] data)
protected override void OnClose()
{
}

Expand Down Expand Up @@ -66,7 +66,7 @@ protected override void SendRaw(byte[] raw, bool reliable)
});
}

public override void Close(MpDisconnectReason reason, byte[] data)
protected override void OnClose()
{
}

Expand Down
18 changes: 10 additions & 8 deletions Source/Client/Networking/NetworkingLiteNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMetho

public void OnPeerDisconnected(NetPeer peer, DisconnectInfo info)
{
// Fallback: should generally be handled by ClientBaseState.HandleDisconnected.
MpDisconnectReason reason;
byte[] data;
ByteReader reader;

if (info.AdditionalData.IsNull)
if (info.AdditionalData.IsNull || info.AdditionalData.AvailableBytes == 0)
{
if (info.Reason is DisconnectReason.DisconnectPeerCalled or DisconnectReason.RemoteConnectionClose)
reason = MpDisconnectReason.Generic;
Expand All @@ -46,17 +47,18 @@ public void OnPeerDisconnected(NetPeer peer, DisconnectInfo info)
else
reason = MpDisconnectReason.NetFailed;

data = new [] { (byte)info.Reason };
var writer = new ByteWriter();
writer.WriteEnum(info.Reason);
reader = new ByteReader(writer.ToArray());
}
else
{
var reader = new ByteReader(info.AdditionalData.GetRemainingBytes());
reason = reader.ReadEnum<MpDisconnectReason>();
data = reader.ReadPrefixedBytes();
var rawReader = new ByteReader(info.AdditionalData.GetRemainingBytes());
reason = rawReader.ReadEnum<MpDisconnectReason>();
reader = rawReader;
}

Multiplayer.session.ProcessDisconnectPacket(reason, data);
ConnectionStatusListeners.TryNotifyAll_Disconnected();
ConnectionStatusListeners.TryNotifyAll_Disconnected(SessionDisconnectInfo.From(reason, reader));

Multiplayer.StopMultiplayer();
MpLog.Log($"Net client disconnected {info.Reason}");
Expand Down
25 changes: 12 additions & 13 deletions Source/Client/Networking/NetworkingSteam.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ public void SendRawSteam(byte[] raw, bool reliable)
);
}

public override void Close(MpDisconnectReason reason, byte[] data)
protected override void OnClose()
{
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SteamBaseConn.OnClose() is now a no-op, but both SteamClientConn and SteamServerConn still have logic to handle Packets.Special_Steam_Disconnect. If the client calls Close(), the server may no longer be notified promptly (potentially leaving ghost players until a timeout). Consider either sending Special_Steam_Disconnect from OnClose() (client->server), or explicitly closing the P2P session via Steamworks APIs.

Suggested change
{
{
// Ensure the Steam P2P session is explicitly closed so the remote side
// is promptly notified of the disconnect (avoids ghost players).
SteamNetworking.CloseP2PSessionWithUser(remoteId);

Copilot uses AI. Check for mistakes.
if (State != ConnectionStateEnum.ClientSteam)
Send(Packets.Special_Steam_Disconnect, GetDisconnectBytes(reason, data));
}

public abstract void OnError(EP2PSessionError error);
Expand All @@ -61,11 +59,8 @@ protected override void HandleReceiveMsg(int msgId, int fragState, ByteReader re
{
if (msgId == (int)Packets.Special_Steam_Disconnect)
{
Multiplayer.session.ProcessDisconnectPacket(
reader.ReadEnum<MpDisconnectReason>(),
reader.ReadPrefixedBytes()
);
OnDisconnect();
var reason = reader.ReadEnum<MpDisconnectReason>();
OnDisconnect(SessionDisconnectInfo.From(reason, reader));
return;
}

Expand All @@ -74,15 +69,19 @@ 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();
var info = new SessionDisconnectInfo
{
titleTranslated = error == EP2PSessionError.k_EP2PSessionErrorTimeout
? "MpSteamTimedOut".Translate()
: "MpSteamGenericError".Translate()
};

OnDisconnect();
OnDisconnect(info);
}

private void OnDisconnect()
private void OnDisconnect(SessionDisconnectInfo info)
{
ConnectionStatusListeners.TryNotifyAll_Disconnected();
ConnectionStatusListeners.TryNotifyAll_Disconnected(info);
Multiplayer.StopMultiplayer();
}
}
Expand Down
9 changes: 7 additions & 2 deletions Source/Client/Networking/State/ClientBaseState.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using Multiplayer.Client.Networking;
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;

namespace Multiplayer.Client;

public abstract class ClientBaseState : MpConnectionState
public abstract class ClientBaseState(ConnectionBase connection) : MpConnectionState(connection)
{
protected MultiplayerSession Session => Multiplayer.session;

public ClientBaseState(ConnectionBase connection) : base(connection)
[TypedPacketHandler]
public void HandleDisconnected(ServerDisconnectPacket packet)
{
ConnectionStatusListeners.TryNotifyAll_Disconnected(SessionDisconnectInfo.From(packet.reason, new ByteReader(packet.data)));
Multiplayer.StopMultiplayer();
}
Comment on lines +11 to 16
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientBaseState.HandleDisconnected relies on inheriting packet handlers from ClientBaseState, but MpConnectionState.SetImplementation only scans base-class methods when the concrete state type has [PacketHandlerClass(inheritHandlers: true)]. As-is, this disconnect handler will only be registered for states that opt into inheritance (currently only ClientLoadingState), so Server_Disconnect can be unhandled in joining/playing states.

Copilot uses AI. Check for mistakes.
}
19 changes: 15 additions & 4 deletions Source/Client/Networking/State/ClientLoadingState.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Ionic.Zlib;
using Multiplayer.Client.Saving;
Expand All @@ -14,6 +15,7 @@ public enum LoadingState
Downloading
}

[PacketHandlerClass(inheritHandlers: true)]
public class ClientLoadingState(ConnectionBase connection) : ClientBaseState(connection)
{
public LoadingState subState = LoadingState.Waiting;
Expand All @@ -29,19 +31,23 @@ public int DownloadSpeedKBps
var firstCheckpoint = downloadCheckpoints.First();
var lastCheckpoint = downloadCheckpoints.Last();
var timeTakenMs = Utils.MillisNow - firstCheckpoint.Item1;
var timeTakenSecs = Math.Max(1, timeTakenMs / 1000);
var timeTakenSecs = timeTakenMs / 1000f;

var downloadedBytes = lastCheckpoint.Item2 - firstCheckpoint.Item2;
return (int)(downloadedBytes / 1000 / timeTakenSecs);
var downloadedKBytes = downloadedBytes / 1000;
return (int)(downloadedKBytes / timeTakenSecs);
}
}

private List<(long, uint)> downloadCheckpoints = new(capacity: 64);
private Stopwatch downloadTimeStopwatch = new();

[PacketHandler(Packets.Server_WorldDataStart)]
public void HandleWorldDataStart(ByteReader data)
{
subState = LoadingState.Downloading;
connection.Lenient = false; // Lenient is set while rejoining
downloadTimeStopwatch.Start();
}

[FragmentedPacketHandler(Packets.Server_WorldData)]
Expand All @@ -57,7 +63,9 @@ public void HandleWorldDataFragment(FragmentedPacket packet)
[PacketHandler(Packets.Server_WorldData, allowFragmented: true)]
public void HandleWorldData(ByteReader data)
{
Log.Message("Game data size: " + data.Length);
var downloadMs = downloadTimeStopwatch.ElapsedMilliseconds;
downloadTimeStopwatch.Reset();
Log.Message($"Game data size: {data.Length}. Took {downloadMs}ms to receive.");

int factionId = data.ReadInt32();
Multiplayer.session.myFactionId = factionId;
Expand Down Expand Up @@ -125,7 +133,10 @@ public void HandleWorldData(ByteReader data)
onCancel: GenScene.GoToMainMenu // Calls StopMultiplayer through a patch
);

Stopwatch watch = Stopwatch.StartNew();
Loader.ReloadGame(mapsToLoad, true, false);
var loadingMs = watch.ElapsedMilliseconds;
Log.Message($"Loaded game in {loadingMs}ms");
connection.ChangeState(ConnectionStateEnum.ClientPlaying);
}
}
2 changes: 1 addition & 1 deletion Source/Client/Saving/ReplayConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public override void HandleReceiveRaw(ByteReader data, bool reliable)
{
}

public override void Close(MpDisconnectReason reason, byte[] data)
protected override void OnClose()
{
}
}
73 changes: 2 additions & 71 deletions Source/Client/Session/MultiplayerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ public class MultiplayerSession : IConnectionStatusListener

public bool desynced;

public SessionDisconnectInfo disconnectInfo;

public List<CSteamID> pendingSteam = new();
public List<CSteamID> knownUsers = new();

Expand Down Expand Up @@ -114,73 +112,6 @@ public void NotifyChat()
SoundDefOf.PageChange.PlayOneShotOnCamera();
}

public void ProcessDisconnectPacket(MpDisconnectReason reason, byte[] data)
{
var reader = new ByteReader(data);
string titleKey = null;
string descKey = null;

if (reason == MpDisconnectReason.GenericKeyed) titleKey = reader.ReadString();

if (reason == MpDisconnectReason.Protocol)
{
titleKey = "MpWrongProtocol";

string strVersion = reader.ReadString();
int proto = reader.ReadInt32();

disconnectInfo.wideWindow = true;
disconnectInfo.descTranslated = "MpWrongMultiplayerVersionDesc".Translate(strVersion, proto, MpVersion.Version, MpVersion.Protocol);

if (proto < MpVersion.Protocol)
disconnectInfo.descTranslated += "\n" + "MpWrongVersionUpdateInfoHost".Translate();
else
disconnectInfo.descTranslated += "\n" + "MpWrongVersionUpdateInfo".Translate();
}

if (reason == MpDisconnectReason.ConnectingFailed)
{
var netReason = reader.ReadEnum<DisconnectReason>();

disconnectInfo.titleTranslated =
netReason == DisconnectReason.ConnectionFailed ?
"MpConnectionFailed".Translate() :
"MpConnectionFailedWithInfo".Translate(netReason.ToString().CamelSpace().ToLowerInvariant());
}

if (reason == MpDisconnectReason.NetFailed)
{
var netReason = reader.ReadEnum<DisconnectReason>();

disconnectInfo.titleTranslated =
"MpDisconnectedWithInfo".Translate(netReason.ToString().CamelSpace().ToLowerInvariant());
}

if (reason == MpDisconnectReason.UsernameAlreadyOnline)
{
titleKey = "MpInvalidUsernameAlreadyPlaying";
descKey = "MpChangeUsernameInfo";

var newName = Multiplayer.username.Substring(0, Math.Min(Multiplayer.username.Length, MultiplayerServer.MaxUsernameLength - 3));
newName += new Random().Next(1000);

disconnectInfo.specialButtonTranslated = "MpConnectAsUsername".Translate(newName);
disconnectInfo.specialButtonAction = () => Reconnect(newName);
}

if (reason == MpDisconnectReason.UsernameLength) { titleKey = "MpInvalidUsernameLength"; descKey = "MpChangeUsernameInfo"; }
if (reason == MpDisconnectReason.UsernameChars) { titleKey = "MpInvalidUsernameChars"; descKey = "MpChangeUsernameInfo"; }
if (reason == MpDisconnectReason.ServerClosed) titleKey = "MpServerClosed";
if (reason == MpDisconnectReason.ServerFull) titleKey = "MpServerFull";
if (reason == MpDisconnectReason.ServerStarting) titleKey = "MpDisconnectServerStarting";
if (reason == MpDisconnectReason.Kick) titleKey = "MpKicked";
if (reason == MpDisconnectReason.ServerPacketRead) descKey = "MpPacketErrorRemote";
if (reason == MpDisconnectReason.BadGamePassword) descKey = "MpBadGamePassword";

disconnectInfo.titleTranslated ??= titleKey?.Translate();
disconnectInfo.descTranslated ??= descKey?.Translate();
}

public void Reconnect(string username)
{
Multiplayer.username = username;
Expand All @@ -195,11 +126,11 @@ public void Connected()
{
}

public void Disconnected()
public void Disconnected(SessionDisconnectInfo info)
{
MpUI.ClearWindowStack();

Find.WindowStack.Add(new DisconnectedWindow(disconnectInfo)
Find.WindowStack.Add(new DisconnectedWindow(info)
{
returnToServerBrowser = Multiplayer.Client?.State != ConnectionStateEnum.ClientPlaying
});
Expand Down
Loading
Loading