Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f0790d2
disconnect reason, wip
jeffreyrainy Oct 26, 2022
8ef9969
adding disconnect reason as a property to NetworkManager
jeffreyrainy Oct 27, 2022
7b7b5cf
feat: providing a disconnect reason
jeffreyrainy Oct 27, 2022
3c67ce9
removing unwanted changes in PR
jeffreyrainy Oct 27, 2022
4c4335f
Merge branch 'develop' into feat/disconnect-reason
jeffreyrainy Oct 27, 2022
37055ec
style: standards
jeffreyrainy Oct 27, 2022
25b812f
Adding reason in DisconnectClient, checking for null reason
jeffreyrainy Oct 28, 2022
55f0ec5
add summary xmldoc for ConnectionApprovalResponse.Reason
jeffreyrainy Oct 28, 2022
98eaeb0
Keeping API compatibility
jeffreyrainy Oct 28, 2022
99358bf
fix: default parameter hiding
jeffreyrainy Oct 28, 2022
0168a14
Merge branch 'develop' into feat/disconnect-reason
jeffreyrainy Oct 29, 2022
1bea4ae
WIP, adding test for disconnection reason
jeffreyrainy Oct 31, 2022
8dc1c2b
Merge branch 'feat/disconnect-reason' of github.com:Unity-Technologie…
jeffreyrainy Oct 31, 2022
d683e98
fixed typo
jeffreyrainy Oct 31, 2022
80b278d
Refactor, moving the disconnect reason SendMessage higher in the call…
jeffreyrainy Oct 31, 2022
a9767dd
test improvements
jeffreyrainy Oct 31, 2022
35f9279
final adjustments
jeffreyrainy Oct 31, 2022
7ea9d6b
final adjustments
jeffreyrainy Oct 31, 2022
890c8f6
Merge branch 'feat/disconnect-reason' of github.com:Unity-Technologie…
jeffreyrainy Oct 31, 2022
3471527
adding line removed mistakenly
jeffreyrainy Oct 31, 2022
3ea4a4f
style: coding standards
jeffreyrainy Oct 31, 2022
9efaee1
some PR review comments
jeffreyrainy Nov 1, 2022
a6d8796
better handling of oversized reasons
jeffreyrainy Nov 1, 2022
6047dda
Addressing some more PR review comments
jeffreyrainy Nov 1, 2022
1203c8a
Merge branch 'develop' into feat/disconnect-reason
jeffreyrainy Nov 1, 2022
496dfce
Read/WriteValueSafe instead of duplicating the size
jeffreyrainy Nov 2, 2022
e4494a8
Merge branch 'develop' into feat/disconnect-reason
jeffreyrainy Nov 2, 2022
f113e06
more PR review comments
jeffreyrainy Nov 2, 2022
6739f30
Merge branch 'develop' into feat/disconnect-reason
jeffreyrainy Nov 2, 2022
8127eeb
Changelog
jeffreyrainy Nov 2, 2022
7b06f84
moving test into /messaging
jeffreyrainy Nov 2, 2022
35d9315
style: netcode.standards
jeffreyrainy Nov 2, 2022
f622388
Merge develop into feat/disconnect-reason
netcode-ci-service Nov 3, 2022
3030a05
Testing approval disconnection reason
jeffreyrainy Nov 3, 2022
dc10881
Merge branch 'feat/disconnect-reason' of github.com:Unity-Technologie…
jeffreyrainy Nov 3, 2022
b14cc83
Merge develop into feat/disconnect-reason
netcode-ci-service Nov 3, 2022
0a52c7b
Merge develop into feat/disconnect-reason
netcode-ci-service Nov 4, 2022
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
78 changes: 78 additions & 0 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ public NetworkPrefabHandler PrefabHandler
private bool m_ShuttingDown;
private bool m_StopProcessingMessages;

// <summary>
Comment thread
jeffreyrainy marked this conversation as resolved.
// When disconnected from the server, the server may send a reason. If a reason was sent, this property will
// tell client code what the reason was. It should be queried after the OnClientDisconnectCallback is called
// </summary>
public string DisconnectReason { get; internal set; }
Comment thread
jeffreyrainy marked this conversation as resolved.
Comment thread
jeffreyrainy marked this conversation as resolved.

private class NetworkManagerHooks : INetworkHooks
{
private NetworkManager m_NetworkManager;
Expand Down Expand Up @@ -443,6 +449,11 @@ public class ConnectionApprovalResponse
/// 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.
/// </summary>
public bool Pending;

// <summary>
// Optional reason. If Approved is false, this reason will be sent to the client so they know why they
// were not approved.
public string Reason;
}

/// <summary>
Expand Down Expand Up @@ -889,6 +900,7 @@ private void Initialize(bool server)
return;
}

DisconnectReason = "";
Comment thread
jeffreyrainy marked this conversation as resolved.
Outdated
IsApproved = false;

ComponentFactory.SetDefaults();
Expand Down Expand Up @@ -1997,12 +2009,31 @@ internal void HandleIncomingData(ulong clientId, ArraySegment<byte> payload, flo
/// </summary>
/// <param name="clientId">The ClientId to disconnect</param>
public void DisconnectClient(ulong clientId)
{
DisconnectClient(clientId, null);
}

/// <summary>
/// Disconnects the remote client.
/// </summary>
/// <param name="clientId">The ClientId to disconnect</param>
/// <param name="reason">Disconnection reason. If set, client will receive a DisconnectReasonMessage and have the
/// reason available in the NetworkManager.DisconnectReason property</param>
public void DisconnectClient(ulong clientId, string reason)
Copy link
Copy Markdown
Contributor

@SamuelBellomo SamuelBellomo Nov 2, 2022

Choose a reason for hiding this comment

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

We have a flow in boss room where when the host shuts down gracefully, we'll send a message to all clients telling them "hey! this is a nice shutdown, please give a popup saying the host just left". We need this flow, since else clients will think there was some wifi issues and will do their reconnect coroutine.
I wonder how we'd do this now that we have this new flow. Since shutdown also disconnects clients, could it be possible to set a reason there as well?
Or would users be expected to disconnect all clients with reason before shutting down?
Cause without that message, users would have no way of knowing if this is an elegant shutdown (and that clients just go back to main menu) vs a weird disconnect where the client would make some effort to try to reconnect, not knowing what happened to the host.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess the solution could still be a custom message (or RPC thinking of it) and still have the "delay one frame to send the RPC so we flush before closing the connection). We would still have this weird "wait one frame" though :(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We could add an API to shutdown with a disconnect reason. Internally, it would disconnect all clients with this reason, and then shutdown. This would be exactly equivalent to you disconnecting all clients before shutting down. Please let me know if you are requesting this API.

That said, from there, two things can happen. Either the UTP flushing of its queue survives the shutdown. Or maybe it doesn't. Maybe shutdown overrides the queuing and you lose recent sends. Maybe it guarantees flushing before shutting down. Someone would need to check. In any case, if it doesn't do what is needed you'd have to file a bug with Transport.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pretty sure I don't have the power to request anything of you :P
Since we are adding this new way of doing things, with disconnect reason, my thinking was it'd be great if this was used in most places a disconnect could happen.
We couldn't do it for ALL cases for sure, timeouts and the such wouldn't get the reason message anyway. But it'd make sure disconnect reason is a first class citizen instead of just a patch/something added later?
Besides shutdown, are there other places users can be disconnected?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So just tested this real quick in boss room when pointing to your branch and it seems to work (and yep, UTP already allowed to flush, that was fixed a few months ago):

        if (m_DisconnectReason != ConnectStatus.Undefined)
        {
            string reasonString = JsonUtility.ToJson(m_DisconnectReason);
            var connectedClients = NetworkManager.Singleton.ConnectedClients.ToList();
            for (int i = connectedClients.Count - 1; i >= 0; i--)
            {
                var connectedClient = connectedClients[i];
                if (connectedClient.Key != NetworkManager.Singleton.LocalClientId)
                {
                    NetworkManager.Singleton.DisconnectClient(connectedClient.Key, reason: reasonString);
                }
            }
        }

        m_ConnectionManager.NetworkManager.Shutdown()

And then reading that value in OnClientDisconnectCallback. Testing it in game, when I shutdown the host I'll get the usual "host has left" popup on clients. It's a bit cumbersome to write that for loop but not the end of the world. Not the mountain I'll die on :P

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok, if UTP flushes even on shutdown, you may convince me to add a Shutdown API that also takes a reason. Let me think about it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it's particular to your DisconnectClient which does its own sort of flushing. I'd need to check, but if I were to send an RPC right before shutdown, I think it wouldn't work and I'd need to wait a frame before doing the Shutdown. If I remember the conversation from a few months ago correctly, UTP did its flush, but NGO was clearing things before the UTP flush. Your disconnect flushes things before the shutdown clearing (I think, to confirm)

{
if (!IsServer)
{
throw new NotServerException($"Only server can disconnect remote clients. Please use `{nameof(Shutdown)}()` instead.");
}

if (!string.IsNullOrEmpty(reason))
{
var disconnectReason = new DisconnectReasonMessage();
disconnectReason.Reason = reason;
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, clientId);
Comment thread
SamuelBellomo marked this conversation as resolved.
}
MessagingSystem.ProcessSendQueues();

Comment thread
jeffreyrainy marked this conversation as resolved.
OnClientDisconnectFromServer(clientId);
DisconnectRemoteClient(clientId);
}
Expand Down Expand Up @@ -2123,6 +2154,44 @@ private void SyncTime()
#endif
}

internal struct DisconnectReasonMessage : INetworkMessage
Comment thread
jeffreyrainy marked this conversation as resolved.
Outdated
{
public string Reason;

public void Serialize(FastBufferWriter writer)
{
if (writer.TryBeginWrite(sizeof(int) + FastBufferWriter.GetWriteSize(Reason)))
Comment thread
jeffreyrainy marked this conversation as resolved.
Outdated
{
writer.WriteValue(Reason.Length);
writer.WriteValue(Reason);
}
else
{
writer.TryBeginWrite(sizeof(int) + FastBufferWriter.GetWriteSize(""));
Comment thread
jeffreyrainy marked this conversation as resolved.
Outdated
writer.WriteValue(0);
writer.WriteValue("");
Comment thread
jeffreyrainy marked this conversation as resolved.
Outdated
Debug.LogWarning("Disconnect reason didn't fit. Disconnected without sending a reason. Consider shortening the reason string.");
}
}

public bool Deserialize(FastBufferReader reader, ref NetworkContext context)
{
int size;
reader.TryBeginRead(sizeof(int));
reader.ReadValue(out size);
Reason = new string(' ', size);
Comment thread
jeffreyrainy marked this conversation as resolved.
Outdated
reader.TryBeginRead(FastBufferWriter.GetWriteSize(Reason));
reader.ReadValue(out Reason);

return true;
}

public void Handle(ref NetworkContext context)
{
((NetworkManager)context.SystemOwner).DisconnectReason = Reason;
}
};

/// <summary>
/// Server Side: Handles the approval of a client
/// </summary>
Expand Down Expand Up @@ -2236,6 +2305,15 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe
}
else
{
if (!string.IsNullOrEmpty(response.Reason))
{
var disconnectReason = new DisconnectReasonMessage();
disconnectReason.Reason = response.Reason;
SendMessage(ref disconnectReason, NetworkDelivery.Reliable, ownerClientId);

MessagingSystem.ProcessSendQueues();
}

PendingClients.Remove(ownerClientId);
DisconnectRemoteClient(ownerClientId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Collections;
Comment thread
jeffreyrainy marked this conversation as resolved.
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Unity.Netcode.TestHelpers.Runtime;

namespace Unity.Netcode.RuntimeTests
{
public class DisconnectReasonObject : NetworkBehaviour
{

}

public class DisconnectReasonTests : NetcodeIntegrationTest
{
protected override int NumberOfClients => 2;

private GameObject m_PrefabToSpawn;

protected override void OnServerAndClientsCreated()
{
m_PrefabToSpawn = CreateNetworkObjectPrefab("DisconnectReasonObject");
m_PrefabToSpawn.AddComponent<DisconnectReasonObject>();
}

private int m_DisconnectCount;

public void OnClientDisconnectCallback(ulong clientId)
{
m_DisconnectCount++;
Debug.Log($"Disconnected {clientId}");
}

[UnityTest]
public IEnumerator DisconnectReasonTest()
{
float startTime = Time.realtimeSinceStartup;

// Add a callback for first client, when they get disconnected
m_ClientNetworkManagers[0].OnClientDisconnectCallback += OnClientDisconnectCallback;
m_ClientNetworkManagers[1].OnClientDisconnectCallback += OnClientDisconnectCallback;

// Disconnect first client, from the server
m_ServerNetworkManager.DisconnectClient(m_ClientNetworkManagers[0].LocalClientId, "Bogus reason 1");
m_ServerNetworkManager.DisconnectClient(m_ClientNetworkManagers[1].LocalClientId, "Bogus reason 2");

while (m_DisconnectCount < 2 && Time.realtimeSinceStartup < startTime + 10.0f)
{
yield return null;
}

Assert.AreEqual(m_ClientNetworkManagers[0].DisconnectReason, "Bogus reason 1");
Assert.AreEqual(m_ClientNetworkManagers[1].DisconnectReason, "Bogus reason 2");
}
}
Comment thread
jeffreyrainy marked this conversation as resolved.
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.