From 9f188375cd1324c0583892fc87f49269622987e6 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Mon, 8 Aug 2022 18:30:00 +0200 Subject: [PATCH 1/8] Added Identity Spawn Methods --- Assets/Mirror/Runtime/NetworkServer.cs | 113 +++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Runtime/NetworkServer.cs index 44e273a63c..4be0e315e0 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ b/Assets/Mirror/Runtime/NetworkServer.cs @@ -1094,6 +1094,64 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) RebuildObservers(identity, true); } + static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnection) + { + GameObject obj = identity.gameObject; + + // verify if we can spawn this + if (Utils.IsPrefab(obj)) + { + Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first."); + return; + } + + if (!active) + { + Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server."); + return; + } + + if (identity == null) + { + Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}"); + return; + } + + if (identity.SpawnedFromInstantiate) + { + // Using Instantiate on SceneObject is not allowed, so stop spawning here + // NetworkIdentity.Awake already logs error, no need to log a second error here + return; + } + + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; + + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.hasAuthority = true; + + identity.OnStartServer(); + + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); + + if (aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnSpawned(identity); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + RebuildObservers(identity, true); + } + /// Spawn the given game object on all clients which are ready. // This will cause a new object to be instantiated from the registered // prefab, or from a custom spawn function. @@ -1102,6 +1160,14 @@ public static void Spawn(GameObject obj, NetworkConnection ownerConnection = nul SpawnObject(obj, ownerConnection); } + /// Spawn the given game object on all clients which are ready. + // This will cause a new object to be instantiated from the registered + // prefab, or from a custom spawn function. + public static void Spawn(NetworkIdentity identity, NetworkConnection ownerConnection = null) + { + SpawnObject(identity, ownerConnection); + } + /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, GameObject ownerPlayer) @@ -1122,6 +1188,45 @@ public static void Spawn(GameObject obj, GameObject ownerPlayer) Spawn(obj, identity.connectionToClient); } + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity spawnIdentity, GameObject ownerPlayer) + { + NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + if (ownerIdentity == null) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (ownerIdentity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(spawnIdentity, ownerIdentity.connectionToClient); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity spawnIdentity, NetworkIdentity ownerIdentity) + { + if (ownerIdentity == null) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (ownerIdentity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(spawnIdentity, ownerIdentity.connectionToClient); + } + /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerConnection = null) @@ -1133,6 +1238,14 @@ public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerCo SpawnObject(obj, ownerConnection); } + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity identity, Guid assetId, NetworkConnection ownerConnection = null) + { + identity.assetId = assetId; + SpawnObject(identity, ownerConnection); + } + internal static bool ValidateSceneObject(NetworkIdentity identity) { if (identity.gameObject.hideFlags == HideFlags.NotEditable || From 6750d9416173c0846968db8d8d524d255b74d0a9 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Mon, 8 Aug 2022 19:03:40 +0200 Subject: [PATCH 2/8] Added New Tests & Fixed a Test Added possibly obsolete tests for Spawn using Network Identity Fixed Shutdown_DisabledAllSpawnedPrefabs() using wrong identity --- .../Tests/Editor/NetworkIdentityTests.cs | 45 +++++++++++++++++++ .../Tests/Runtime/NetworkServerRuntimeTest.cs | 14 +++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs index 1863fa36a5..64ac1b33e3 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs @@ -272,6 +272,24 @@ public void ServerMode_IsFlags_Test() Assert.That(component.OnStartServer_isServer, Is.EqualTo(true)); } + // check isClient/isServer/isLocalPlayer in server-only mode + [Test] + public void ServerMode_IsFlags_Identity_Test() + { + CreateNetworked(out GameObject _, out NetworkIdentity identity, out IsClientServerCheckComponent component); + + // start the server + NetworkServer.Listen(1000); + + // spawn it + NetworkServer.Spawn(identity); + + // OnStartServer should have been called. check the flags. + Assert.That(component.OnStartServer_isClient, Is.EqualTo(false)); + Assert.That(component.OnStartServer_isLocalPlayer, Is.EqualTo(false)); + Assert.That(component.OnStartServer_isServer, Is.EqualTo(true)); + } + // check isClient/isServer/isLocalPlayer in host mode [Test] public void HostMode_IsFlags_Test() @@ -299,6 +317,33 @@ public void HostMode_IsFlags_Test() NetworkServer.RemoveLocalConnection(); } + // check isClient/isServer/isLocalPlayer in host mode + [Test] + public void HostMode_IsFlags_Identity_Test() + { + CreateNetworked(out GameObject gameObject, out NetworkIdentity identity, out IsClientServerCheckComponent component); + + // start the server + NetworkServer.Listen(1000); + + // start the client + NetworkClient.ConnectHost(); + + // set is as local player + NetworkClient.InternalAddPlayer(identity); + + // spawn it + NetworkServer.Spawn(identity); + + // OnStartServer should have been called. check the flags. + Assert.That(component.OnStartServer_isClient, Is.EqualTo(true)); + Assert.That(component.OnStartServer_isLocalPlayer, Is.EqualTo(true)); + Assert.That(component.OnStartServer_isServer, Is.EqualTo(true)); + + // stop the client + NetworkServer.RemoveLocalConnection(); + } + [Test] public void GetSetAssetId() { diff --git a/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs b/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs index 03559af624..c5f3140004 100644 --- a/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs +++ b/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs @@ -62,6 +62,8 @@ public IEnumerator Shutdown_DestroysAllSpawnedPrefabs() NetworkIdentity identity1 = SpawnPrefab(prefab); NetworkIdentity identity2 = SpawnPrefab(prefab); + NetworkIdentity identity3 = SpawnNetworkPrefab(prefab); + NetworkIdentity identity4 = SpawnNetworkPrefab(prefab); // shutdown, wait 1 frame for unity to destroy objects NetworkServer.Shutdown(); @@ -71,6 +73,8 @@ public IEnumerator Shutdown_DestroysAllSpawnedPrefabs() // need to use untiy `==` check Assert.IsTrue(identity1 == null); Assert.IsTrue(identity2 == null); + Assert.IsTrue(identity3 == null); + Assert.IsTrue(identity4 == null); Assert.That(NetworkServer.spawned, Is.Empty); } @@ -84,6 +88,14 @@ NetworkIdentity SpawnPrefab(GameObject prefab) return identity1; } + NetworkIdentity SpawnNetworkPrefab(GameObject prefab) + { + NetworkIdentity clone1 = GameObject.Instantiate(prefab).GetComponent(); + NetworkServer.Spawn(clone1); + Assert.IsTrue(NetworkServer.spawned.ContainsValue(clone1)); + return clone1; + } + [Test] public void Shutdown_DisablesAllSpawnedPrefabs() { @@ -104,7 +116,7 @@ public void Shutdown_DisablesAllSpawnedPrefabs() Assert.IsTrue(identity1 != null); Assert.IsTrue(identity2 != null); Assert.IsFalse(identity1.gameObject.activeSelf); - Assert.IsFalse(identity1.gameObject.activeSelf); + Assert.IsFalse(identity2.gameObject.activeSelf); Assert.That(NetworkServer.spawned, Is.Empty); } From 0e7cb11f37fa475370b87ec2803350a296763531 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Thu, 11 Aug 2022 20:41:25 +0200 Subject: [PATCH 3/8] Adjusted Spawn methods to use NetworkIdentity --- Assets/Mirror/Runtime/NetworkServer.cs | 58 +++++++++++++++++++------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/Assets/Mirror/Runtime/NetworkServer.cs b/Assets/Mirror/Runtime/NetworkServer.cs index 4be0e315e0..935ffd5bdb 100644 --- a/Assets/Mirror/Runtime/NetworkServer.cs +++ b/Assets/Mirror/Runtime/NetworkServer.cs @@ -1096,6 +1096,13 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnection) { + // ensure spawn object has a network identity + if (identity == null) + { + Debug.LogError($"SpawnObject has no NetworkIdentity. Please add a NetworkIdentity to the SpawnObject"); + return; + } + GameObject obj = identity.gameObject; // verify if we can spawn this @@ -1111,12 +1118,6 @@ static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnect return; } - if (identity == null) - { - Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}"); - return; - } - if (identity.SpawnedFromInstantiate) { // Using Instantiate on SceneObject is not allowed, so stop spawning here @@ -1157,7 +1158,15 @@ static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnect // prefab, or from a custom spawn function. public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) { - SpawnObject(obj, ownerConnection); + NetworkIdentity identity = obj.GetComponent(); + + if (identity == null) + { + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); + return; + } + + SpawnObject(identity, ownerConnection); } /// Spawn the given game object on all clients which are ready. @@ -1172,20 +1181,28 @@ public static void Spawn(NetworkIdentity identity, NetworkConnection ownerConnec // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, GameObject ownerPlayer) { - NetworkIdentity identity = ownerPlayer.GetComponent(); + NetworkIdentity identity = obj.GetComponent(); + NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + if (identity == null) { - Debug.LogError("Player object has no NetworkIdentity"); + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); return; } - if (identity.connectionToClient == null) + if (ownerIdentity == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + if (ownerIdentity.connectionToClient == null) { Debug.LogError("Player object is not a player."); return; } - Spawn(obj, identity.connectionToClient); + Spawn(identity, ownerIdentity.connectionToClient); } /// Spawns an object and also assigns Client Authority to the specified client. @@ -1193,6 +1210,7 @@ public static void Spawn(GameObject obj, GameObject ownerPlayer) public static void Spawn(NetworkIdentity spawnIdentity, GameObject ownerPlayer) { NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + if (ownerIdentity == null) { Debug.LogError("Player object has no NetworkIdentity"); @@ -1231,18 +1249,28 @@ public static void Spawn(NetworkIdentity spawnIdentity, NetworkIdentity ownerIde // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerConnection = null) { - if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + NetworkIdentity identity = obj.GetComponent(); + + if (identity == null) { - identity.assetId = assetId; + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); + return; } - SpawnObject(obj, ownerConnection); + + identity.assetId = assetId; + + SpawnObject(identity, ownerConnection); } /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(NetworkIdentity identity, Guid assetId, NetworkConnection ownerConnection = null) { - identity.assetId = assetId; + if(identity != null) + { + identity.assetId = assetId; + } + SpawnObject(identity, ownerConnection); } From 84e7ac61c9b27462e3fd3d473b7cbb2f8b459068 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Mon, 8 Aug 2022 18:30:00 +0200 Subject: [PATCH 4/8] Added Identity Spawn Methods --- Assets/Mirror/Core/NetworkServer.cs | 445 ++++++++++++---------------- 1 file changed, 188 insertions(+), 257 deletions(-) diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 063b573361..4be0e315e0 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -7,30 +7,10 @@ namespace Mirror { /// NetworkServer handles remote connections and has a local connection for a local client. - public static partial class NetworkServer + public static class NetworkServer { - static bool initialized; - public static int maxConnections; - - /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. - // overwritten by NetworkManager (if any) - public static int tickRate = 30; - - // tick rate is in Hz. - // convert to interval in seconds for convenience where needed. - // - // send interval is 1 / sendRate. - // but for tests we need a way to set it to exactly 0. - // 1 / int.max would not be exactly 0, so handel that manually. - public static float tickInterval => tickRate < int.MaxValue ? 1f / tickRate : 0; // for 30 Hz, that's 33ms - - // time & value snapshot interpolation are separate. - // -> time is interpolated globally on NetworkClient / NetworkConnection - // -> value is interpolated per-component, i.e. NetworkTransform. - // however, both need to be on the same send interval. - public static int sendRate => tickRate; - public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms - static double lastSendTime; + static bool initialized; + public static int maxConnections; /// Connection to host mode client (if any) public static NetworkConnectionToClient localConnection { get; private set; } @@ -70,27 +50,10 @@ public static partial class NetworkServer // Connected/Disconnected messages over the network causing undefined // behaviour. // => public so that custom NetworkManagers can hook into it - public static Action OnConnectedEvent; - public static Action OnDisconnectedEvent; + public static Action OnConnectedEvent; + public static Action OnDisconnectedEvent; public static Action OnErrorEvent; - // keep track of actual achieved tick rate. - // might become lower under heavy load. - // very useful for profiling etc. - // measured over 1s each, same as frame rate. no EMA here. - public static int actualTickRate; - static double actualTickRateStart; // start time when counting - static int actualTickRateCounter; // current counter since start - - // profiling - // includes transport update time, because transport calls handlers etc. - // averaged over 1s by passing 'tickRate' to constructor. - public static TimeSample earlyUpdateDuration; - public static TimeSample lateUpdateDuration; - - // capture full Unity update time from before Early- to after LateUpdate - public static TimeSample fullUpdateDuration; - // initialization / shutdown /////////////////////////////////////////// static void Initialize() { @@ -109,33 +72,28 @@ static void Initialize() // reset NetworkTime NetworkTime.ResetStatics(); - Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.active' first"); + Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.activeTransport' first"); AddTransportHandlers(); initialized = true; - - // profiling - earlyUpdateDuration = new TimeSample(sendRate); - lateUpdateDuration = new TimeSample(sendRate); - fullUpdateDuration = new TimeSample(sendRate); } static void AddTransportHandlers() { // += so that other systems can also hook into it (i.e. statistics) - Transport.active.OnServerConnected += OnTransportConnected; - Transport.active.OnServerDataReceived += OnTransportData; - Transport.active.OnServerDisconnected += OnTransportDisconnected; - Transport.active.OnServerError += OnTransportError; + Transport.activeTransport.OnServerConnected += OnTransportConnected; + Transport.activeTransport.OnServerDataReceived += OnTransportData; + Transport.activeTransport.OnServerDisconnected += OnTransportDisconnected; + Transport.activeTransport.OnServerError += OnTransportError; } static void RemoveTransportHandlers() { // -= so that other systems can also hook into it (i.e. statistics) - Transport.active.OnServerConnected -= OnTransportConnected; - Transport.active.OnServerDataReceived -= OnTransportData; - Transport.active.OnServerDisconnected -= OnTransportDisconnected; - Transport.active.OnServerError -= OnTransportError; + Transport.activeTransport.OnServerConnected -= OnTransportConnected; + Transport.activeTransport.OnServerDataReceived -= OnTransportData; + Transport.activeTransport.OnServerDisconnected -= OnTransportDisconnected; + Transport.activeTransport.OnServerError -= OnTransportError; } // calls OnStartClient for all SERVER objects in host mode once. @@ -147,7 +105,7 @@ public static void ActivateHostScene() if (!identity.isClient) { // Debug.Log($"ActivateHostScene {identity.netId} {identity}"); - NetworkClient.CheckForStartClient(identity); + identity.OnStartClient(); } } } @@ -157,8 +115,6 @@ internal static void RegisterMessageHandlers() RegisterHandler(OnClientReadyMessage); RegisterHandler(OnCommandMessage); RegisterHandler(NetworkTime.OnServerPing, false); - RegisterHandler(OnEntityStateMessage, true); - RegisterHandler(OnTimeSnapshotMessage, true); } /// Starts server and listens to incoming connections with max connections limit. @@ -170,7 +126,7 @@ public static void Listen(int maxConns) // only start server if we want to listen if (!dontListen) { - Transport.active.ServerStart(); + Transport.activeTransport.ServerStart(); //Debug.Log("Server started listening"); } @@ -227,7 +183,7 @@ public static void Shutdown() // someone might enabled dontListen at runtime. // but we still need to stop the server. // fixes https://github.com/vis2k/Mirror/issues/2536 - Transport.active.ServerStop(); + Transport.activeTransport.ServerStop(); // transport handlers are hooked into when initializing. // so only remove them when shutting down. @@ -240,7 +196,6 @@ public static void Shutdown() dontListen = false; active = false; isLoadingScene = false; - lastSendTime = 0; localConnection = null; @@ -340,7 +295,7 @@ public static void SendToAll(T message, int channelId = Channels.Reliable, bo using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - NetworkMessages.Pack(message, writer); + MessagePacking.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); // filter and then send to all internet connections at once @@ -380,13 +335,13 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI where T : struct, NetworkMessage { // Debug.Log($"Server.SendToObservers {typeof(T)}"); - if (identity == null || identity.observers.Count == 0) + if (identity == null || identity.observers == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message into byte[] once - NetworkMessages.Pack(message, writer); + MessagePacking.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); foreach (NetworkConnectionToClient conn in identity.observers.Values) @@ -399,22 +354,22 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI } /// Send a message to only clients which are ready with option to include the owner of the object identity - // TODO obsolete this later. it's not used anymore + // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! public static void SendToReadyObservers(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) where T : struct, NetworkMessage { // Debug.Log($"Server.SendToReady {typeof(T)}"); - if (identity == null || identity.observers.Count == 0) + if (identity == null || identity.observers == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - NetworkMessages.Pack(message, writer); + MessagePacking.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); int count = 0; - foreach (NetworkConnectionToClient conn in identity.observers.Values) + foreach (NetworkConnection conn in identity.observers.Values) { bool isOwner = conn == identity.connectionToClient; if ((!isOwner || includeOwner) && conn.isReady) @@ -429,7 +384,7 @@ public static void SendToReadyObservers(NetworkIdentity identity, T message, } /// Send a message to only clients which are ready including the owner of the NetworkIdentity - // TODO obsolete this later. it's not used anymore + // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) where T : struct, NetworkMessage { @@ -448,14 +403,14 @@ static void OnTransportConnected(int connectionId) if (connectionId == 0) { Debug.LogError($"Server.HandleConnect: invalid connectionId: {connectionId} . Needs to be != 0, because 0 is reserved for local player."); - Transport.active.ServerDisconnect(connectionId); + Transport.activeTransport.ServerDisconnect(connectionId); return; } // connectionId not in use yet? if (connections.ContainsKey(connectionId)) { - Transport.active.ServerDisconnect(connectionId); + Transport.activeTransport.ServerDisconnect(connectionId); // Debug.Log($"Server connectionId {connectionId} already in use...kicked client"); return; } @@ -474,7 +429,7 @@ static void OnTransportConnected(int connectionId) else { // kick - Transport.active.ServerDisconnect(connectionId); + Transport.activeTransport.ServerDisconnect(connectionId); // Debug.Log($"Server full, kicked client {connectionId}"); } } @@ -490,7 +445,7 @@ internal static void OnConnected(NetworkConnectionToClient conn) static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId) { - if (NetworkMessages.UnpackId(reader, out ushort msgType)) + if (MessagePacking.Unpack(reader, out ushort msgType)) { // try to invoke the handler for that message if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) @@ -552,7 +507,7 @@ internal static void OnTransportData(int connectionId, ArraySegment data, connection.unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) { // enough to read at least header size? - if (reader.Remaining >= NetworkMessages.IdSize) + if (reader.Remaining >= MessagePacking.HeaderSize) { // make remoteTimeStamp available to the user connection.remoteTimeStamp = remoteTimestamp; @@ -653,12 +608,12 @@ static void OnTransportError(int connectionId, TransportError error, string reas public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = NetworkMessages.GetId(); + ushort msgType = MessagePacking.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); + handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); } /// Register a handler for message type T. Most should require authentication. @@ -666,20 +621,20 @@ public static void RegisterHandler(Action handl public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = NetworkMessages.GetId(); + ushort msgType = MessagePacking.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); + handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); } /// Replace a handler for message type T. Most should require authentication. public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = NetworkMessages.GetId(); - handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); + ushort msgType = MessagePacking.GetId(); + handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); } /// Replace a handler for message type T. Most should require authentication. @@ -693,7 +648,7 @@ public static void ReplaceHandler(Action handler, bool requireAuthenticati public static void UnregisterHandler() where T : struct, NetworkMessage { - ushort msgType = NetworkMessages.GetId(); + ushort msgType = MessagePacking.GetId(); handlers.Remove(msgType); } @@ -784,7 +739,7 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.isOwned = true; + identity.hasAuthority = true; NetworkClient.InternalAddPlayer(identity); } @@ -804,7 +759,7 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb // for that object. This function is used for "adding" a player, not for // "replacing" the player on a connection. If there is already a player // on this playerControllerId for this connection, this will fail. - public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId) + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId) { if (GetNetworkIdentity(player, out NetworkIdentity identity)) { @@ -844,7 +799,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.isOwned = true; + identity.hasAuthority = true; NetworkClient.InternalAddPlayer(identity); } @@ -877,7 +832,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga /// Replaces connection's player object. The old object is not destroyed. // This does NOT change the ready state of the connection, so it can // safely be used while changing scenes. - public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, bool keepAuthority = false) + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId, bool keepAuthority = false) { if (GetNetworkIdentity(player, out NetworkIdentity identity)) { @@ -1007,70 +962,13 @@ static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) - identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn); - } - - // client to server broadcast ////////////////////////////////////////// - // for client's owned ClientToServer components. - static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message) - { - // need to validate permissions carefully. - // an attacker may attempt to modify a not-owned or not-ClientToServer component. - - // valid netId? - if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) - { - // owned by the connection? - if (identity.connectionToClient == connection) - { - using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) - { - // DeserializeServer checks permissions internally. - // failure to deserialize disconnects to prevent exploits. - if (!identity.DeserializeServer(reader)) - { - Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); - connection.Disconnect(); - } - } - } - // an attacker may attempt to modify another connection's entity - else - { - Debug.LogWarning($"Connection {connection.connectionId} attempted to modify {identity} which is not owned by the connection. Disconnecting the connection."); - connection.Disconnect(); - } - } - // no warning. don't spam server logs. - // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); - } - - // client sends TimeSnapshotMessage every sendInterval. - // batching already includes the remoteTimestamp. - // we simply insert it on-message here. - // => only for reliable channel. unreliable would always arrive earlier. - static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnapshotMessage _) - { - // insert another snapshot for snapshot interpolation. - // before calling OnDeserialize so components can use - // NetworkTime.time and NetworkTime.timeStamp. - - // TODO validation? - // maybe we shouldn't allow timeline to deviate more than a certain %. - // for now, this is only used for client authority movement. - -#if !UNITY_2020_3_OR_NEWER - // Unity 2019 doesn't have Time.timeAsDouble yet - connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); -#else - connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); -#endif + identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn as NetworkConnectionToClient); } // spawning //////////////////////////////////////////////////////////// static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) { - // Only call SerializeAll if there are NetworkBehaviours + // Only call OnSerializeAllSafely if there are NetworkBehaviours if (identity.NetworkBehaviours.Length == 0) { return default; @@ -1078,7 +976,7 @@ static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentit // serialize all components with initialState = true // (can be null if has none) - identity.SerializeServer(true, ownerWriter, observersWriter); + identity.OnSerializeAllSafely(true, ownerWriter, observersWriter); // convert to ArraySegment to avoid reader allocations // if nothing was written, .ToArraySegment returns an empty segment. @@ -1168,37 +1066,73 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) return; } - // Spawn should only be called once per netId - if (spawned.ContainsKey(identity.netId)) - { - Debug.LogWarning($"{identity} with netId={identity.netId} was already spawned."); - return; - } - identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; // special case to make sure hasAuthority is set // on start server in host mode if (ownerConnection is LocalConnectionToClient) - identity.isOwned = true; + identity.hasAuthority = true; + + identity.OnStartServer(); + + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); + + if (aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnSpawned(identity); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + RebuildObservers(identity, true); + } + + static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnection) + { + GameObject obj = identity.gameObject; + + // verify if we can spawn this + if (Utils.IsPrefab(obj)) + { + Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first."); + return; + } - // only call OnStartServer if not spawned yet. - // check used to be in NetworkIdentity. may not be necessary anymore. - if (!identity.isServer && identity.netId == 0) + if (!active) { - // configure NetworkIdentity - identity.isLocalPlayer = NetworkClient.localPlayer == identity; - identity.isClient = NetworkClient.active; - identity.isServer = true; - identity.netId = NetworkIdentity.GetNextNetworkId(); + Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server."); + return; + } - // add to spawned (after assigning netId) - spawned[identity.netId] = identity; + if (identity == null) + { + Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}"); + return; + } - // callback after all fields were set - identity.OnStartServer(); + if (identity.SpawnedFromInstantiate) + { + // Using Instantiate on SceneObject is not allowed, so stop spawning here + // NetworkIdentity.Awake already logs error, no need to log a second error here + return; } + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; + + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.hasAuthority = true; + + identity.OnStartServer(); + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); if (aoi) @@ -1226,6 +1160,14 @@ public static void Spawn(GameObject obj, NetworkConnection ownerConnection = nul SpawnObject(obj, ownerConnection); } + /// Spawn the given game object on all clients which are ready. + // This will cause a new object to be instantiated from the registered + // prefab, or from a custom spawn function. + public static void Spawn(NetworkIdentity identity, NetworkConnection ownerConnection = null) + { + SpawnObject(identity, ownerConnection); + } + /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, GameObject ownerPlayer) @@ -1248,7 +1190,46 @@ public static void Spawn(GameObject obj, GameObject ownerPlayer) /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerConnection = null) + public static void Spawn(NetworkIdentity spawnIdentity, GameObject ownerPlayer) + { + NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + if (ownerIdentity == null) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (ownerIdentity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(spawnIdentity, ownerIdentity.connectionToClient); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity spawnIdentity, NetworkIdentity ownerIdentity) + { + if (ownerIdentity == null) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (ownerIdentity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(spawnIdentity, ownerIdentity.connectionToClient); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerConnection = null) { if (GetNetworkIdentity(obj, out NetworkIdentity identity)) { @@ -1257,6 +1238,29 @@ public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerCo SpawnObject(obj, ownerConnection); } + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity identity, Guid assetId, NetworkConnection ownerConnection = null) + { + identity.assetId = assetId; + SpawnObject(identity, ownerConnection); + } + + internal static bool ValidateSceneObject(NetworkIdentity identity) + { + if (identity.gameObject.hideFlags == HideFlags.NotEditable || + identity.gameObject.hideFlags == HideFlags.HideAndDontSave) + return false; + +#if UNITY_EDITOR + if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) + return false; +#endif + + // If not a scene object + return identity.sceneId != 0; + } + /// Spawns NetworkIdentities in the scene on the server. // NetworkIdentity objects in a scene are disabled by default. Calling // SpawnObjects() causes these scene objects to be enabled and spawned. @@ -1272,7 +1276,7 @@ public static bool SpawnObjects() // first pass: activate all scene objects foreach (NetworkIdentity identity in identities) { - if (Utils.IsSceneObject(identity)) + if (ValidateSceneObject(identity)) { // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); identity.gameObject.SetActive(true); @@ -1292,7 +1296,7 @@ public static bool SpawnObjects() // second pass: spawn all scene objects foreach (NetworkIdentity identity in identities) { - if (Utils.IsSceneObject(identity)) + if (ValidateSceneObject(identity)) // pass connection so that authority is not lost when server loads a scene // https://github.com/vis2k/Mirror/pull/2987 Spawn(identity.gameObject, identity.connectionToClient); @@ -1441,10 +1445,7 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) identity.connectionToClient?.RemoveOwnedObject(identity); // send object destroy message to all observers, clear observers - SendToObservers(identity, new ObjectDestroyMessage - { - netId = identity.netId - }); + SendToObservers(identity, new ObjectDestroyMessage{netId = identity.netId}); identity.ClearObservers(); // in host mode, call OnStopClient/OnStopLocalPlayer manually @@ -1457,11 +1458,10 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) // The object may have been spawned with host client ownership, // e.g. a pet so we need to clear hasAuthority and call // NotifyAuthority which invokes OnStopAuthority if hasAuthority. - identity.isOwned = false; + identity.hasAuthority = false; identity.NotifyAuthority(); // remove from NetworkClient dictionary - NetworkClient.connection.owned.Remove(identity); NetworkClient.spawned.Remove(identity.netId); } @@ -1666,6 +1666,10 @@ static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) // both worlds without any worrying now! public static void RebuildObservers(NetworkIdentity identity, bool initialize) { + // observers are null until OnStartServer creates them + if (identity.observers == null) + return; + // if there is no interest management system, // or if 'force shown' then add all connections if (aoi == null || identity.visible == Visibility.ForceShown) @@ -1681,11 +1685,11 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize) // broadcasting //////////////////////////////////////////////////////// // helper function to get the right serialization for a connection - static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + static NetworkWriter GetEntitySerializationForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) { // get serialization for this entity (cached) // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount); + NetworkIdentitySerialization serialization = identity.GetSerializationAtTick(Time.frameCount); // is this entity owned by this connection? bool owned = identity.connectionToClient == connection; @@ -1724,7 +1728,7 @@ static void BroadcastToConnection(NetworkConnectionToClient connection) { // get serialization for this entity viewed by this connection // (if anything was serialized this time) - NetworkWriter serialization = SerializeForConnection(identity, connection); + NetworkWriter serialization = GetEntitySerializationForConnection(identity, connection); if (serialization != null) { EntityStateMessage message = new EntityStateMessage @@ -1770,17 +1774,6 @@ static void Broadcast() // pull in UpdateVarsMessage for each entity it observes if (connection.isReady) { - // send time for snapshot interpolation every sendInterval. - // BroadcastToConnection() may not send if nothing is new. - // - // sent over unreliable. - // NetworkTime / Transform both use unreliable. - // - // make sure Broadcast() is only called every sendInterval, - // even if targetFrameRate isn't set in host mode (!) - // (done via AccurateInterval) - connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); - // broadcast world state to this connection BroadcastToConnection(connection); } @@ -1819,83 +1812,21 @@ static void Broadcast() // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkEarlyUpdate() { - // measure update time for profiling. - if (active) - { - earlyUpdateDuration.Begin(); - fullUpdateDuration.Begin(); - } - // process all incoming messages first before updating the world - if (Transport.active != null) - Transport.active.ServerEarlyUpdate(); - - // step each connection's local time interpolation in early update. - foreach (NetworkConnectionToClient connection in connections.Values) - connection.UpdateTimeInterpolation(); - - if (active) earlyUpdateDuration.End(); + if (Transport.activeTransport != null) + Transport.activeTransport.ServerEarlyUpdate(); } internal static void NetworkLateUpdate() { + // only broadcast world if active if (active) - { - // measure update time for profiling. - lateUpdateDuration.Begin(); - - // only broadcast world if active - // broadcast every sendInterval. - // AccurateInterval to avoid update frequency inaccuracy issues: - // https://github.com/vis2k/Mirror/pull/3153 - // - // for example, host mode server doesn't set .targetFrameRate. - // Broadcast() would be called every tick. - // snapshots might be sent way too often, etc. - // - // during tests, we always call Broadcast() though. - // - // also important for syncInterval=0 components like - // NetworkTransform, so they can sync on same interval as time - // snapshots _but_ not every single tick. - if (!Application.isPlaying || -#if !UNITY_2020_3_OR_NEWER - // Unity 2019 doesn't have Time.timeAsDouble yet - AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime)) -#else - AccurateInterval.Elapsed(Time.timeAsDouble, sendInterval, ref lastSendTime)) -#endif - { - Broadcast(); - } - } + Broadcast(); // process all outgoing messages after updating the world // (even if not active. still want to process disconnects etc.) - if (Transport.active != null) - Transport.active.ServerLateUpdate(); - - // measure actual tick rate every second. - if (active) - { - ++actualTickRateCounter; - - // NetworkTime.localTime has defines for 2019 / 2020 compatibility - if (NetworkTime.localTime >= actualTickRateStart + 1) - { - // calculate avg by exact elapsed time. - // assuming 1s wouldn't be accurate, usually a few more ms passed. - float elapsed = (float)(NetworkTime.localTime - actualTickRateStart); - actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed); - actualTickRateStart = NetworkTime.localTime; - actualTickRateCounter = 0; - } - - // measure total update time. including transport. - // because in early update, transport update calls handlers. - lateUpdateDuration.End(); - fullUpdateDuration.End(); - } + if (Transport.activeTransport != null) + Transport.activeTransport.ServerLateUpdate(); } } } From 4b3dd236594db71b696c45395c191dfd41834948 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Mon, 8 Aug 2022 19:03:40 +0200 Subject: [PATCH 5/8] Added New Tests & Fixed a Test Added possibly obsolete tests for Spawn using Network Identity Fixed Shutdown_DisabledAllSpawnedPrefabs() using wrong identity --- .../Tests/Editor/NetworkIdentityTests.cs | 45 +++++++++++++++++++ .../Tests/Runtime/NetworkServerRuntimeTest.cs | 14 +++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs index f1b0b9d49b..400cd5eb26 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentityTests.cs @@ -278,6 +278,24 @@ public void ServerMode_IsFlags_Test() Assert.That(component.OnStartServer_isServer, Is.EqualTo(true)); } + // check isClient/isServer/isLocalPlayer in server-only mode + [Test] + public void ServerMode_IsFlags_Identity_Test() + { + CreateNetworked(out GameObject _, out NetworkIdentity identity, out IsClientServerCheckComponent component); + + // start the server + NetworkServer.Listen(1000); + + // spawn it + NetworkServer.Spawn(identity); + + // OnStartServer should have been called. check the flags. + Assert.That(component.OnStartServer_isClient, Is.EqualTo(false)); + Assert.That(component.OnStartServer_isLocalPlayer, Is.EqualTo(false)); + Assert.That(component.OnStartServer_isServer, Is.EqualTo(true)); + } + // check isClient/isServer/isLocalPlayer in host mode [Test] public void HostMode_IsFlags_Test() @@ -305,6 +323,33 @@ public void HostMode_IsFlags_Test() NetworkServer.RemoveLocalConnection(); } + // check isClient/isServer/isLocalPlayer in host mode + [Test] + public void HostMode_IsFlags_Identity_Test() + { + CreateNetworked(out GameObject gameObject, out NetworkIdentity identity, out IsClientServerCheckComponent component); + + // start the server + NetworkServer.Listen(1000); + + // start the client + NetworkClient.ConnectHost(); + + // set is as local player + NetworkClient.InternalAddPlayer(identity); + + // spawn it + NetworkServer.Spawn(identity); + + // OnStartServer should have been called. check the flags. + Assert.That(component.OnStartServer_isClient, Is.EqualTo(true)); + Assert.That(component.OnStartServer_isLocalPlayer, Is.EqualTo(true)); + Assert.That(component.OnStartServer_isServer, Is.EqualTo(true)); + + // stop the client + NetworkServer.RemoveLocalConnection(); + } + [Test] public void GetSetAssetId() { diff --git a/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs b/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs index 03559af624..c5f3140004 100644 --- a/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs +++ b/Assets/Mirror/Tests/Runtime/NetworkServerRuntimeTest.cs @@ -62,6 +62,8 @@ public IEnumerator Shutdown_DestroysAllSpawnedPrefabs() NetworkIdentity identity1 = SpawnPrefab(prefab); NetworkIdentity identity2 = SpawnPrefab(prefab); + NetworkIdentity identity3 = SpawnNetworkPrefab(prefab); + NetworkIdentity identity4 = SpawnNetworkPrefab(prefab); // shutdown, wait 1 frame for unity to destroy objects NetworkServer.Shutdown(); @@ -71,6 +73,8 @@ public IEnumerator Shutdown_DestroysAllSpawnedPrefabs() // need to use untiy `==` check Assert.IsTrue(identity1 == null); Assert.IsTrue(identity2 == null); + Assert.IsTrue(identity3 == null); + Assert.IsTrue(identity4 == null); Assert.That(NetworkServer.spawned, Is.Empty); } @@ -84,6 +88,14 @@ NetworkIdentity SpawnPrefab(GameObject prefab) return identity1; } + NetworkIdentity SpawnNetworkPrefab(GameObject prefab) + { + NetworkIdentity clone1 = GameObject.Instantiate(prefab).GetComponent(); + NetworkServer.Spawn(clone1); + Assert.IsTrue(NetworkServer.spawned.ContainsValue(clone1)); + return clone1; + } + [Test] public void Shutdown_DisablesAllSpawnedPrefabs() { @@ -104,7 +116,7 @@ public void Shutdown_DisablesAllSpawnedPrefabs() Assert.IsTrue(identity1 != null); Assert.IsTrue(identity2 != null); Assert.IsFalse(identity1.gameObject.activeSelf); - Assert.IsFalse(identity1.gameObject.activeSelf); + Assert.IsFalse(identity2.gameObject.activeSelf); Assert.That(NetworkServer.spawned, Is.Empty); } From a4cbdb6016b25b9e0278d0ecc0838b150b37b148 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Thu, 11 Aug 2022 20:41:25 +0200 Subject: [PATCH 6/8] Adjusted Spawn methods to use NetworkIdentity --- Assets/Mirror/Core/NetworkServer.cs | 58 +++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 4be0e315e0..935ffd5bdb 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -1096,6 +1096,13 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnection) { + // ensure spawn object has a network identity + if (identity == null) + { + Debug.LogError($"SpawnObject has no NetworkIdentity. Please add a NetworkIdentity to the SpawnObject"); + return; + } + GameObject obj = identity.gameObject; // verify if we can spawn this @@ -1111,12 +1118,6 @@ static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnect return; } - if (identity == null) - { - Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}"); - return; - } - if (identity.SpawnedFromInstantiate) { // Using Instantiate on SceneObject is not allowed, so stop spawning here @@ -1157,7 +1158,15 @@ static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnect // prefab, or from a custom spawn function. public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) { - SpawnObject(obj, ownerConnection); + NetworkIdentity identity = obj.GetComponent(); + + if (identity == null) + { + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); + return; + } + + SpawnObject(identity, ownerConnection); } /// Spawn the given game object on all clients which are ready. @@ -1172,20 +1181,28 @@ public static void Spawn(NetworkIdentity identity, NetworkConnection ownerConnec // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, GameObject ownerPlayer) { - NetworkIdentity identity = ownerPlayer.GetComponent(); + NetworkIdentity identity = obj.GetComponent(); + NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + if (identity == null) { - Debug.LogError("Player object has no NetworkIdentity"); + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); return; } - if (identity.connectionToClient == null) + if (ownerIdentity == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + if (ownerIdentity.connectionToClient == null) { Debug.LogError("Player object is not a player."); return; } - Spawn(obj, identity.connectionToClient); + Spawn(identity, ownerIdentity.connectionToClient); } /// Spawns an object and also assigns Client Authority to the specified client. @@ -1193,6 +1210,7 @@ public static void Spawn(GameObject obj, GameObject ownerPlayer) public static void Spawn(NetworkIdentity spawnIdentity, GameObject ownerPlayer) { NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + if (ownerIdentity == null) { Debug.LogError("Player object has no NetworkIdentity"); @@ -1231,18 +1249,28 @@ public static void Spawn(NetworkIdentity spawnIdentity, NetworkIdentity ownerIde // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerConnection = null) { - if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + NetworkIdentity identity = obj.GetComponent(); + + if (identity == null) { - identity.assetId = assetId; + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); + return; } - SpawnObject(obj, ownerConnection); + + identity.assetId = assetId; + + SpawnObject(identity, ownerConnection); } /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(NetworkIdentity identity, Guid assetId, NetworkConnection ownerConnection = null) { - identity.assetId = assetId; + if(identity != null) + { + identity.assetId = assetId; + } + SpawnObject(identity, ownerConnection); } From cb763408ee0ece877b07bf5cdf913ec6603618d1 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Sat, 10 Dec 2022 21:01:18 +0100 Subject: [PATCH 7/8] Reset NetworkServer.cs --- Assets/Mirror/Core/NetworkServer.cs | 477 +++++++++++++++------------- 1 file changed, 259 insertions(+), 218 deletions(-) diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 935ffd5bdb..4021f26efe 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -7,11 +7,31 @@ namespace Mirror { /// NetworkServer handles remote connections and has a local connection for a local client. - public static class NetworkServer + public static partial class NetworkServer { static bool initialized; public static int maxConnections; + /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + // overwritten by NetworkManager (if any) + public static int tickRate = 30; + + // tick rate is in Hz. + // convert to interval in seconds for convenience where needed. + // + // send interval is 1 / sendRate. + // but for tests we need a way to set it to exactly 0. + // 1 / int.max would not be exactly 0, so handel that manually. + public static float tickInterval => tickRate < int.MaxValue ? 1f / tickRate : 0; // for 30 Hz, that's 33ms + + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + public static int sendRate => tickRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; + /// Connection to host mode client (if any) public static NetworkConnectionToClient localConnection { get; private set; } @@ -54,6 +74,23 @@ public static class NetworkServer public static Action OnDisconnectedEvent; public static Action OnErrorEvent; + // keep track of actual achieved tick rate. + // might become lower under heavy load. + // very useful for profiling etc. + // measured over 1s each, same as frame rate. no EMA here. + public static int actualTickRate; + static double actualTickRateStart; // start time when counting + static int actualTickRateCounter; // current counter since start + + // profiling + // includes transport update time, because transport calls handlers etc. + // averaged over 1s by passing 'tickRate' to constructor. + public static TimeSample earlyUpdateDuration; + public static TimeSample lateUpdateDuration; + + // capture full Unity update time from before Early- to after LateUpdate + public static TimeSample fullUpdateDuration; + // initialization / shutdown /////////////////////////////////////////// static void Initialize() { @@ -72,28 +109,33 @@ static void Initialize() // reset NetworkTime NetworkTime.ResetStatics(); - Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.activeTransport' first"); + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.active' first"); AddTransportHandlers(); initialized = true; + + // profiling + earlyUpdateDuration = new TimeSample(sendRate); + lateUpdateDuration = new TimeSample(sendRate); + fullUpdateDuration = new TimeSample(sendRate); } static void AddTransportHandlers() { // += so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnServerConnected += OnTransportConnected; - Transport.activeTransport.OnServerDataReceived += OnTransportData; - Transport.activeTransport.OnServerDisconnected += OnTransportDisconnected; - Transport.activeTransport.OnServerError += OnTransportError; + Transport.active.OnServerConnected += OnTransportConnected; + Transport.active.OnServerDataReceived += OnTransportData; + Transport.active.OnServerDisconnected += OnTransportDisconnected; + Transport.active.OnServerError += OnTransportError; } static void RemoveTransportHandlers() { // -= so that other systems can also hook into it (i.e. statistics) - Transport.activeTransport.OnServerConnected -= OnTransportConnected; - Transport.activeTransport.OnServerDataReceived -= OnTransportData; - Transport.activeTransport.OnServerDisconnected -= OnTransportDisconnected; - Transport.activeTransport.OnServerError -= OnTransportError; + Transport.active.OnServerConnected -= OnTransportConnected; + Transport.active.OnServerDataReceived -= OnTransportData; + Transport.active.OnServerDisconnected -= OnTransportDisconnected; + Transport.active.OnServerError -= OnTransportError; } // calls OnStartClient for all SERVER objects in host mode once. @@ -105,7 +147,7 @@ public static void ActivateHostScene() if (!identity.isClient) { // Debug.Log($"ActivateHostScene {identity.netId} {identity}"); - identity.OnStartClient(); + NetworkClient.CheckForStartClient(identity); } } } @@ -115,6 +157,8 @@ internal static void RegisterMessageHandlers() RegisterHandler(OnClientReadyMessage); RegisterHandler(OnCommandMessage); RegisterHandler(NetworkTime.OnServerPing, false); + RegisterHandler(OnEntityStateMessage, true); + RegisterHandler(OnTimeSnapshotMessage, true); } /// Starts server and listens to incoming connections with max connections limit. @@ -126,7 +170,7 @@ public static void Listen(int maxConns) // only start server if we want to listen if (!dontListen) { - Transport.activeTransport.ServerStart(); + Transport.active.ServerStart(); //Debug.Log("Server started listening"); } @@ -183,7 +227,7 @@ public static void Shutdown() // someone might enabled dontListen at runtime. // but we still need to stop the server. // fixes https://github.com/vis2k/Mirror/issues/2536 - Transport.activeTransport.ServerStop(); + Transport.active.ServerStop(); // transport handlers are hooked into when initializing. // so only remove them when shutting down. @@ -196,6 +240,7 @@ public static void Shutdown() dontListen = false; active = false; isLoadingScene = false; + lastSendTime = 0; localConnection = null; @@ -295,7 +340,7 @@ public static void SendToAll(T message, int channelId = Channels.Reliable, bo using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); // filter and then send to all internet connections at once @@ -335,13 +380,13 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI where T : struct, NetworkMessage { // Debug.Log($"Server.SendToObservers {typeof(T)}"); - if (identity == null || identity.observers == null || identity.observers.Count == 0) + if (identity == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message into byte[] once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); foreach (NetworkConnectionToClient conn in identity.observers.Values) @@ -354,22 +399,22 @@ static void SendToObservers(NetworkIdentity identity, T message, int channelI } /// Send a message to only clients which are ready with option to include the owner of the object identity - // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + // TODO obsolete this later. it's not used anymore public static void SendToReadyObservers(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) where T : struct, NetworkMessage { // Debug.Log($"Server.SendToReady {typeof(T)}"); - if (identity == null || identity.observers == null || identity.observers.Count == 0) + if (identity == null || identity.observers.Count == 0) return; using (NetworkWriterPooled writer = NetworkWriterPool.Get()) { // pack message only once - MessagePacking.Pack(message, writer); + NetworkMessages.Pack(message, writer); ArraySegment segment = writer.ToArraySegment(); int count = 0; - foreach (NetworkConnection conn in identity.observers.Values) + foreach (NetworkConnectionToClient conn in identity.observers.Values) { bool isOwner = conn == identity.connectionToClient; if ((!isOwner || includeOwner) && conn.isReady) @@ -384,7 +429,7 @@ public static void SendToReadyObservers(NetworkIdentity identity, T message, } /// Send a message to only clients which are ready including the owner of the NetworkIdentity - // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + // TODO obsolete this later. it's not used anymore public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) where T : struct, NetworkMessage { @@ -403,14 +448,14 @@ static void OnTransportConnected(int connectionId) if (connectionId == 0) { Debug.LogError($"Server.HandleConnect: invalid connectionId: {connectionId} . Needs to be != 0, because 0 is reserved for local player."); - Transport.activeTransport.ServerDisconnect(connectionId); + Transport.active.ServerDisconnect(connectionId); return; } // connectionId not in use yet? if (connections.ContainsKey(connectionId)) { - Transport.activeTransport.ServerDisconnect(connectionId); + Transport.active.ServerDisconnect(connectionId); // Debug.Log($"Server connectionId {connectionId} already in use...kicked client"); return; } @@ -429,7 +474,7 @@ static void OnTransportConnected(int connectionId) else { // kick - Transport.activeTransport.ServerDisconnect(connectionId); + Transport.active.ServerDisconnect(connectionId); // Debug.Log($"Server full, kicked client {connectionId}"); } } @@ -445,7 +490,7 @@ internal static void OnConnected(NetworkConnectionToClient conn) static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId) { - if (MessagePacking.Unpack(reader, out ushort msgType)) + if (NetworkMessages.UnpackId(reader, out ushort msgType)) { // try to invoke the handler for that message if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) @@ -507,7 +552,7 @@ internal static void OnTransportData(int connectionId, ArraySegment data, connection.unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) { // enough to read at least header size? - if (reader.Remaining >= MessagePacking.HeaderSize) + if (reader.Remaining >= NetworkMessages.IdSize) { // make remoteTimeStamp available to the user connection.remoteTimeStamp = remoteTimestamp; @@ -608,12 +653,12 @@ static void OnTransportError(int connectionId, TransportError error, string reas public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); } /// Register a handler for message type T. Most should require authentication. @@ -621,20 +666,20 @@ public static void RegisterHandler(Action handl public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); } /// Replace a handler for message type T. Most should require authentication. public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); - handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); + ushort msgType = NetworkMessages.GetId(); + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); } /// Replace a handler for message type T. Most should require authentication. @@ -648,7 +693,7 @@ public static void ReplaceHandler(Action handler, bool requireAuthenticati public static void UnregisterHandler() where T : struct, NetworkMessage { - ushort msgType = MessagePacking.GetId(); + ushort msgType = NetworkMessages.GetId(); handlers.Remove(msgType); } @@ -739,7 +784,7 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.hasAuthority = true; + identity.isOwned = true; NetworkClient.InternalAddPlayer(identity); } @@ -759,7 +804,7 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb // for that object. This function is used for "adding" a player, not for // "replacing" the player on a connection. If there is already a player // on this playerControllerId for this connection, this will fail. - public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId) + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId) { if (GetNetworkIdentity(player, out NetworkIdentity identity)) { @@ -799,7 +844,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga // special case, we are in host mode, set hasAuthority to true so that all overrides see it if (conn is LocalConnectionToClient) { - identity.hasAuthority = true; + identity.isOwned = true; NetworkClient.InternalAddPlayer(identity); } @@ -832,7 +877,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga /// Replaces connection's player object. The old object is not destroyed. // This does NOT change the ready state of the connection, so it can // safely be used while changing scenes. - public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, Guid assetId, bool keepAuthority = false) + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, bool keepAuthority = false) { if (GetNetworkIdentity(player, out NetworkIdentity identity)) { @@ -962,13 +1007,70 @@ static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) - identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn as NetworkConnectionToClient); + identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn); + } + + // client to server broadcast ////////////////////////////////////////// + // for client's owned ClientToServer components. + static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message) + { + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. + + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + if (!identity.DeserializeServer(reader)) + { + Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + } + } + // an attacker may attempt to modify another connection's entity + else + { + Debug.LogWarning($"Connection {connection.connectionId} attempted to modify {identity} which is not owned by the connection. Disconnecting the connection."); + connection.Disconnect(); + } + } + // no warning. don't spam server logs. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } + + // client sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. + + // TODO validation? + // maybe we shouldn't allow timeline to deviate more than a certain %. + // for now, this is only used for client authority movement. + +#if !UNITY_2020_3_OR_NEWER + // Unity 2019 doesn't have Time.timeAsDouble yet + connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); +#else + connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); +#endif } // spawning //////////////////////////////////////////////////////////// static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) { - // Only call OnSerializeAllSafely if there are NetworkBehaviours + // Only call SerializeAll if there are NetworkBehaviours if (identity.NetworkBehaviours.Length == 0) { return default; @@ -976,7 +1078,7 @@ static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentit // serialize all components with initialState = true // (can be null if has none) - identity.OnSerializeAllSafely(true, ownerWriter, observersWriter); + identity.SerializeServer(true, ownerWriter, observersWriter); // convert to ArraySegment to avoid reader allocations // if nothing was written, .ToArraySegment returns an empty segment. @@ -1066,62 +1168,10 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) return; } - identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; - - // special case to make sure hasAuthority is set - // on start server in host mode - if (ownerConnection is LocalConnectionToClient) - identity.hasAuthority = true; - - identity.OnStartServer(); - - // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); - - if (aoi) + // Spawn should only be called once per netId + if (spawned.ContainsKey(identity.netId)) { - // This calls user code which might throw exceptions - // We don't want this to leave us in bad state - try - { - aoi.OnSpawned(identity); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - - RebuildObservers(identity, true); - } - - static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnection) - { - // ensure spawn object has a network identity - if (identity == null) - { - Debug.LogError($"SpawnObject has no NetworkIdentity. Please add a NetworkIdentity to the SpawnObject"); - return; - } - - GameObject obj = identity.gameObject; - - // verify if we can spawn this - if (Utils.IsPrefab(obj)) - { - Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first."); - return; - } - - if (!active) - { - Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server."); - return; - } - - if (identity.SpawnedFromInstantiate) - { - // Using Instantiate on SceneObject is not allowed, so stop spawning here - // NetworkIdentity.Awake already logs error, no need to log a second error here + Debug.LogWarning($"{identity} with netId={identity.netId} was already spawned."); return; } @@ -1130,9 +1180,24 @@ static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnect // special case to make sure hasAuthority is set // on start server in host mode if (ownerConnection is LocalConnectionToClient) - identity.hasAuthority = true; + identity.isOwned = true; + + // only call OnStartServer if not spawned yet. + // check used to be in NetworkIdentity. may not be necessary anymore. + if (!identity.isServer && identity.netId == 0) + { + // configure NetworkIdentity + identity.isLocalPlayer = NetworkClient.localPlayer == identity; + identity.isClient = NetworkClient.active; + identity.isServer = true; + identity.netId = NetworkIdentity.GetNextNetworkId(); - identity.OnStartServer(); + // add to spawned (after assigning netId) + spawned[identity.netId] = identity; + + // callback after all fields were set + identity.OnStartServer(); + } // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); @@ -1158,135 +1223,38 @@ static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnect // prefab, or from a custom spawn function. public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) { - NetworkIdentity identity = obj.GetComponent(); - - if (identity == null) - { - Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); - return; - } - - SpawnObject(identity, ownerConnection); - } - - /// Spawn the given game object on all clients which are ready. - // This will cause a new object to be instantiated from the registered - // prefab, or from a custom spawn function. - public static void Spawn(NetworkIdentity identity, NetworkConnection ownerConnection = null) - { - SpawnObject(identity, ownerConnection); + SpawnObject(obj, ownerConnection); } /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, GameObject ownerPlayer) { - NetworkIdentity identity = obj.GetComponent(); - NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); - + NetworkIdentity identity = ownerPlayer.GetComponent(); if (identity == null) - { - Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); - return; - } - - if (ownerIdentity == null) - { - Debug.LogError("Player object is not a player."); - return; - } - - if (ownerIdentity.connectionToClient == null) - { - Debug.LogError("Player object is not a player."); - return; - } - - Spawn(identity, ownerIdentity.connectionToClient); - } - - /// Spawns an object and also assigns Client Authority to the specified client. - // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(NetworkIdentity spawnIdentity, GameObject ownerPlayer) - { - NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); - - if (ownerIdentity == null) - { - Debug.LogError("Player object has no NetworkIdentity"); - return; - } - - if (ownerIdentity.connectionToClient == null) - { - Debug.LogError("Player object is not a player."); - return; - } - - Spawn(spawnIdentity, ownerIdentity.connectionToClient); - } - - /// Spawns an object and also assigns Client Authority to the specified client. - // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(NetworkIdentity spawnIdentity, NetworkIdentity ownerIdentity) - { - if (ownerIdentity == null) { Debug.LogError("Player object has no NetworkIdentity"); return; } - if (ownerIdentity.connectionToClient == null) + if (identity.connectionToClient == null) { Debug.LogError("Player object is not a player."); return; } - Spawn(spawnIdentity, ownerIdentity.connectionToClient); + Spawn(obj, identity.connectionToClient); } /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(GameObject obj, Guid assetId, NetworkConnection ownerConnection = null) + public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerConnection = null) { - NetworkIdentity identity = obj.GetComponent(); - - if (identity == null) - { - Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); - return; - } - - identity.assetId = assetId; - - SpawnObject(identity, ownerConnection); - } - - /// Spawns an object and also assigns Client Authority to the specified client. - // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(NetworkIdentity identity, Guid assetId, NetworkConnection ownerConnection = null) - { - if(identity != null) + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) { identity.assetId = assetId; } - - SpawnObject(identity, ownerConnection); - } - - internal static bool ValidateSceneObject(NetworkIdentity identity) - { - if (identity.gameObject.hideFlags == HideFlags.NotEditable || - identity.gameObject.hideFlags == HideFlags.HideAndDontSave) - return false; - -#if UNITY_EDITOR - if (UnityEditor.EditorUtility.IsPersistent(identity.gameObject)) - return false; -#endif - - // If not a scene object - return identity.sceneId != 0; + SpawnObject(obj, ownerConnection); } /// Spawns NetworkIdentities in the scene on the server. @@ -1304,7 +1272,7 @@ public static bool SpawnObjects() // first pass: activate all scene objects foreach (NetworkIdentity identity in identities) { - if (ValidateSceneObject(identity)) + if (Utils.IsSceneObject(identity)) { // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); identity.gameObject.SetActive(true); @@ -1324,7 +1292,7 @@ public static bool SpawnObjects() // second pass: spawn all scene objects foreach (NetworkIdentity identity in identities) { - if (ValidateSceneObject(identity)) + if (Utils.IsSceneObject(identity)) // pass connection so that authority is not lost when server loads a scene // https://github.com/vis2k/Mirror/pull/2987 Spawn(identity.gameObject, identity.connectionToClient); @@ -1473,7 +1441,10 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) identity.connectionToClient?.RemoveOwnedObject(identity); // send object destroy message to all observers, clear observers - SendToObservers(identity, new ObjectDestroyMessage{netId = identity.netId}); + SendToObservers(identity, new ObjectDestroyMessage + { + netId = identity.netId + }); identity.ClearObservers(); // in host mode, call OnStopClient/OnStopLocalPlayer manually @@ -1486,10 +1457,11 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode) // The object may have been spawned with host client ownership, // e.g. a pet so we need to clear hasAuthority and call // NotifyAuthority which invokes OnStopAuthority if hasAuthority. - identity.hasAuthority = false; + identity.isOwned = false; identity.NotifyAuthority(); // remove from NetworkClient dictionary + NetworkClient.connection.owned.Remove(identity); NetworkClient.spawned.Remove(identity.netId); } @@ -1694,10 +1666,6 @@ static void RebuildObserversCustom(NetworkIdentity identity, bool initialize) // both worlds without any worrying now! public static void RebuildObservers(NetworkIdentity identity, bool initialize) { - // observers are null until OnStartServer creates them - if (identity.observers == null) - return; - // if there is no interest management system, // or if 'force shown' then add all connections if (aoi == null || identity.visible == Visibility.ForceShown) @@ -1713,11 +1681,11 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize) // broadcasting //////////////////////////////////////////////////////// // helper function to get the right serialization for a connection - static NetworkWriter GetEntitySerializationForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) { // get serialization for this entity (cached) // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - NetworkIdentitySerialization serialization = identity.GetSerializationAtTick(Time.frameCount); + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount); // is this entity owned by this connection? bool owned = identity.connectionToClient == connection; @@ -1756,7 +1724,7 @@ static void BroadcastToConnection(NetworkConnectionToClient connection) { // get serialization for this entity viewed by this connection // (if anything was serialized this time) - NetworkWriter serialization = GetEntitySerializationForConnection(identity, connection); + NetworkWriter serialization = SerializeForConnection(identity, connection); if (serialization != null) { EntityStateMessage message = new EntityStateMessage @@ -1802,6 +1770,17 @@ static void Broadcast() // pull in UpdateVarsMessage for each entity it observes if (connection.isReady) { + // send time for snapshot interpolation every sendInterval. + // BroadcastToConnection() may not send if nothing is new. + // + // sent over unreliable. + // NetworkTime / Transform both use unreliable. + // + // make sure Broadcast() is only called every sendInterval, + // even if targetFrameRate isn't set in host mode (!) + // (done via AccurateInterval) + connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); + // broadcast world state to this connection BroadcastToConnection(connection); } @@ -1840,21 +1819,83 @@ static void Broadcast() // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkEarlyUpdate() { + // measure update time for profiling. + if (active) + { + earlyUpdateDuration.Begin(); + fullUpdateDuration.Begin(); + } + // process all incoming messages first before updating the world - if (Transport.activeTransport != null) - Transport.activeTransport.ServerEarlyUpdate(); + if (Transport.active != null) + Transport.active.ServerEarlyUpdate(); + + // step each connection's local time interpolation in early update. + foreach (NetworkConnectionToClient connection in connections.Values) + connection.UpdateTimeInterpolation(); + + if (active) earlyUpdateDuration.End(); } internal static void NetworkLateUpdate() { - // only broadcast world if active if (active) - Broadcast(); + { + // measure update time for profiling. + lateUpdateDuration.Begin(); + + // only broadcast world if active + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + if (!Application.isPlaying || +#if !UNITY_2020_3_OR_NEWER + // Unity 2019 doesn't have Time.timeAsDouble yet + AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime)) +#else + AccurateInterval.Elapsed(Time.timeAsDouble, sendInterval, ref lastSendTime)) +#endif + { + Broadcast(); + } + } // process all outgoing messages after updating the world // (even if not active. still want to process disconnects etc.) - if (Transport.activeTransport != null) - Transport.activeTransport.ServerLateUpdate(); + if (Transport.active != null) + Transport.active.ServerLateUpdate(); + + // measure actual tick rate every second. + if (active) + { + ++actualTickRateCounter; + + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= actualTickRateStart + 1) + { + // calculate avg by exact elapsed time. + // assuming 1s wouldn't be accurate, usually a few more ms passed. + float elapsed = (float)(NetworkTime.localTime - actualTickRateStart); + actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed); + actualTickRateStart = NetworkTime.localTime; + actualTickRateCounter = 0; + } + + // measure total update time. including transport. + // because in early update, transport update calls handlers. + lateUpdateDuration.End(); + fullUpdateDuration.End(); + } } } } From de81c5ef071a8c1da2e086c83c1002abe8615143 Mon Sep 17 00:00:00 2001 From: realQuartzi <34374881+realQuartzi@users.noreply.github.com> Date: Sat, 10 Dec 2022 21:30:36 +0100 Subject: [PATCH 8/8] Re-added NetworkIdentity Spawn Options --- Assets/Mirror/Core/NetworkServer.cs | 159 +++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 4 deletions(-) diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 4021f26efe..c2d5a58948 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -1218,32 +1218,171 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) RebuildObservers(identity, true); } + static void SpawnObject(NetworkIdentity identity, NetworkConnection ownerConnection) + { + // ensure network identity is not null + if (identity == null) + { + Debug.LogError($"SpawnObject has no NetworkIdentity. Please add a NetworkIdentity to the SpawnObject"); + return; + } + + GameObject obj = identity.gameObject; + + // verify if we can spawn this + if (Utils.IsPrefab(obj)) + { + Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first."); + return; + } + + if (identity.SpawnedFromInstantiate) + { + // Using Instantiate on SceneObject is not allowed, so stop spawning here + // NetworkIdentity.Awake already logs error, no need to log a second error here + return; + } + + // Spawn should only be called once per netId + if (spawned.ContainsKey(identity.netId)) + { + Debug.LogWarning($"{identity} with netId={identity.netId} was already spawned."); + return; + } + + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; + + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.isOwned = true; + + // only call OnStartServer if not spawned yet. + // check used to be in NetworkIdentity. may not be necessary anymore. + if (!identity.isServer && identity.netId == 0) + { + // configure NetworkIdentity + identity.isLocalPlayer = NetworkClient.localPlayer == identity; + identity.isClient = NetworkClient.active; + identity.isServer = true; + identity.netId = NetworkIdentity.GetNextNetworkId(); + + // add to spawned (after assigning netId) + spawned[identity.netId] = identity; + + // callback after all fields were set + identity.OnStartServer(); + } + + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); + + if (aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnSpawned(identity); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + RebuildObservers(identity, true); + } + /// Spawn the given game object on all clients which are ready. // This will cause a new object to be instantiated from the registered // prefab, or from a custom spawn function. public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) { - SpawnObject(obj, ownerConnection); + NetworkIdentity identity = obj.GetComponent(); + + if(identity == null) + { + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); + return; + } + + SpawnObject(identity, ownerConnection); + } + + /// Spawn the given game object on all clients which are ready. + // This will cause a new object to be instantiated from the registered + // prefab, or from a custom spawn function. + public static void Spawn(NetworkIdentity identity, NetworkConnection ownerConnection = null) + { + SpawnObject(identity, ownerConnection); } /// Spawns an object and also assigns Client Authority to the specified client. // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. public static void Spawn(GameObject obj, GameObject ownerPlayer) { - NetworkIdentity identity = ownerPlayer.GetComponent(); + NetworkIdentity identity = obj.GetComponent(); + NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + if (identity == null) + { + Debug.LogError($"GameObject {obj.name} doesn't have NetworkIdentity."); + return; + } + + if (ownerIdentity == null) { Debug.LogError("Player object has no NetworkIdentity"); return; } - if (identity.connectionToClient == null) + if (ownerIdentity.connectionToClient == null) { Debug.LogError("Player object is not a player."); return; } - Spawn(obj, identity.connectionToClient); + Spawn(identity, ownerIdentity.connectionToClient); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity spawnIdentity, GameObject ownerPlayer) + { + NetworkIdentity ownerIdentity = ownerPlayer.GetComponent(); + + if (ownerIdentity == null) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (ownerIdentity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(spawnIdentity, ownerIdentity.connectionToClient); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity spawnIdentity, NetworkIdentity ownerIdentity) + { + if (ownerIdentity == null) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (ownerIdentity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(spawnIdentity, ownerIdentity.connectionToClient); } /// Spawns an object and also assigns Client Authority to the specified client. @@ -1257,6 +1396,18 @@ public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerCo SpawnObject(obj, ownerConnection); } + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(NetworkIdentity identity, uint assetId, NetworkConnection ownerConnection = null) + { + if(identity != null) + { + identity.assetId = assetId; + } + + SpawnObject(identity, ownerConnection); + } + /// Spawns NetworkIdentities in the scene on the server. // NetworkIdentity objects in a scene are disabled by default. Calling // SpawnObjects() causes these scene objects to be enabled and spawned.