/
NetworkClient.cs
1467 lines (1264 loc) · 64.1 KB
/
NetworkClient.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Mirror
{
public enum ConnectState
{
None,
// connecting between Connect() and OnTransportConnected()
Connecting,
Connected,
// disconnecting between Disconnect() and OnTransportDisconnected()
Disconnecting,
Disconnected
}
/// <summary>NetworkClient with connection to server.</summary>
public static class NetworkClient
{
// message handlers by messageId
internal static readonly Dictionary<ushort, NetworkMessageDelegate> handlers =
new Dictionary<ushort, NetworkMessageDelegate>();
/// <summary>All spawned NetworkIdentities by netId.</summary>
// client sees OBSERVED spawned ones.
public static readonly Dictionary<uint, NetworkIdentity> spawned =
new Dictionary<uint, NetworkIdentity>();
/// <summary>Client's NetworkConnection to server.</summary>
public static NetworkConnection connection { get; internal set; }
/// <summary>True if client is ready (= joined world).</summary>
// TODO redundant state. point it to .connection.isReady instead (& test)
// TODO OR remove NetworkConnection.isReady? unless it's used on server
//
// TODO maybe ClientState.Connected/Ready/AddedPlayer/etc.?
// way better for security if we can check states in callbacks
public static bool ready;
/// <summary>NetworkIdentity of the localPlayer </summary>
public static NetworkIdentity localPlayer { get; internal set; }
// NetworkClient state
internal static ConnectState connectState = ConnectState.None;
/// <summary>IP address of the connection to server.</summary>
// empty if the client has not connected yet.
public static string serverIp => connection.address;
/// <summary>active is true while a client is connecting/connected</summary>
// (= while the network is active)
public static bool active => connectState == ConnectState.Connecting ||
connectState == ConnectState.Connected;
/// <summary>Check if client is connecting (before connected).</summary>
public static bool isConnecting => connectState == ConnectState.Connecting;
/// <summary>Check if client is connected (after connecting).</summary>
public static bool isConnected => connectState == ConnectState.Connected;
/// <summary>True if client is running in host mode.</summary>
public static bool isHostClient => connection is LocalConnectionToServer;
// OnConnected / OnDisconnected used to be NetworkMessages that were
// invoked. this introduced a bug where external clients could send
// 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<Exception> OnErrorEvent;
/// <summary>Registered spawnable prefabs by assetId.</summary>
public static readonly Dictionary<Guid, GameObject> prefabs =
new Dictionary<Guid, GameObject>();
// spawn handlers
internal static readonly Dictionary<Guid, SpawnHandlerDelegate> spawnHandlers =
new Dictionary<Guid, SpawnHandlerDelegate>();
internal static readonly Dictionary<Guid, UnSpawnDelegate> unspawnHandlers =
new Dictionary<Guid, UnSpawnDelegate>();
// spawning
// internal for tests
internal static bool isSpawnFinished;
// Disabled scene objects that can be spawned again, by sceneId.
internal static readonly Dictionary<ulong, NetworkIdentity> spawnableObjects =
new Dictionary<ulong, NetworkIdentity>();
static Unbatcher unbatcher = new Unbatcher();
// interest management component (optional)
// only needed for SetHostVisibility
public static InterestManagement aoi;
// scene loading
public static bool isLoadingScene;
// initialization //////////////////////////////////////////////////////
static void AddTransportHandlers()
{
Transport.activeTransport.OnClientConnected = OnTransportConnected;
Transport.activeTransport.OnClientDataReceived = OnTransportData;
Transport.activeTransport.OnClientDisconnected = OnTransportDisconnected;
Transport.activeTransport.OnClientError = OnError;
}
internal static void RegisterSystemHandlers(bool hostMode)
{
// host mode client / remote client react to some messages differently.
// but we still need to add handlers for all of them to avoid
// 'message id not found' errors.
if (hostMode)
{
RegisterHandler<ObjectDestroyMessage>(OnHostClientObjectDestroy);
RegisterHandler<ObjectHideMessage>(OnHostClientObjectHide);
RegisterHandler<NetworkPongMessage>(_ => {}, false);
RegisterHandler<SpawnMessage>(OnHostClientSpawn);
// host mode doesn't need spawning
RegisterHandler<ObjectSpawnStartedMessage>(_ => {});
// host mode doesn't need spawning
RegisterHandler<ObjectSpawnFinishedMessage>(_ => {});
// host mode doesn't need state updates
RegisterHandler<EntityStateMessage>(_ => {});
}
else
{
RegisterHandler<ObjectDestroyMessage>(OnObjectDestroy);
RegisterHandler<ObjectHideMessage>(OnObjectHide);
RegisterHandler<NetworkPongMessage>(NetworkTime.OnClientPong, false);
RegisterHandler<SpawnMessage>(OnSpawn);
RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
}
// These handlers are the same for host and remote clients
RegisterHandler<ChangeOwnerMessage>(OnChangeOwner);
RegisterHandler<RpcMessage>(OnRPCMessage);
}
// connect /////////////////////////////////////////////////////////////
/// <summary>Connect client to a NetworkServer by address.</summary>
public static void Connect(string address)
{
// Debug.Log($"Client Connect: {address}");
Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first");
RegisterSystemHandlers(false);
Transport.activeTransport.enabled = true;
AddTransportHandlers();
connectState = ConnectState.Connecting;
Transport.activeTransport.ClientConnect(address);
connection = new NetworkConnectionToServer();
}
/// <summary>Connect client to a NetworkServer by Uri.</summary>
public static void Connect(Uri uri)
{
// Debug.Log($"Client Connect: {uri}");
Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first");
RegisterSystemHandlers(false);
Transport.activeTransport.enabled = true;
AddTransportHandlers();
connectState = ConnectState.Connecting;
Transport.activeTransport.ClientConnect(uri);
connection = new NetworkConnectionToServer();
}
// TODO why are there two connect host methods?
// called from NetworkManager.FinishStartHost()
public static void ConnectHost()
{
//Debug.Log("Client Connect Host to Server");
RegisterSystemHandlers(true);
connectState = ConnectState.Connected;
// create local connection objects and connect them
LocalConnectionToServer connectionToServer = new LocalConnectionToServer();
LocalConnectionToClient connectionToClient = new LocalConnectionToClient();
connectionToServer.connectionToClient = connectionToClient;
connectionToClient.connectionToServer = connectionToServer;
connection = connectionToServer;
// create server connection to local client
NetworkServer.SetLocalConnection(connectionToClient);
}
/// <summary>Connect host mode</summary>
// called from NetworkManager.StartHostClient
// TODO why are there two connect host methods?
public static void ConnectLocalServer()
{
// call server OnConnected with server's connection to client
NetworkServer.OnConnected(NetworkServer.localConnection);
// call client OnConnected with client's connection to server
// => previously we used to send a ConnectMessage to
// NetworkServer.localConnection. this would queue the message
// until NetworkClient.Update processes it.
// => invoking the client's OnConnected event directly here makes
// tests fail. so let's do it exactly the same order as before by
// queueing the event for next Update!
//OnConnectedEvent?.Invoke(connection);
((LocalConnectionToServer)connection).QueueConnectedEvent();
}
// disconnect //////////////////////////////////////////////////////////
/// <summary>Disconnect from server.</summary>
public static void Disconnect()
{
// only if connected or connecting.
// don't disconnect() again if already in the process of
// disconnecting or fully disconnected.
if (connectState != ConnectState.Connecting &&
connectState != ConnectState.Connected)
return;
// we are disconnecting until OnTransportDisconnected is called.
// setting state to Disconnected would stop OnTransportDisconnected
// from calling cleanup code because it would think we are already
// disconnected fully.
// TODO move to 'cleanup' code below if safe
connectState = ConnectState.Disconnecting;
ready = false;
// call Disconnect on the NetworkConnection
connection?.Disconnect();
// IMPORTANT: do NOT clear connection here yet.
// we still need it in OnTransportDisconnected for callbacks.
// connection = null;
}
// transport events ////////////////////////////////////////////////////
// called by Transport
static void OnTransportConnected()
{
if (connection != null)
{
// reset network time stats
NetworkTime.ResetStatics();
// reset unbatcher in case any batches from last session remain.
unbatcher = new Unbatcher();
// the handler may want to send messages to the client
// thus we should set the connected state before calling the handler
connectState = ConnectState.Connected;
NetworkTime.UpdateClient();
OnConnectedEvent?.Invoke();
}
else Debug.LogError("Skipped Connect message handling because connection is null.");
}
// helper function
static bool UnpackAndInvoke(NetworkReader reader, int channelId)
{
if (MessagePacking.Unpack(reader, out ushort msgType))
{
// try to invoke the handler for that message
if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler))
{
handler.Invoke(connection, reader, channelId);
// message handler may disconnect client, making connection = null
// therefore must check for null to avoid NRE.
if (connection != null)
connection.lastMessageTime = Time.time;
return true;
}
else
{
// message in a batch are NOT length prefixed to save bandwidth.
// every message needs to be handled and read until the end.
// otherwise it would overlap into the next message.
// => need to warn and disconnect to avoid undefined behaviour.
// => WARNING, not error. can happen if attacker sends random data.
Debug.LogWarning($"Unknown message id: {msgType}. This can happen if no handler was registered for this message.");
// simply return false. caller is responsible for disconnecting.
//connection.Disconnect();
return false;
}
}
else
{
// => WARNING, not error. can happen if attacker sends random data.
Debug.LogWarning("Invalid message header.");
// simply return false. caller is responsible for disconnecting.
//connection.Disconnect();
return false;
}
}
// called by Transport
internal static void OnTransportData(ArraySegment<byte> data, int channelId)
{
if (connection != null)
{
// server might batch multiple messages into one packet.
// feed it to the Unbatcher.
// NOTE: we don't need to associate a channelId because we
// always process all messages in the batch.
if (!unbatcher.AddBatch(data))
{
Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting.");
connection.Disconnect();
return;
}
// process all messages in the batch.
// only while NOT loading a scene.
// if we get a scene change message, then we need to stop
// processing. otherwise we might apply them to the old scene.
// => fixes https://github.com/vis2k/Mirror/issues/2651
//
// NOTE: is scene starts loading, then the rest of the batch
// would only be processed when OnTransportData is called
// the next time.
// => consider moving processing to NetworkEarlyUpdate.
while (!isLoadingScene &&
unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp))
{
// enough to read at least header size?
if (reader.Remaining >= MessagePacking.HeaderSize)
{
// make remoteTimeStamp available to the user
connection.remoteTimeStamp = remoteTimestamp;
// handle message
if (!UnpackAndInvoke(reader, channelId))
{
// warn, disconnect and return if failed
// -> warning because attackers might send random data
// -> messages in a batch aren't length prefixed.
// failing to read one would cause undefined
// behaviour for every message afterwards.
// so we need to disconnect.
// -> return to avoid the below unbatches.count error.
// we already disconnected and handled it.
Debug.LogWarning($"NetworkClient: failed to unpack and invoke message. Disconnecting.");
connection.Disconnect();
return;
}
}
// otherwise disconnect
else
{
// WARNING, not error. can happen if attacker sends random data.
Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)");
connection.Disconnect();
return;
}
}
// if we weren't interrupted by a scene change,
// then all batched messages should have been processed now.
// if not, we need to log an error to avoid debugging hell.
// otherwise batches would silently grow.
// we need to log an error to avoid debugging hell.
//
// EXAMPLE: https://github.com/vis2k/Mirror/issues/2882
// -> UnpackAndInvoke silently returned because no handler for id
// -> Reader would never be read past the end
// -> Batch would never be retired because end is never reached
//
// NOTE: prefixing every message in a batch with a length would
// avoid ever not reading to the end. for extra bandwidth.
//
// IMPORTANT: always keep this check to detect memory leaks.
// this took half a day to debug last time.
if (!isLoadingScene && unbatcher.BatchesCount > 0)
{
Debug.LogError($"Still had {unbatcher.BatchesCount} batches remaining after processing, even though processing was not interrupted by a scene change. This should never happen, as it would cause ever growing batches.\nPossible reasons:\n* A message didn't deserialize as much as it serialized\n*There was no message handler for a message id, so the reader wasn't read until the end.");
}
}
else Debug.LogError("Skipped Data message handling because connection is null.");
}
// called by Transport
// IMPORTANT: often times when disconnecting, we call this from Mirror
// too because we want to remove the connection and handle
// the disconnect immediately.
// => which is fine as long as we guarantee it only runs once
// => which we do by setting the state to Disconnected!
internal static void OnTransportDisconnected()
{
// StopClient called from user code triggers Disconnected event
// from transport which calls StopClient again, so check here
// and short circuit running the Shutdown process twice.
if (connectState == ConnectState.Disconnected) return;
// Raise the event before changing ConnectState
// because 'active' depends on this during shutdown
if (connection != null) OnDisconnectedEvent?.Invoke();
connectState = ConnectState.Disconnected;
ready = false;
// now that everything was handled, clear the connection.
// previously this was done in Disconnect() already, but we still
// need it for the above OnDisconnectedEvent.
connection = null;
}
static void OnError(Exception exception)
{
Debug.LogException(exception);
OnErrorEvent?.Invoke(exception);
}
// send ////////////////////////////////////////////////////////////////
/// <summary>Send a NetworkMessage to the server over the given channel.</summary>
public static void Send<T>(T message, int channelId = Channels.Reliable)
where T : struct, NetworkMessage
{
if (connection != null)
{
if (connectState == ConnectState.Connected)
{
connection.Send(message, channelId);
}
else Debug.LogError("NetworkClient Send when not connected to a server");
}
else Debug.LogError("NetworkClient Send with no connection");
}
// message handlers ////////////////////////////////////////////////////
/// <summary>Register a handler for a message type T. Most should require authentication.</summary>
public static void RegisterHandler<T>(Action<T> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
ushort msgType = MessagePacking.GetId<T>();
if (handlers.ContainsKey(msgType))
{
Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning.");
}
// we use the same WrapHandler function for server and client.
// so let's wrap it to ignore the NetworkConnection parameter.
// it's not needed on client. it's always NetworkClient.connection.
void HandlerWrapped(NetworkConnection _, T value) => handler(value);
handlers[msgType] = MessagePacking.WrapHandler((Action<NetworkConnection, T>) HandlerWrapped, requireAuthentication);
}
/// <summary>Replace a handler for a particular message type. Should require authentication by default.</summary>
// RegisterHandler throws a warning (as it should) if a handler is assigned twice
// Use of ReplaceHandler makes it clear the user intended to replace the handler
public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
ushort msgType = MessagePacking.GetId<T>();
handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication);
}
/// <summary>Replace a handler for a particular message type. Should require authentication by default.</summary>
// RegisterHandler throws a warning (as it should) if a handler is assigned twice
// Use of ReplaceHandler makes it clear the user intended to replace the handler
public static void ReplaceHandler<T>(Action<T> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication);
}
/// <summary>Unregister a message handler of type T.</summary>
public static bool UnregisterHandler<T>()
where T : struct, NetworkMessage
{
// use int to minimize collisions
ushort msgType = MessagePacking.GetId<T>();
return handlers.Remove(msgType);
}
// spawnable prefabs ///////////////////////////////////////////////////
/// <summary>Find the registered prefab for this asset id.</summary>
// Useful for debuggers
public static bool GetPrefab(Guid assetId, out GameObject prefab)
{
prefab = null;
return assetId != Guid.Empty &&
prefabs.TryGetValue(assetId, out prefab) && prefab != null;
}
/// <summary>Validates Prefab then adds it to prefabs dictionary.</summary>
static void RegisterPrefabIdentity(NetworkIdentity prefab)
{
if (prefab.assetId == Guid.Empty)
{
Debug.LogError($"Can not Register '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead");
return;
}
if (prefab.sceneId != 0)
{
Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
return;
}
NetworkIdentity[] identities = prefab.GetComponentsInChildren<NetworkIdentity>();
if (identities.Length > 1)
{
Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object.");
}
if (prefabs.ContainsKey(prefab.assetId))
{
GameObject existingPrefab = prefabs[prefab.assetId];
Debug.LogWarning($"Replacing existing prefab with assetId '{prefab.assetId}'. Old prefab '{existingPrefab.name}', New prefab '{prefab.name}'");
}
if (spawnHandlers.ContainsKey(prefab.assetId) || unspawnHandlers.ContainsKey(prefab.assetId))
{
Debug.LogWarning($"Adding prefab '{prefab.name}' with assetId '{prefab.assetId}' when spawnHandlers with same assetId already exists.");
}
// Debug.Log($"Registering prefab '{prefab.name}' as asset:{prefab.assetId}");
prefabs[prefab.assetId] = prefab.gameObject;
}
/// <summary>Register spawnable prefab with custom assetId.</summary>
// Note: newAssetId can not be set on GameObjects that already have an assetId
// Note: registering with assetId is useful for assetbundles etc. a lot
// of people use this.
public static void RegisterPrefab(GameObject prefab, Guid newAssetId)
{
if (prefab == null)
{
Debug.LogError("Could not register prefab because it was null");
return;
}
if (newAssetId == Guid.Empty)
{
Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty");
return;
}
NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
if (identity.assetId != Guid.Empty && identity.assetId != newAssetId)
{
Debug.LogError($"Could not register '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}");
return;
}
identity.assetId = newAssetId;
RegisterPrefabIdentity(identity);
}
/// <summary>Register spawnable prefab.</summary>
public static void RegisterPrefab(GameObject prefab)
{
if (prefab == null)
{
Debug.LogError("Could not register prefab because it was null");
return;
}
NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
RegisterPrefabIdentity(identity);
}
/// <summary>Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers.</summary>
// Note: newAssetId can not be set on GameObjects that already have an assetId
// Note: registering with assetId is useful for assetbundles etc. a lot
// of people use this.
// TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
// We need this check here because we don't want a null handler in the lambda expression below
if (spawnHandler == null)
{
Debug.LogError($"Can not Register null SpawnHandler for {newAssetId}");
return;
}
RegisterPrefab(prefab, newAssetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler);
}
/// <summary>Register a spawnable prefab with custom spawn/unspawn handlers.</summary>
// TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
if (prefab == null)
{
Debug.LogError("Could not register handler for prefab because the prefab was null");
return;
}
NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
if (identity.sceneId != 0)
{
Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
return;
}
Guid assetId = identity.assetId;
if (assetId == Guid.Empty)
{
Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead");
return;
}
// We need this check here because we don't want a null handler in the lambda expression below
if (spawnHandler == null)
{
Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
return;
}
RegisterPrefab(prefab, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler);
}
/// <summary>Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers.</summary>
// Note: newAssetId can not be set on GameObjects that already have an assetId
// Note: registering with assetId is useful for assetbundles etc. a lot
// of people use this.
// TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
if (newAssetId == Guid.Empty)
{
Debug.LogError($"Could not register handler for '{prefab.name}' with new assetId because the new assetId was empty");
return;
}
if (prefab == null)
{
Debug.LogError("Could not register handler for prefab because the prefab was null");
return;
}
NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
if (identity.assetId != Guid.Empty && identity.assetId != newAssetId)
{
Debug.LogError($"Could not register Handler for '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}");
return;
}
if (identity.sceneId != 0)
{
Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
return;
}
identity.assetId = newAssetId;
Guid assetId = identity.assetId;
if (spawnHandler == null)
{
Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
return;
}
if (unspawnHandler == null)
{
Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}");
return;
}
if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId))
{
Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'");
}
if (prefabs.ContainsKey(assetId))
{
// this is error because SpawnPrefab checks prefabs before handler
Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler");
}
NetworkIdentity[] identities = prefab.GetComponentsInChildren<NetworkIdentity>();
if (identities.Length > 1)
{
Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object.");
}
//Debug.Log($"Registering custom prefab {prefab.name} as asset:{assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}");
spawnHandlers[assetId] = spawnHandler;
unspawnHandlers[assetId] = unspawnHandler;
}
/// <summary>Register a spawnable prefab with custom spawn/unspawn handlers.</summary>
// TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate?
public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
if (prefab == null)
{
Debug.LogError("Could not register handler for prefab because the prefab was null");
return;
}
NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
Debug.LogError($"Could not register handler for '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
if (identity.sceneId != 0)
{
Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene.");
return;
}
Guid assetId = identity.assetId;
if (assetId == Guid.Empty)
{
Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead");
return;
}
if (spawnHandler == null)
{
Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
return;
}
if (unspawnHandler == null)
{
Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}");
return;
}
if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId))
{
Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'");
}
if (prefabs.ContainsKey(assetId))
{
// this is error because SpawnPrefab checks prefabs before handler
Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler");
}
NetworkIdentity[] identities = prefab.GetComponentsInChildren<NetworkIdentity>();
if (identities.Length > 1)
{
Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object.");
}
//Debug.Log($"Registering custom prefab {prefab.name} as asset:{assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}");
spawnHandlers[assetId] = spawnHandler;
unspawnHandlers[assetId] = unspawnHandler;
}
/// <summary>Removes a registered spawn prefab that was setup with NetworkClient.RegisterPrefab.</summary>
public static void UnregisterPrefab(GameObject prefab)
{
if (prefab == null)
{
Debug.LogError("Could not unregister prefab because it was null");
return;
}
NetworkIdentity identity = prefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
Debug.LogError($"Could not unregister '{prefab.name}' since it contains no NetworkIdentity component");
return;
}
Guid assetId = identity.assetId;
prefabs.Remove(assetId);
spawnHandlers.Remove(assetId);
unspawnHandlers.Remove(assetId);
}
// spawn handlers //////////////////////////////////////////////////////
/// <summary>This is an advanced spawning function that registers a custom assetId with the spawning system.</summary>
// This can be used to register custom spawning methods for an assetId -
// instead of the usual method of registering spawning methods for a
// prefab. This should be used when no prefab exists for the spawned
// objects - such as when they are constructed dynamically at runtime
// from configuration data.
public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
// We need this check here because we don't want a null handler in the lambda expression below
if (spawnHandler == null)
{
Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
return;
}
RegisterSpawnHandler(assetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler);
}
/// <summary>This is an advanced spawning function that registers a custom assetId with the spawning system.</summary>
// This can be used to register custom spawning methods for an assetId -
// instead of the usual method of registering spawning methods for a
// prefab. This should be used when no prefab exists for the spawned
// objects - such as when they are constructed dynamically at runtime
// from configuration data.
public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler)
{
if (spawnHandler == null)
{
Debug.LogError($"Can not Register null SpawnHandler for {assetId}");
return;
}
if (unspawnHandler == null)
{
Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}");
return;
}
if (assetId == Guid.Empty)
{
Debug.LogError("Can not Register SpawnHandler for empty Guid");
return;
}
if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId))
{
Debug.LogWarning($"Replacing existing spawnHandlers for {assetId}");
}
if (prefabs.ContainsKey(assetId))
{
// this is error because SpawnPrefab checks prefabs before handler
Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}'");
}
// Debug.Log("RegisterSpawnHandler asset {assetId} {spawnHandler.GetMethodName()}/{unspawnHandler.GetMethodName()}");
spawnHandlers[assetId] = spawnHandler;
unspawnHandlers[assetId] = unspawnHandler;
}
/// <summary> Removes a registered spawn handler function that was registered with NetworkClient.RegisterHandler().</summary>
public static void UnregisterSpawnHandler(Guid assetId)
{
spawnHandlers.Remove(assetId);
unspawnHandlers.Remove(assetId);
}
/// <summary>This clears the registered spawn prefabs and spawn handler functions for this client.</summary>
public static void ClearSpawners()
{
prefabs.Clear();
spawnHandlers.Clear();
unspawnHandlers.Clear();
}
internal static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj)
{
if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null)
{
handler(obj);
return true;
}
return false;
}
// ready ///////////////////////////////////////////////////////////////
/// <summary>Sends Ready message to server, indicating that we loaded the scene, ready to enter the game.</summary>
// This could be for example when a client enters an ongoing game and
// has finished loading the current scene. The server should respond to
// the SYSTEM_READY event with an appropriate handler which instantiates
// the players object for example.
public static bool Ready()
{
// Debug.Log($"NetworkClient.Ready() called with connection {conn}");
if (ready)
{
Debug.LogError("NetworkClient is already ready. It shouldn't be called twice.");
return false;
}
// need a valid connection to become ready
if (connection == null)
{
Debug.LogError("Ready() called with invalid connection object: conn=null");
return false;
}
// Set these before sending the ReadyMessage, otherwise host client
// will fail in InternalAddPlayer with null readyConnection.
// TODO this is redundant. have one source of truth for .ready
ready = true;
connection.isReady = true;
// Tell server we're ready to have a player object spawned
connection.Send(new ReadyMessage());
return true;
}
// add player //////////////////////////////////////////////////////////
// called from message handler for Owner message
internal static void InternalAddPlayer(NetworkIdentity identity)
{
//Debug.Log("NetworkClient.InternalAddPlayer");
// NOTE: It can be "normal" when changing scenes for the player to be destroyed and recreated.
// But, the player structures are not cleaned up, we'll just replace the old player
localPlayer = identity;
// NOTE: we DONT need to set isClient=true here, because OnStartClient
// is called before OnStartLocalPlayer, hence it's already set.
// localPlayer.isClient = true;
// TODO this check might not be necessary
//if (readyConnection != null)
if (ready && connection != null)
{
connection.identity = identity;
}
else Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer");
}
/// <summary>Sends AddPlayer message to the server, indicating that we want to join the world.</summary>
public static bool AddPlayer()
{
// ensure valid ready connection
if (connection == null)
{
Debug.LogError("AddPlayer requires a valid NetworkClient.connection.");
return false;
}
// UNET checked 'if readyConnection != null'.
// in other words, we need a connection and we need to be ready.
if (!ready)
{
Debug.LogError("AddPlayer requires a ready NetworkClient.");
return false;
}
if (connection.identity != null)
{
Debug.LogError("NetworkClient.AddPlayer: a PlayerController was already added. Did you call AddPlayer twice?");
return false;
}
// Debug.Log($"NetworkClient.AddPlayer() called with connection {readyConnection}");
connection.Send(new AddPlayerMessage());
return true;
}
// spawning ////////////////////////////////////////////////////////////
internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message)
{
if (message.assetId != Guid.Empty)
identity.assetId = message.assetId;
if (!identity.gameObject.activeSelf)
{
identity.gameObject.SetActive(true);
}
// apply local values for VR support
identity.transform.localPosition = message.position;
identity.transform.localRotation = message.rotation;
identity.transform.localScale = message.scale;
identity.hasAuthority = message.isOwner;
identity.netId = message.netId;
if (message.isLocalPlayer)