Skip to content

Commit 632bf35

Browse files
fix: networkvariable collections can be modified without write permissions and more (#3081)
* fix Provide additional copy of last known current NetworkVariable.Value to be able to compare against the current local value in order to detect if a client without write permissions has modified a collection. * test Updated collections validation tests to spot check the restore known current state when client without write permissions modifies a collection. * update added changelog entry * fix Changing order of operations within CheckDirtyState for clients without write permissions * test Add some spot checks to one of the dictionary tests * fix Fixing issue where upon a client gaining ownership of a NetworkObject that has one or more owner write permission NetworkVariables using a collection type with keyed indices (like a dictionary) can resend already synchronized entries when making a change to the collection causing non-owner clients to throw a key already exists exception. * update Adding changelog entrry * update Adjusted when the NetworkVariable update for ownership change is invoked to account for updates to owner write NetworkVariables within the OnGainedOwnership callback. When changing ownership: - Marking any owner read permissions NetworkVariables as dirty when sending to the new owner (both within NetworkSpawnManager for server-side update and within the NetworkVariableDeltaMessage). - Sending any pending updates to all clients prior to sending the change in ownership message to assure pending updates are synchronized as the owner. When initially synchronizing a client, if a NetworkVariable has a pending state update then send serialize the previously known value(s) to the synchronizing client so when the pending updates are sent they don't duplicate values. * test Adjusting two deferred message tests to not account for a NetworkVariable delta state update message when changing ownership and there are no pending updates. * update updating changelog entries * style * fix This includes additional fixes for NetworkVariable collections that ended up requiring a different approach to how a server forwards NetworkVariable deltas. Now, NetworkVariableDeltaMessage forwards NetworkVariable field deltas immediately after a server has finished processing the deltas (i.e. the keeping a NetworkVariable dirty concept is not used from this point forward). I went ahead and kept the compatibility of this functionality. NetworkVariableDeltaMessage has had its Version incremented as we now send the NetworkDelivery type in order to be able to handle this (it seemed less risky to include this than to try and bubble up this property to all message types). This also separates the duplication of the "original internal value" and the "previous value" until after processing all deltas. If the server has to foward, it forwards first then invokes the PostDeltaRead method that performs the duplication. This includes some minor adjustments to NetworkList in order to adjust to this new order of operations while also preserving the legacy approach. This also includes some adjustments to NetworkBehaviourUpdater where it can force a send (flush) any pending dirty fields when changing ownership. This includes some minor modifications to NetworkObject.PostNetworkVariableWrite. * test Just some updates to two integration tests. These are all primarily for debugging purposes. * update Adding last change log entry for this PR. * style Updated a changelog entry to make it clearer. * fix DAHost fixes: Fixed issue where it was possible to ignore forwarding a change in ownership message to the destination owner if the original owner changed ownership. Fixed issue where it was possible to re-send a change in ownership message back to the original owner if the original owner sent the message. * test Adding and additional test that validates several of the fixes in this PR. * test fix Removing the DAHost testfixture that wasn't supposed to be added. Fixing an issue with the NetworkObjectOwnershipTests when running in DAHost mode. It was passing because the DAHost was sending the ChangeOwnershipMessage back to the owner that changed ownership to another client (without the fix for that in this PR it would send the message to the authority/owner which is why the test was passing). * Update com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NetworkVariableDeltaMessage.cs Co-authored-by: Dominick <[email protected]> * style Updating all instances of k_ServerDeltaForwadingAndNetworkDelivery and replacing with k_ServerDeltaForwardingAndNetworkDelivery --------- Co-authored-by: Dominick <[email protected]>
1 parent 6b459a1 commit 632bf35

File tree

14 files changed

+1608
-185
lines changed

14 files changed

+1608
-185
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ Additional documentation and release notes are available at [Multiplayer Documen
2222
- Fixed issue with nested `NetworkTransform` components clearing their initial prefab settings when in owner authoritative mode on the server side while using a client-server network topology which resulted in improper synchronization of the nested `NetworkTransform` components. (#3099)
2323
- Fixed issue with service not getting synchronized with in-scene placed `NetworkObject` instances when a session owner starts a `SceneEventType.Load` event. (#3096)
2424
- Fixed issue with the in-scene network prefab instance update menu tool where it was not properly updating scenes when invoked on the root prefab instance. (#3092)
25+
- Fixed issue where a newly synchronizing client would be synchronized with the current `NetworkVariable` values always which could cause issues with collections if there were any pending state updates. Now, when initially synchronizing a client, if a `NetworkVariable` has a pending state update it will serialize the previously known value(s) to the synchronizing client so when the pending updates are sent they aren't duplicate values on the newly connected client side. (#3081)
26+
- Fixed issue where changing ownership would mark every `NetworkVariable` dirty. Now, it will only mark any `NetworkVariable` with owner read permissions as dirty and will send/flush any pending updates to all clients prior to sending the change in ownership message. (#3081)
27+
- Fixed issue with `NetworkVariable` collections where transferring ownership to another client would not update the new owner's previous value to the most current value which could cause the last/previous added value to be detected as a change when adding or removing an entry (as long as the entry removed was not the last/previously added value). (#3081)
28+
- Fixed issue where a client (or server) with no write permissions for a `NetworkVariable` using a standard .NET collection type could still modify the collection which could cause various issues depending upon the modification and collection type. (#3081)
2529
- Fixed issue where applying the position and/or rotation to the `NetworkManager.ConnectionApprovalResponse` when connection approval and auto-spawn player prefab were enabled would not apply the position and/or rotation when the player prefab was instantiated. (#3078)
2630
- Fixed issue where `NetworkObject.SpawnWithObservers` was not being honored when spawning the player prefab. (#3077)
2731
- Fixed issue with the client count not being correct on the host or server side when a client disconnects itself from a session. (#3075)
2832

2933
### Changed
3034

3135
- Changed `NetworkConfig.AutoSpawnPlayerPrefabClientSide` is no longer automatically set when starting `NetworkManager`. (#3097)
36+
- Changed `NetworkVariableDeltaMessage` so the server now forwards delta state updates (owner write permission based from a client) to other clients immediately as opposed to keeping a `NetworkVariable` or `NetworkList` dirty and processing them at the end of the frame or potentially on the next network tick. (#3081)
37+
3238

3339
## [2.0.0] - 2024-09-12
3440

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

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,13 @@ public virtual void OnGainedOwnership() { }
826826
internal void InternalOnGainedOwnership()
827827
{
828828
UpdateNetworkProperties();
829+
// New owners need to assure any NetworkVariables they have write permissions
830+
// to are updated so the previous and original values are aligned with the
831+
// current value (primarily for collections).
832+
if (OwnerClientId == NetworkManager.LocalClientId)
833+
{
834+
UpdateNetworkVariableOnOwnershipChanged();
835+
}
829836
OnGainedOwnership();
830837
}
831838

@@ -1016,9 +1023,14 @@ internal void PreVariableUpdate()
10161023
internal readonly List<int> NetworkVariableIndexesToReset = new List<int>();
10171024
internal readonly HashSet<int> NetworkVariableIndexesToResetSet = new HashSet<int>();
10181025

1019-
internal void NetworkVariableUpdate(ulong targetClientId)
1026+
/// <summary>
1027+
/// Determines if a NetworkVariable should have any changes to state sent out
1028+
/// </summary>
1029+
/// <param name="targetClientId">target to send the updates to</param>
1030+
/// <param name="forceSend">specific to change in ownership</param>
1031+
internal void NetworkVariableUpdate(ulong targetClientId, bool forceSend = false)
10201032
{
1021-
if (!CouldHaveDirtyNetworkVariables())
1033+
if (!forceSend && !CouldHaveDirtyNetworkVariables())
10221034
{
10231035
return;
10241036
}
@@ -1069,7 +1081,11 @@ internal void NetworkVariableUpdate(ulong targetClientId)
10691081
NetworkBehaviourIndex = behaviourIndex,
10701082
NetworkBehaviour = this,
10711083
TargetClientId = targetClientId,
1072-
DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j]
1084+
DeliveryMappedNetworkVariableIndex = m_DeliveryMappedNetworkVariableIndices[j],
1085+
// By sending the network delivery we can forward messages immediately as opposed to processing them
1086+
// at the end. While this will send updates to clients that cannot read, the handler will ignore anything
1087+
// sent to a client that does not have read permissions.
1088+
NetworkDelivery = m_DeliveryTypesForNetworkVariableGroups[j]
10731089
};
10741090
// TODO: Serialization is where the IsDirty flag gets changed.
10751091
// Messages don't get sent from the server to itself, so if we're host and sending to ourselves,
@@ -1114,6 +1130,26 @@ private bool CouldHaveDirtyNetworkVariables()
11141130
return false;
11151131
}
11161132

1133+
/// <summary>
1134+
/// Invoked on a new client to assure the previous and original values
1135+
/// are synchronized with the current known value.
1136+
/// </summary>
1137+
/// <remarks>
1138+
/// Primarily for collections to assure the previous value(s) is/are the
1139+
/// same as the current value(s) in order to not re-send already known entries.
1140+
/// </remarks>
1141+
internal void UpdateNetworkVariableOnOwnershipChanged()
1142+
{
1143+
for (int j = 0; j < NetworkVariableFields.Count; j++)
1144+
{
1145+
// Only invoke OnInitialize on NetworkVariables the owner can write to
1146+
if (NetworkVariableFields[j].CanClientWrite(OwnerClientId))
1147+
{
1148+
NetworkVariableFields[j].OnInitialize();
1149+
}
1150+
}
1151+
}
1152+
11171153
internal void MarkVariablesDirty(bool dirty)
11181154
{
11191155
for (int j = 0; j < NetworkVariableFields.Count; j++)
@@ -1122,6 +1158,17 @@ internal void MarkVariablesDirty(bool dirty)
11221158
}
11231159
}
11241160

1161+
internal void MarkOwnerReadVariablesDirty()
1162+
{
1163+
for (int j = 0; j < NetworkVariableFields.Count; j++)
1164+
{
1165+
if (NetworkVariableFields[j].ReadPerm == NetworkVariableReadPermission.Owner)
1166+
{
1167+
NetworkVariableFields[j].SetDirty(true);
1168+
}
1169+
}
1170+
}
1171+
11251172
/// <summary>
11261173
/// Synchronizes by setting only the NetworkVariable field values that the client has permission to read.
11271174
/// Note: This is only invoked when first synchronizing a NetworkBehaviour (i.e. late join or spawned NetworkObject)
@@ -1172,17 +1219,24 @@ internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClie
11721219
// The way we do packing, any value > 63 in a ushort will use the full 2 bytes to represent.
11731220
writer.WriteValueSafe((ushort)0);
11741221
var startPos = writer.Position;
1175-
NetworkVariableFields[j].WriteField(writer);
1222+
// Write the NetworkVariable field value
1223+
// WriteFieldSynchronization will write the current value only if there are no pending changes.
1224+
// Otherwise, it will write the previous value if there are pending changes since the pending
1225+
// changes will be sent shortly after the client's synchronization.
1226+
NetworkVariableFields[j].WriteFieldSynchronization(writer);
11761227
var size = writer.Position - startPos;
11771228
writer.Seek(writePos);
1178-
// Write the NetworkVariable value
1229+
// Write the NetworkVariable field value size
11791230
writer.WriteValueSafe((ushort)size);
11801231
writer.Seek(startPos + size);
11811232
}
11821233
else // Client-Server Only: Should only ever be invoked when using a client-server NetworkTopology
11831234
{
1184-
// Write the NetworkVariable value
1185-
NetworkVariableFields[j].WriteField(writer);
1235+
// Write the NetworkVariable field value
1236+
// WriteFieldSynchronization will write the current value only if there are no pending changes.
1237+
// Otherwise, it will write the previous value if there are pending changes since the pending
1238+
// changes will be sent shortly after the client's synchronization.
1239+
NetworkVariableFields[j].WriteFieldSynchronization(writer);
11861240
}
11871241
}
11881242
else if (ensureLengthSafety)

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ public class NetworkBehaviourUpdater
1919

2020
internal void AddForUpdate(NetworkObject networkObject)
2121
{
22+
// Since this is a HashSet, we don't need to worry about duplicate entries
2223
m_PendingDirtyNetworkObjects.Add(networkObject);
2324
}
2425

25-
internal void NetworkBehaviourUpdate()
26+
/// <summary>
27+
/// Sends NetworkVariable deltas
28+
/// </summary>
29+
/// <param name="forceSend">internal only, when changing ownership we want to send this before the change in ownership message</param>
30+
internal void NetworkBehaviourUpdate(bool forceSend = false)
2631
{
2732
#if DEVELOPMENT_BUILD || UNITY_EDITOR
2833
m_NetworkBehaviourUpdate.Begin();
@@ -53,7 +58,7 @@ internal void NetworkBehaviourUpdate()
5358
// Sync just the variables for just the objects this client sees
5459
for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++)
5560
{
56-
dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId);
61+
dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId, forceSend);
5762
}
5863
}
5964
}
@@ -72,7 +77,7 @@ internal void NetworkBehaviourUpdate()
7277
}
7378
for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++)
7479
{
75-
sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId);
80+
sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId, forceSend);
7681
}
7782
}
7883
}
@@ -85,19 +90,24 @@ internal void NetworkBehaviourUpdate()
8590
var behaviour = dirtyObj.ChildNetworkBehaviours[k];
8691
for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++)
8792
{
93+
// Set to true for NetworkVariable to ignore duplication of the
94+
// "internal original value" for collections support.
95+
behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = true;
8896
if (behaviour.NetworkVariableFields[i].IsDirty() &&
8997
!behaviour.NetworkVariableIndexesToResetSet.Contains(i))
9098
{
9199
behaviour.NetworkVariableIndexesToResetSet.Add(i);
92100
behaviour.NetworkVariableIndexesToReset.Add(i);
93101
}
102+
// Reset back to false when done
103+
behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = false;
94104
}
95105
}
96106
}
97107
// Now, reset all the no-longer-dirty variables
98108
foreach (var dirtyobj in m_DirtyNetworkObjects)
99109
{
100-
dirtyobj.PostNetworkVariableWrite();
110+
dirtyobj.PostNetworkVariableWrite(forceSend);
101111
// Once done processing, we set the previous owner id to the current owner id
102112
dirtyobj.PreviousOwnerId = dirtyobj.OwnerClientId;
103113
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,14 @@ internal void MarkVariablesDirty(bool dirty)
24452445
}
24462446
}
24472447

2448+
internal void MarkOwnerReadVariablesDirty()
2449+
{
2450+
for (int i = 0; i < ChildNetworkBehaviours.Count; i++)
2451+
{
2452+
ChildNetworkBehaviours[i].MarkOwnerReadVariablesDirty();
2453+
}
2454+
}
2455+
24482456
// NGO currently guarantees that the client will receive spawn data for all objects in one network tick.
24492457
// Children may arrive before their parents; when they do they are stored in OrphanedChildren and then
24502458
// resolved when their parents arrived. Because we don't send a partial list of spawns (yet), something
@@ -2771,11 +2779,11 @@ public void Deserialize(FastBufferReader reader)
27712779
}
27722780
}
27732781

2774-
internal void PostNetworkVariableWrite()
2782+
internal void PostNetworkVariableWrite(bool forced = false)
27752783
{
27762784
for (int k = 0; k < ChildNetworkBehaviours.Count; k++)
27772785
{
2778-
ChildNetworkBehaviours[k].PostNetworkVariableWrite();
2786+
ChildNetworkBehaviours[k].PostNetworkVariableWrite(forced);
27792787
}
27802788
}
27812789

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ internal struct ChangeOwnershipMessage : INetworkMessage, INetworkSerializeByMem
99

1010
public ulong NetworkObjectId;
1111
public ulong OwnerClientId;
12-
// DANGOEXP TODO: Remove these notes or change their format
1312
// SERVICE NOTES:
1413
// When forwarding the message to clients on the CMB Service side,
1514
// you can set the ClientIdCount to 0 and skip writing the ClientIds.
@@ -258,15 +257,18 @@ public void Handle(ref NetworkContext context)
258257
continue;
259258
}
260259

261-
// If ownership is changing and this is not an ownership request approval then ignore the OnwerClientId
260+
// If ownership is changing and this is not an ownership request approval then ignore the SenderId
261+
if (OwnershipIsChanging && !RequestApproved && context.SenderId == clientId)
262+
{
263+
continue;
264+
}
265+
262266
// If it is just updating flags then ignore sending to the owner
263267
// If it is a request or approving request, then ignore the RequestClientId
264-
if ((OwnershipIsChanging && !RequestApproved && OwnerClientId == clientId) || (OwnershipFlagsUpdate && clientId == OwnerClientId)
265-
|| ((RequestOwnership || RequestApproved) && clientId == RequestClientId))
268+
if ((OwnershipFlagsUpdate && clientId == OwnerClientId) || ((RequestOwnership || RequestApproved) && clientId == RequestClientId))
266269
{
267270
continue;
268271
}
269-
270272
networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, clientId);
271273
}
272274
}
@@ -327,10 +329,12 @@ private void HandleOwnershipChange(ref NetworkContext context)
327329
var networkManager = (NetworkManager)context.SystemOwner;
328330
var networkObject = networkManager.SpawnManager.SpawnedObjects[NetworkObjectId];
329331

330-
// DANGO-TODO: This probably shouldn't be allowed to happen.
332+
// Sanity check that we are not sending duplicated change ownership messages
331333
if (networkObject.OwnerClientId == OwnerClientId)
332334
{
333-
UnityEngine.Debug.LogWarning($"Unnecessary ownership changed message for {NetworkObjectId}");
335+
UnityEngine.Debug.LogError($"Unnecessary ownership changed message for {NetworkObjectId}.");
336+
// Ignore the message
337+
return;
334338
}
335339

336340
var originalOwner = networkObject.OwnerClientId;
@@ -347,12 +351,6 @@ private void HandleOwnershipChange(ref NetworkContext context)
347351
networkObject.InvokeBehaviourOnLostOwnership();
348352
}
349353

350-
// We are new owner or (client-server) or running in distributed authority mode
351-
if (OwnerClientId == networkManager.LocalClientId || networkManager.DistributedAuthorityMode)
352-
{
353-
networkObject.InvokeBehaviourOnGainedOwnership();
354-
}
355-
356354
// If in distributed authority mode
357355
if (networkManager.DistributedAuthorityMode)
358356
{
@@ -374,6 +372,22 @@ private void HandleOwnershipChange(ref NetworkContext context)
374372
}
375373
}
376374

375+
// We are new owner or (client-server) or running in distributed authority mode
376+
if (OwnerClientId == networkManager.LocalClientId || networkManager.DistributedAuthorityMode)
377+
{
378+
networkObject.InvokeBehaviourOnGainedOwnership();
379+
}
380+
381+
382+
if (originalOwner == networkManager.LocalClientId && !networkManager.DistributedAuthorityMode)
383+
{
384+
// Mark any owner read variables as dirty
385+
networkObject.MarkOwnerReadVariablesDirty();
386+
// Immediately queue any pending deltas and order the message before the
387+
// change in ownership message.
388+
networkManager.BehaviourUpdater.NetworkBehaviourUpdate(true);
389+
}
390+
377391
// Always invoke ownership change notifications
378392
networkObject.InvokeOwnershipChanged(originalOwner, OwnerClientId);
379393

0 commit comments

Comments
 (0)