Skip to content
Merged
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
13 changes: 11 additions & 2 deletions src/SiegeFX.Core/Net/MpSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public void SendPvpHit(int target, float damage)
public MpSession(ISessionTransport transport, bool isHost, string localName)
{
_t = transport; IsHost = isHost; _localName = localName ?? "Player";
_t.PeerDisconnected += OnPeerGone;
_t.PeerDisconnected += QueuePeerGone;
if (IsHost) _roster[0] = new MpRosterEntry(0, _localName, true, 0);
}

Expand Down Expand Up @@ -336,6 +336,13 @@ void RosterChanged()
Broadcast(BuildRosterFrame());
}

// Transports raise PeerDisconnected from their own threads (the LAN
// keepalive task, EOS callbacks). OnPeerGone fires OnPlayerLeft, whose
// handlers edit the render host's actor list, so it must run on the
// thread that ticks the session — queue it and drain it in Tick.
readonly System.Collections.Concurrent.ConcurrentQueue<int> _gonePeers = new();
void QueuePeerGone(int peer) => _gonePeers.Enqueue(peer);

void OnPeerGone(int peer)
{
if (IsHost)
Expand Down Expand Up @@ -546,6 +553,8 @@ public void SendRecruitGrant(uint scid, int owningPlayer)
/// Call once per engine frame with the frame dt.</summary>
public void Tick(double dt)
{
while (_gonePeers.TryDequeue(out int gone))
OnPeerGone(gone);
SecondsSinceHost += dt;
// The game clock runs while a game is under way and not paused.
if (!Paused && (IsHost ? InProgress is not null : _joined)) GameSeconds += dt;
Expand Down Expand Up @@ -1226,7 +1235,7 @@ void RelayChat(int fromPlayer, string text)

public void Dispose()
{
_t.PeerDisconnected -= OnPeerGone;
_t.PeerDisconnected -= QueuePeerGone;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/SiegeFX.Runtime/NetSelfTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ void Assert(bool cond, string what)
"in-region: GameStart delivered region + difficulty to the client");

c2.Dispose(); // client drops — host must be told player 1 left
hs.Tick(0); // (on its next tick: transports report leaves off-thread)
Assert(hostSawLeave == 1, $"in-region: host fired OnPlayerLeft on disconnect (got {hostSawLeave})");
hs.Dispose(); cs.Dispose(); h2.Dispose();
}
Expand Down Expand Up @@ -230,6 +231,7 @@ void Assert(bool cond, string what)
hs.OnPlayerLeft = id => leftName = hostRoster?.FirstOrDefault(e => e.Player == id).Name;
var before = hostRoster;
c3.Dispose();
hs.Tick(0); // leaves are handled on the next tick (transports report them off-thread)
Assert(hostRoster is { Count: 1 } && before!.Count == 2 && hs.Roster.Count == 1 && leftName == "Joiner",
$"staging: a player who leaves drops out of the host's roster (left: '{leftName}')");
hs.Dispose(); cs.Dispose(); h3.Dispose();
Expand Down
31 changes: 31 additions & 0 deletions tests/SiegeFX.Tests/MpDisconnectTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using SiegeFX.Core.Net;
using Xunit;

namespace SiegeFX.Tests;

public class MpDisconnectTests
{
[Fact]
public void ADisconnectFromATransportThreadIsHandledOnTick()
{
// The LAN keepalive drops a silent peer from a pool thread; the
// render host's OnPlayerLeft edits its actor list, so the session
// must hold the leave until the main thread ticks it.
var (h, c) = LoopbackTransport.CreatePair();
var host = new MpSession(h, isHost: true, "Host") { BuildSnapshot = () => [] };
var client = new MpSession(c, isHost: false, "Client");
client.SendJoinRequest();
for (int i = 0; i < 4; i++) { host.Tick(0); client.Tick(0); }

int left = -1, leftOnThread = -1, mainThread = Environment.CurrentManagedThreadId;
host.OnPlayerLeft = p => { left = p; leftOnThread = Environment.CurrentManagedThreadId; };

var t = new Thread(c.Dispose);
t.Start(); t.Join();
Assert.Equal(-1, left);

host.Tick(0);
Assert.Equal(1, left);
Assert.Equal(mainThread, leftOnThread);
}
}
Loading