Skip to content

Commit e242f1b

Browse files
authored
feat: message versioning [MTT-3048] (#2290)
Adds support for SDKs at different versions with different message formats to be able to talk to each other - the side with the higher version being responsible for both converting up when receiving and converting down when sending.
1 parent f1870cd commit e242f1b

32 files changed

+990
-257
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
99
## [Unreleased]
1010

1111
### Added
12-
12+
- Added support for different versions of the SDK to talk to each other in circumstances where changes permit it. Starting with this version and into future versions, patch versions should be compatible as long as the minor version is the same. (#2290)
1313
- Added `NetworkObject` auto-add helper and Multiplayer Tools install reminder settings to Project Settings. (#2285)
1414
- Added `public string DisconnectReason` getter to `NetworkManager` and `string Reason` to `ConnectionApprovalResponse`. Allows connection approval to communicate back a reason. Also added `public void DisconnectClient(ulong clientId, string reason)` allowing setting a disconnection reason, when explicitly disconnecting a client.
1515

com.unity.netcode.gameobjects/Editor/CodeGen/INetworkMessageILPP.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,19 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
102102
private PostProcessorAssemblyResolver m_AssemblyResolver;
103103

104104
private MethodReference m_MessagingSystem_ReceiveMessage_MethodRef;
105+
private MethodReference m_MessagingSystem_CreateMessageAndGetVersion_MethodRef;
105106
private TypeReference m_MessagingSystem_MessageWithHandler_TypeRef;
106107
private MethodReference m_MessagingSystem_MessageHandler_Constructor_TypeRef;
108+
private MethodReference m_MessagingSystem_VersionGetter_Constructor_TypeRef;
107109
private FieldReference m_ILPPMessageProvider___network_message_types_FieldRef;
108110
private FieldReference m_MessagingSystem_MessageWithHandler_MessageType_FieldRef;
109111
private FieldReference m_MessagingSystem_MessageWithHandler_Handler_FieldRef;
112+
private FieldReference m_MessagingSystem_MessageWithHandler_GetVersion_FieldRef;
110113
private MethodReference m_Type_GetTypeFromHandle_MethodRef;
111114
private MethodReference m_List_Add_MethodRef;
112115

113116
private const string k_ReceiveMessageName = nameof(MessagingSystem.ReceiveMessage);
117+
private const string k_CreateMessageAndGetVersionName = nameof(MessagingSystem.CreateMessageAndGetVersion);
114118

115119
private bool ImportReferences(ModuleDefinition moduleDefinition)
116120
{
@@ -126,6 +130,7 @@ private bool ImportReferences(ModuleDefinition moduleDefinition)
126130
TypeDefinition listTypeDef = moduleDefinition.ImportReference(typeof(List<>)).Resolve();
127131

128132
TypeDefinition messageHandlerTypeDef = null;
133+
TypeDefinition versionGetterTypeDef = null;
129134
TypeDefinition messageWithHandlerTypeDef = null;
130135
TypeDefinition ilppMessageProviderTypeDef = null;
131136
TypeDefinition messagingSystemTypeDef = null;
@@ -137,6 +142,12 @@ private bool ImportReferences(ModuleDefinition moduleDefinition)
137142
continue;
138143
}
139144

145+
if (versionGetterTypeDef == null && netcodeTypeDef.Name == nameof(MessagingSystem.VersionGetter))
146+
{
147+
versionGetterTypeDef = netcodeTypeDef;
148+
continue;
149+
}
150+
140151
if (messageWithHandlerTypeDef == null && netcodeTypeDef.Name == nameof(MessagingSystem.MessageWithHandler))
141152
{
142153
messageWithHandlerTypeDef = netcodeTypeDef;
@@ -157,6 +168,7 @@ private bool ImportReferences(ModuleDefinition moduleDefinition)
157168
}
158169

159170
m_MessagingSystem_MessageHandler_Constructor_TypeRef = moduleDefinition.ImportReference(messageHandlerTypeDef.GetConstructors().First());
171+
m_MessagingSystem_VersionGetter_Constructor_TypeRef = moduleDefinition.ImportReference(versionGetterTypeDef.GetConstructors().First());
160172

161173
m_MessagingSystem_MessageWithHandler_TypeRef = moduleDefinition.ImportReference(messageWithHandlerTypeDef);
162174
foreach (var fieldDef in messageWithHandlerTypeDef.Fields)
@@ -169,6 +181,9 @@ private bool ImportReferences(ModuleDefinition moduleDefinition)
169181
case nameof(MessagingSystem.MessageWithHandler.Handler):
170182
m_MessagingSystem_MessageWithHandler_Handler_FieldRef = moduleDefinition.ImportReference(fieldDef);
171183
break;
184+
case nameof(MessagingSystem.MessageWithHandler.GetVersion):
185+
m_MessagingSystem_MessageWithHandler_GetVersion_FieldRef = moduleDefinition.ImportReference(fieldDef);
186+
break;
172187
}
173188
}
174189

@@ -211,6 +226,9 @@ private bool ImportReferences(ModuleDefinition moduleDefinition)
211226
case k_ReceiveMessageName:
212227
m_MessagingSystem_ReceiveMessage_MethodRef = moduleDefinition.ImportReference(methodDef);
213228
break;
229+
case k_CreateMessageAndGetVersionName:
230+
m_MessagingSystem_CreateMessageAndGetVersion_MethodRef = moduleDefinition.ImportReference(methodDef);
231+
break;
214232
}
215233
}
216234

@@ -236,7 +254,7 @@ private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinit
236254
return staticCtorMethodDef;
237255
}
238256

239-
private void CreateInstructionsToRegisterType(ILProcessor processor, List<Instruction> instructions, TypeReference type, MethodReference receiveMethod)
257+
private void CreateInstructionsToRegisterType(ILProcessor processor, List<Instruction> instructions, TypeReference type, MethodReference receiveMethod, MethodReference versionMethod)
240258
{
241259
// MessagingSystem.__network_message_types.Add(new MessagingSystem.MessageWithHandler{MessageType=typeof(type), Handler=type.Receive});
242260
processor.Body.Variables.Add(new VariableDefinition(m_MessagingSystem_MessageWithHandler_TypeRef));
@@ -252,14 +270,23 @@ private void CreateInstructionsToRegisterType(ILProcessor processor, List<Instru
252270
instructions.Add(processor.Create(OpCodes.Call, m_Type_GetTypeFromHandle_MethodRef));
253271
instructions.Add(processor.Create(OpCodes.Stfld, m_MessagingSystem_MessageWithHandler_MessageType_FieldRef));
254272

255-
// tmp.Handler = type.Receive
273+
// tmp.Handler = MessageHandler.ReceveMessage<type>
256274
instructions.Add(processor.Create(OpCodes.Ldloca, messageWithHandlerLocIdx));
257275
instructions.Add(processor.Create(OpCodes.Ldnull));
258276

259277
instructions.Add(processor.Create(OpCodes.Ldftn, receiveMethod));
260278
instructions.Add(processor.Create(OpCodes.Newobj, m_MessagingSystem_MessageHandler_Constructor_TypeRef));
261279
instructions.Add(processor.Create(OpCodes.Stfld, m_MessagingSystem_MessageWithHandler_Handler_FieldRef));
262280

281+
282+
// tmp.GetVersion = MessageHandler.CreateMessageAndGetVersion<type>
283+
instructions.Add(processor.Create(OpCodes.Ldloca, messageWithHandlerLocIdx));
284+
instructions.Add(processor.Create(OpCodes.Ldnull));
285+
286+
instructions.Add(processor.Create(OpCodes.Ldftn, versionMethod));
287+
instructions.Add(processor.Create(OpCodes.Newobj, m_MessagingSystem_VersionGetter_Constructor_TypeRef));
288+
instructions.Add(processor.Create(OpCodes.Stfld, m_MessagingSystem_MessageWithHandler_GetVersion_FieldRef));
289+
263290
// ILPPMessageProvider.__network_message_types.Add(tmp);
264291
instructions.Add(processor.Create(OpCodes.Ldloc, messageWithHandlerLocIdx));
265292
instructions.Add(processor.Create(OpCodes.Callvirt, m_List_Add_MethodRef));
@@ -285,7 +312,9 @@ private void CreateModuleInitializer(AssemblyDefinition assembly, List<TypeDefin
285312
{
286313
var receiveMethod = new GenericInstanceMethod(m_MessagingSystem_ReceiveMessage_MethodRef);
287314
receiveMethod.GenericArguments.Add(type);
288-
CreateInstructionsToRegisterType(processor, instructions, type, receiveMethod);
315+
var versionMethod = new GenericInstanceMethod(m_MessagingSystem_CreateMessageAndGetVersion_MethodRef);
316+
versionMethod.GenericArguments.Add(type);
317+
CreateInstructionsToRegisterType(processor, instructions, type, receiveMethod, versionMethod);
289318
}
290319

291320
instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ private void NetworkVariableUpdate(ulong targetClientId, int behaviourIndex)
730730
var tmpWriter = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, MessagingSystem.FRAGMENTED_MESSAGE_MAX_SIZE);
731731
using (tmpWriter)
732732
{
733-
message.Serialize(tmpWriter);
733+
message.Serialize(tmpWriter, message.Version);
734734
}
735735
}
736736
else

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,7 +1440,7 @@ internal void ShutdownInternal()
14401440
}
14411441
}
14421442

1443-
if (IsClient && IsConnectedClient)
1443+
if (IsClient && IsListening)
14441444
{
14451445
// Client only, send disconnect to server
14461446
NetworkConfig.NetworkTransport.DisconnectLocalClient();
@@ -1598,6 +1598,7 @@ private void OnNetworkEarlyUpdate()
15981598
} while (IsListening && networkEvent != NetworkEvent.Nothing);
15991599

16001600
MessagingSystem.ProcessIncomingMessageQueue();
1601+
MessagingSystem.CleanupDisconnectedClients();
16011602

16021603
#if DEVELOPMENT_BUILD || UNITY_EDITOR
16031604
s_TransportPoll.End();
@@ -1679,7 +1680,23 @@ private void SendConnectionRequest()
16791680
ShouldSendConnectionData = NetworkConfig.ConnectionApproval,
16801681
ConnectionData = NetworkConfig.ConnectionData
16811682
};
1683+
1684+
message.MessageVersions = new NativeArray<MessageVersionData>(MessagingSystem.MessageHandlers.Length, Allocator.Temp);
1685+
for (int index = 0; index < MessagingSystem.MessageHandlers.Length; index++)
1686+
{
1687+
if (MessagingSystem.MessageTypes[index] != null)
1688+
{
1689+
var type = MessagingSystem.MessageTypes[index];
1690+
message.MessageVersions[index] = new MessageVersionData
1691+
{
1692+
Hash = XXHash.Hash32(type.FullName),
1693+
Version = MessagingSystem.GetLocalVersion(type)
1694+
};
1695+
}
1696+
}
1697+
16821698
SendMessage(ref message, NetworkDelivery.ReliableSequenced, ServerClientId);
1699+
message.MessageVersions.Dispose();
16831700
}
16841701

16851702
private IEnumerator ApprovalTimeout(ulong clientId)
@@ -2230,22 +2247,23 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe
22302247
}
22312248
}
22322249

2233-
SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId);
2234-
2250+
message.MessageVersions = new NativeArray<MessageVersionData>(MessagingSystem.MessageHandlers.Length, Allocator.Temp);
22352251
for (int index = 0; index < MessagingSystem.MessageHandlers.Length; index++)
22362252
{
22372253
if (MessagingSystem.MessageTypes[index] != null)
22382254
{
2239-
var orderingMessage = new OrderingMessage
2255+
var type = MessagingSystem.MessageTypes[index];
2256+
message.MessageVersions[index] = new MessageVersionData
22402257
{
2241-
Order = index,
2242-
Hash = XXHash.Hash32(MessagingSystem.MessageTypes[index].FullName)
2258+
Hash = XXHash.Hash32(type.FullName),
2259+
Version = MessagingSystem.GetLocalVersion(type)
22432260
};
2244-
2245-
SendMessage(ref orderingMessage, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId);
22462261
}
22472262
}
22482263

2264+
SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId);
2265+
message.MessageVersions.Dispose();
2266+
22492267
// If scene management is enabled, then let NetworkSceneManager handle the initial scene and NetworkObject synchronization
22502268
if (!NetworkConfig.EnableSceneManagement)
22512269
{

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,26 @@ internal struct DisconnectReasonMessage : INetworkMessage
44
{
55
public string Reason;
66

7-
public void Serialize(FastBufferWriter writer)
7+
public int Version => 0;
8+
9+
public void Serialize(FastBufferWriter writer, int targetVersion)
810
{
911
string reasonSent = Reason;
1012
if (reasonSent == null)
1113
{
1214
reasonSent = string.Empty;
1315
}
1416

17+
// Since we don't send a ConnectionApprovedMessage, the version for this message is encded with the message
18+
// itself. However, note that we HAVE received a ConnectionRequestMessage, so we DO have a valid targetVersion
19+
// on this side of things - we just have to make sure the receiving side knows what version we sent it,
20+
// since whoever has the higher version number is responsible for versioning and they may be the one
21+
// with the higher version number.
22+
BytePacker.WriteValueBitPacked(writer, Version);
23+
1524
if (writer.TryBeginWrite(FastBufferWriter.GetWriteSize(reasonSent)))
1625
{
17-
writer.WriteValueSafe(reasonSent);
26+
writer.WriteValue(reasonSent);
1827
}
1928
else
2029
{
@@ -24,8 +33,11 @@ public void Serialize(FastBufferWriter writer)
2433
}
2534
}
2635

27-
public bool Deserialize(FastBufferReader reader, ref NetworkContext context)
36+
public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion)
2837
{
38+
// Since we don't get a ConnectionApprovedMessage, the version for this message is encded with the message
39+
// itself. This will override what we got from MessagingSystem... which will always be 0 here.
40+
ByteUnpacker.ReadValueBitPacked(reader, out receivedMessageVersion);
2941
reader.ReadValueSafe(out Reason);
3042
return true;
3143
}

com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ namespace Unity.Netcode
4040
/// </summary>
4141
internal interface INetworkMessage
4242
{
43-
void Serialize(FastBufferWriter writer);
44-
bool Deserialize(FastBufferReader reader, ref NetworkContext context);
43+
void Serialize(FastBufferWriter writer, int targetVersion);
44+
bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion);
4545
void Handle(ref NetworkContext context);
46+
int Version { get; }
4647
}
4748
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@ namespace Unity.Netcode
22
{
33
internal struct ChangeOwnershipMessage : INetworkMessage, INetworkSerializeByMemcpy
44
{
5+
public int Version => 0;
6+
57
public ulong NetworkObjectId;
68
public ulong OwnerClientId;
79

8-
public void Serialize(FastBufferWriter writer)
10+
public void Serialize(FastBufferWriter writer, int targetVersion)
911
{
1012
BytePacker.WriteValueBitPacked(writer, NetworkObjectId);
1113
BytePacker.WriteValueBitPacked(writer, OwnerClientId);
1214
}
1315

14-
public bool Deserialize(FastBufferReader reader, ref NetworkContext context)
16+
public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion)
1517
{
1618
var networkManager = (NetworkManager)context.SystemOwner;
1719
if (!networkManager.IsClient)

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
using System;
21
using System.Collections.Generic;
2+
using Unity.Collections;
33

44
namespace Unity.Netcode
55
{
66
internal struct ConnectionApprovedMessage : INetworkMessage
77
{
8+
public int Version => 0;
9+
810
public ulong OwnerClientId;
911
public int NetworkTick;
1012

@@ -13,12 +15,24 @@ internal struct ConnectionApprovedMessage : INetworkMessage
1315

1416
private FastBufferReader m_ReceivedSceneObjectData;
1517

16-
public void Serialize(FastBufferWriter writer)
18+
public NativeArray<MessageVersionData> MessageVersions;
19+
20+
public void Serialize(FastBufferWriter writer, int targetVersion)
1721
{
18-
if (!writer.TryBeginWrite(sizeof(ulong) + sizeof(int) + sizeof(int)))
22+
// ============================================================
23+
// BEGIN FORBIDDEN SEGMENT
24+
// DO NOT CHANGE THIS HEADER. Everything added to this message
25+
// must go AFTER the message version header.
26+
// ============================================================
27+
BytePacker.WriteValueBitPacked(writer, MessageVersions.Length);
28+
foreach (var messageVersion in MessageVersions)
1929
{
20-
throw new OverflowException($"Not enough space in the write buffer to serialize {nameof(ConnectionApprovedMessage)}");
30+
messageVersion.Serialize(writer);
2131
}
32+
// ============================================================
33+
// END FORBIDDEN SEGMENT
34+
// ============================================================
35+
2236
BytePacker.WriteValueBitPacked(writer, OwnerClientId);
2337
BytePacker.WriteValueBitPacked(writer, NetworkTick);
2438

@@ -51,14 +65,42 @@ public void Serialize(FastBufferWriter writer)
5165
}
5266
}
5367

54-
public bool Deserialize(FastBufferReader reader, ref NetworkContext context)
68+
public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int receivedMessageVersion)
5569
{
5670
var networkManager = (NetworkManager)context.SystemOwner;
5771
if (!networkManager.IsClient)
5872
{
5973
return false;
6074
}
6175

76+
// ============================================================
77+
// BEGIN FORBIDDEN SEGMENT
78+
// DO NOT CHANGE THIS HEADER. Everything added to this message
79+
// must go AFTER the message version header.
80+
// ============================================================
81+
ByteUnpacker.ReadValueBitPacked(reader, out int length);
82+
var messageHashesInOrder = new NativeArray<uint>(length, Allocator.Temp);
83+
for (var i = 0; i < length; ++i)
84+
{
85+
var messageVersion = new MessageVersionData();
86+
messageVersion.Deserialize(reader);
87+
networkManager.MessagingSystem.SetVersion(context.SenderId, messageVersion.Hash, messageVersion.Version);
88+
messageHashesInOrder[i] = messageVersion.Hash;
89+
90+
// Update the received version since this message will always be passed version 0, due to the map not
91+
// being initialized until just now.
92+
var messageType = networkManager.MessagingSystem.GetMessageForHash(messageVersion.Hash);
93+
if (messageType == typeof(ConnectionApprovedMessage))
94+
{
95+
receivedMessageVersion = messageVersion.Version;
96+
}
97+
}
98+
networkManager.MessagingSystem.SetServerMessageOrder(messageHashesInOrder);
99+
messageHashesInOrder.Dispose();
100+
// ============================================================
101+
// END FORBIDDEN SEGMENT
102+
// ============================================================
103+
62104
ByteUnpacker.ReadValueBitPacked(reader, out OwnerClientId);
63105
ByteUnpacker.ReadValueBitPacked(reader, out NetworkTick);
64106
m_ReceivedSceneObjectData = reader;

0 commit comments

Comments
 (0)