Skip to content

Commit 23be561

Browse files
authored
feat: disconnect reason (#2280)
* disconnect reason
1 parent f6db03f commit 23be561

File tree

9 files changed

+204
-1
lines changed

9 files changed

+204
-1
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
1111

1212
### Added
1313
- Added `NetworkObject` auto-add helper and Multiplayer Tools install reminder settings to Project Settings. (#2285)
14+
- Added `public string DisconnectReason` getter to `NetworkManager` and `string Reason` to `ConnectionApprovalResponse`. Allows connection approval to communicate back a reason. Also added `public void DisconnectClient(ulong clientId, string reason)` allowing setting a disconnection reason, when explicitly disconnecting a client.
1415

1516
### Fixed
1617

com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ public NetworkPrefabHandler PrefabHandler
8686
private bool m_ShuttingDown;
8787
private bool m_StopProcessingMessages;
8888

89+
// <summary>
90+
// When disconnected from the server, the server may send a reason. If a reason was sent, this property will
91+
// tell client code what the reason was. It should be queried after the OnClientDisconnectCallback is called
92+
// </summary>
93+
public string DisconnectReason { get; internal set; }
94+
8995
private class NetworkManagerHooks : INetworkHooks
9096
{
9197
private NetworkManager m_NetworkManager;
@@ -443,6 +449,11 @@ public class ConnectionApprovalResponse
443449
/// If the Approval decision cannot be made immediately, the client code can set Pending to true, keep a reference to the ConnectionApprovalResponse object and write to it later. Client code must exercise care to setting all the members to the value it wants before marking Pending to false, to indicate completion. If the field is set as Pending = true, we'll monitor the object until it gets set to not pending anymore and use the parameters then.
444450
/// </summary>
445451
public bool Pending;
452+
453+
// <summary>
454+
// Optional reason. If Approved is false, this reason will be sent to the client so they know why they
455+
// were not approved.
456+
public string Reason;
446457
}
447458

448459
/// <summary>
@@ -889,6 +900,7 @@ private void Initialize(bool server)
889900
return;
890901
}
891902

903+
DisconnectReason = string.Empty;
892904
IsApproved = false;
893905

894906
ComponentFactory.SetDefaults();
@@ -2004,12 +2016,31 @@ internal void HandleIncomingData(ulong clientId, ArraySegment<byte> payload, flo
20042016
/// </summary>
20052017
/// <param name="clientId">The ClientId to disconnect</param>
20062018
public void DisconnectClient(ulong clientId)
2019+
{
2020+
DisconnectClient(clientId, null);
2021+
}
2022+
2023+
/// <summary>
2024+
/// Disconnects the remote client.
2025+
/// </summary>
2026+
/// <param name="clientId">The ClientId to disconnect</param>
2027+
/// <param name="reason">Disconnection reason. If set, client will receive a DisconnectReasonMessage and have the
2028+
/// reason available in the NetworkManager.DisconnectReason property</param>
2029+
public void DisconnectClient(ulong clientId, string reason)
20072030
{
20082031
if (!IsServer)
20092032
{
20102033
throw new NotServerException($"Only server can disconnect remote clients. Please use `{nameof(Shutdown)}()` instead.");
20112034
}
20122035

2036+
if (!string.IsNullOrEmpty(reason))
2037+
{
2038+
var disconnectReason = new DisconnectReasonMessage();
2039+
disconnectReason.Reason = reason;
2040+
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, clientId);
2041+
}
2042+
MessagingSystem.ProcessSendQueues();
2043+
20132044
OnClientDisconnectFromServer(clientId);
20142045
DisconnectRemoteClient(clientId);
20152046
}
@@ -2243,6 +2274,15 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe
22432274
}
22442275
else
22452276
{
2277+
if (!string.IsNullOrEmpty(response.Reason))
2278+
{
2279+
var disconnectReason = new DisconnectReasonMessage();
2280+
disconnectReason.Reason = response.Reason;
2281+
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, ownerClientId);
2282+
2283+
MessagingSystem.ProcessSendQueues();
2284+
}
2285+
22462286
PendingClients.Remove(ownerClientId);
22472287
DisconnectRemoteClient(ownerClientId);
22482288
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace Unity.Netcode
2+
{
3+
internal struct DisconnectReasonMessage : INetworkMessage
4+
{
5+
public string Reason;
6+
7+
public void Serialize(FastBufferWriter writer)
8+
{
9+
string reasonSent = Reason;
10+
if (reasonSent == null)
11+
{
12+
reasonSent = string.Empty;
13+
}
14+
15+
if (writer.TryBeginWrite(FastBufferWriter.GetWriteSize(reasonSent)))
16+
{
17+
writer.WriteValueSafe(reasonSent);
18+
}
19+
else
20+
{
21+
writer.WriteValueSafe(string.Empty);
22+
NetworkLog.LogWarning(
23+
"Disconnect reason didn't fit. Disconnected without sending a reason. Consider shortening the reason string.");
24+
}
25+
}
26+
27+
public bool Deserialize(FastBufferReader reader, ref NetworkContext context)
28+
{
29+
reader.ReadValueSafe(out Reason);
30+
return true;
31+
}
32+
33+
public void Handle(ref NetworkContext context)
34+
{
35+
((NetworkManager)context.SystemOwner).DisconnectReason = Reason;
36+
}
37+
};
38+
}

com.unity.netcode.gameobjects/Runtime/Messaging/DisconnectReasonMessage.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using NUnit.Framework;
2+
using Unity.Collections;
3+
4+
namespace Unity.Netcode.EditorTests
5+
{
6+
public class DisconnectMessageTests
7+
{
8+
[Test]
9+
public void EmptyDisconnectReason()
10+
{
11+
var networkContext = new NetworkContext();
12+
var writer = new FastBufferWriter(20, Allocator.Temp, 20);
13+
var msg = new DisconnectReasonMessage();
14+
msg.Reason = string.Empty;
15+
msg.Serialize(writer);
16+
17+
var fbr = new FastBufferReader(writer, Allocator.Temp);
18+
var recvMsg = new DisconnectReasonMessage();
19+
recvMsg.Deserialize(fbr, ref networkContext);
20+
21+
Assert.IsEmpty(recvMsg.Reason);
22+
}
23+
24+
[Test]
25+
public void DisconnectReason()
26+
{
27+
var networkContext = new NetworkContext();
28+
var writer = new FastBufferWriter(20, Allocator.Temp, 20);
29+
var msg = new DisconnectReasonMessage();
30+
msg.Reason = "Foo";
31+
msg.Serialize(writer);
32+
33+
var fbr = new FastBufferReader(writer, Allocator.Temp);
34+
var recvMsg = new DisconnectReasonMessage();
35+
recvMsg.Deserialize(fbr, ref networkContext);
36+
37+
Assert.AreEqual("Foo", recvMsg.Reason);
38+
}
39+
40+
[Test]
41+
public void DisconnectReasonTooLong()
42+
{
43+
var networkContext = new NetworkContext();
44+
var writer = new FastBufferWriter(20, Allocator.Temp, 20);
45+
var msg = new DisconnectReasonMessage();
46+
msg.Reason = "ThisStringIsWayLongerThanTwentyBytes";
47+
msg.Serialize(writer);
48+
49+
var fbr = new FastBufferReader(writer, Allocator.Temp);
50+
var recvMsg = new DisconnectReasonMessage();
51+
recvMsg.Deserialize(fbr, ref networkContext);
52+
53+
Assert.IsEmpty(recvMsg.Reason);
54+
}
55+
}
56+
}

com.unity.netcode.gameobjects/Tests/Editor/DisconnectMessageTests.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

com.unity.netcode.gameobjects/Tests/Runtime/DisconnectReasonTests.cs renamed to com.unity.netcode.gameobjects/Tests/Runtime/Messaging/DisconnectReasonTests.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
33
using System.Text.RegularExpressions;
4+
using NUnit.Framework;
45
using UnityEngine;
56
using UnityEngine.TestTools;
67
using Unity.Netcode.TestHelpers.Runtime;
@@ -25,16 +26,48 @@ protected override void OnServerAndClientsCreated()
2526
}
2627

2728
private int m_DisconnectCount;
29+
private bool m_ThrowOnDisconnect = false;
2830

2931
public void OnClientDisconnectCallback(ulong clientId)
3032
{
3133
m_DisconnectCount++;
32-
throw new SystemException("whatever");
34+
if (m_ThrowOnDisconnect)
35+
{
36+
throw new SystemException("whatever");
37+
}
38+
}
39+
40+
[UnityTest]
41+
public IEnumerator DisconnectReasonTest()
42+
{
43+
float startTime = Time.realtimeSinceStartup;
44+
m_ThrowOnDisconnect = false;
45+
m_DisconnectCount = 0;
46+
47+
// Add a callback for both clients, when they get disconnected
48+
m_ClientNetworkManagers[0].OnClientDisconnectCallback += OnClientDisconnectCallback;
49+
m_ClientNetworkManagers[1].OnClientDisconnectCallback += OnClientDisconnectCallback;
50+
51+
// Disconnect both clients, from the server
52+
m_ServerNetworkManager.DisconnectClient(m_ClientNetworkManagers[0].LocalClientId, "Bogus reason 1");
53+
m_ServerNetworkManager.DisconnectClient(m_ClientNetworkManagers[1].LocalClientId, "Bogus reason 2");
54+
55+
while (m_DisconnectCount < 2 && Time.realtimeSinceStartup < startTime + 10.0f)
56+
{
57+
yield return null;
58+
}
59+
60+
Assert.AreEqual(m_ClientNetworkManagers[0].DisconnectReason, "Bogus reason 1");
61+
Assert.AreEqual(m_ClientNetworkManagers[1].DisconnectReason, "Bogus reason 2");
62+
63+
Debug.Assert(m_DisconnectCount == 2);
3364
}
3465

3566
[UnityTest]
3667
public IEnumerator DisconnectExceptionTest()
3768
{
69+
m_ThrowOnDisconnect = true;
70+
m_DisconnectCount = 0;
3871
float startTime = Time.realtimeSinceStartup;
3972

4073
// Add a callback for first client, when they get disconnected

testproject/Assets/Tests/Runtime/MultiClientConnectionApproval.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ private IEnumerator ConnectionApprovalHandler(int numClients, int failureTestCou
171171
}
172172
}
173173

174+
foreach (var c in clientsToClean)
175+
{
176+
Assert.AreEqual(c.DisconnectReason, "Some valid reason");
177+
}
178+
174179
foreach (var client in clients)
175180
{
176181
// If a client failed, then it will already be shutdown
@@ -228,6 +233,14 @@ private void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest
228233
response.Rotation = null;
229234
response.PlayerPrefabHash = m_PrefabOverrideGlobalObjectIdHash;
230235
}
236+
if (!response.Approved)
237+
{
238+
response.Reason = "Some valid reason";
239+
}
240+
else
241+
{
242+
response.Reason = string.Empty;
243+
}
231244
}
232245

233246

0 commit comments

Comments
 (0)