Skip to content

Commit 421a364

Browse files
authored
feat: host tells client which IDs it uses for INetworkMessages (#2103)
Server sends client reordering messages when host and client have different sets to ensure proper handler mappings.
1 parent 8bac8aa commit 421a364

File tree

6 files changed

+369
-12
lines changed

6 files changed

+369
-12
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,6 +2065,20 @@ internal void HandleConnectionApproval(ulong ownerClientId, ConnectionApprovalRe
20652065

20662066
SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId);
20672067

2068+
for (int index = 0; index < MessagingSystem.MessageHandlers.Length; index++)
2069+
{
2070+
if (MessagingSystem.MessageTypes[index] != null)
2071+
{
2072+
var orderingMessage = new OrderingMessage
2073+
{
2074+
Order = index,
2075+
Hash = XXHash.Hash32(MessagingSystem.MessageTypes[index].FullName)
2076+
};
2077+
2078+
SendMessage(ref orderingMessage, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId);
2079+
}
2080+
}
2081+
20682082
// If scene management is enabled, then let NetworkSceneManager handle the initial scene and NetworkObject synchronization
20692083
if (!NetworkConfig.EnableSceneManagement)
20702084
{
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
3+
namespace Unity.Netcode
4+
{
5+
/// <summary>
6+
/// Upon connecting, the host sends a series of OrderingMessage to the client so that it can make sure both sides
7+
/// have the same message types in the same positions in
8+
/// - MessagingSystem.m_MessageHandlers
9+
/// - MessagingSystem.m_ReverseTypeMap
10+
/// even if one side has extra messages (compilation, version, patch, or platform differences, etc...)
11+
///
12+
/// The ConnectionRequestedMessage, ConnectionApprovedMessage and OrderingMessage are prioritized at the beginning
13+
/// of the mapping, to guarantee they can be exchanged before the two sides share their ordering
14+
/// The sorting used in also stable so that even if MessageType names share hashes, it will work most of the time
15+
/// </summary>
16+
internal struct OrderingMessage : INetworkMessage
17+
{
18+
public int Order;
19+
public uint Hash;
20+
21+
public void Serialize(FastBufferWriter writer)
22+
{
23+
if (!writer.TryBeginWrite(FastBufferWriter.GetWriteSize(Order) + FastBufferWriter.GetWriteSize(Hash)))
24+
{
25+
throw new OverflowException($"Not enough space in the buffer to write {nameof(OrderingMessage)}");
26+
}
27+
28+
writer.WriteValue(Order);
29+
writer.WriteValue(Hash);
30+
}
31+
32+
public bool Deserialize(FastBufferReader reader, ref NetworkContext context)
33+
{
34+
if (!reader.TryBeginRead(FastBufferWriter.GetWriteSize(Order) + FastBufferWriter.GetWriteSize(Hash)))
35+
{
36+
throw new OverflowException($"Not enough data in the buffer to read {nameof(OrderingMessage)}");
37+
}
38+
39+
reader.ReadValue(out Order);
40+
reader.ReadValue(out Hash);
41+
42+
return true;
43+
}
44+
45+
public void Handle(ref NetworkContext context)
46+
{
47+
((NetworkManager)context.SystemOwner).MessagingSystem.ReorderMessage(Order, Hash);
48+
}
49+
}
50+
}

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/OrderingMessage.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 130 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88

99
namespace Unity.Netcode
1010
{
11+
internal class HandlerNotRegisteredException : SystemException
12+
{
13+
public HandlerNotRegisteredException() { }
14+
public HandlerNotRegisteredException(string issue) : base(issue) { }
15+
}
1116

1217
internal class InvalidMessageStructureException : SystemException
1318
{
@@ -44,8 +49,9 @@ public SendQueueItem(NetworkDelivery delivery, int writerSize, Allocator writerA
4449

4550
private NativeList<ReceiveQueueItem> m_IncomingMessageQueue = new NativeList<ReceiveQueueItem>(16, Allocator.Persistent);
4651

47-
private MessageHandler[] m_MessageHandlers = new MessageHandler[255];
48-
private Type[] m_ReverseTypeMap = new Type[255];
52+
// These array will grow as we need more message handlers. 4 is just a starting size.
53+
private MessageHandler[] m_MessageHandlers = new MessageHandler[4];
54+
private Type[] m_ReverseTypeMap = new Type[4];
4955

5056
private Dictionary<Type, uint> m_MessageTypes = new Dictionary<Type, uint>();
5157
private Dictionary<ulong, NativeList<SendQueueItem>> m_SendQueues = new Dictionary<ulong, NativeList<SendQueueItem>>();
@@ -59,6 +65,7 @@ public SendQueueItem(NetworkDelivery delivery, int writerSize, Allocator writerA
5965

6066
internal Type[] MessageTypes => m_ReverseTypeMap;
6167
internal MessageHandler[] MessageHandlers => m_MessageHandlers;
68+
6269
internal uint MessageHandlerCount => m_HighMessageType;
6370

6471
internal uint GetMessageType(Type t)
@@ -75,6 +82,35 @@ internal struct MessageWithHandler
7582
public MessageHandler Handler;
7683
}
7784

85+
internal List<MessageWithHandler> PrioritizeMessageOrder(List<MessageWithHandler> allowedTypes)
86+
{
87+
var prioritizedTypes = new List<MessageWithHandler>();
88+
89+
// first pass puts the priority message in the first indices
90+
// Those are the messages that must be delivered in order to allow re-ordering the others later
91+
foreach (var t in allowedTypes)
92+
{
93+
if (t.MessageType.FullName == "Unity.Netcode.ConnectionRequestMessage" ||
94+
t.MessageType.FullName == "Unity.Netcode.ConnectionApprovedMessage" ||
95+
t.MessageType.FullName == "Unity.Netcode.OrderingMessage")
96+
{
97+
prioritizedTypes.Add(t);
98+
}
99+
}
100+
101+
foreach (var t in allowedTypes)
102+
{
103+
if (t.MessageType.FullName != "Unity.Netcode.ConnectionRequestMessage" &&
104+
t.MessageType.FullName != "Unity.Netcode.ConnectionApprovedMessage" &&
105+
t.MessageType.FullName != "Unity.Netcode.OrderingMessage")
106+
{
107+
prioritizedTypes.Add(t);
108+
}
109+
}
110+
111+
return prioritizedTypes;
112+
}
113+
78114
public MessagingSystem(IMessageSender messageSender, object owner, IMessageProvider provider = null)
79115
{
80116
try
@@ -89,6 +125,7 @@ public MessagingSystem(IMessageSender messageSender, object owner, IMessageProvi
89125
var allowedTypes = provider.GetMessages();
90126

91127
allowedTypes.Sort((a, b) => string.CompareOrdinal(a.MessageType.FullName, b.MessageType.FullName));
128+
allowedTypes = PrioritizeMessageOrder(allowedTypes);
92129
foreach (var type in allowedTypes)
93130
{
94131
RegisterMessageType(type);
@@ -143,6 +180,13 @@ public void Unhook(INetworkHooks hooks)
143180

144181
private void RegisterMessageType(MessageWithHandler messageWithHandler)
145182
{
183+
// if we are out of space, perform amortized linear growth
184+
if (m_HighMessageType == m_MessageHandlers.Length)
185+
{
186+
Array.Resize(ref m_MessageHandlers, 2 * m_MessageHandlers.Length);
187+
Array.Resize(ref m_ReverseTypeMap, 2 * m_ReverseTypeMap.Length);
188+
}
189+
146190
m_MessageHandlers[m_HighMessageType] = messageWithHandler.Handler;
147191
m_ReverseTypeMap[m_HighMessageType] = messageWithHandler.MessageType;
148192
m_MessageTypes[messageWithHandler.MessageType] = m_HighMessageType++;
@@ -226,6 +270,70 @@ private bool CanReceive(ulong clientId, Type messageType, FastBufferReader messa
226270
return true;
227271
}
228272

273+
// Moves the handler for the type having hash `targetHash` to the `desiredOrder` position, in the handler list
274+
// This allows the server to tell the client which id it is using for which message and make sure the right
275+
// message is used when deserializing.
276+
internal void ReorderMessage(int desiredOrder, uint targetHash)
277+
{
278+
if (desiredOrder < 0)
279+
{
280+
throw new ArgumentException("ReorderMessage desiredOrder must be positive");
281+
}
282+
283+
if (desiredOrder < m_ReverseTypeMap.Length &&
284+
XXHash.Hash32(m_ReverseTypeMap[desiredOrder].FullName) == targetHash)
285+
{
286+
// matching positions and hashes. All good.
287+
return;
288+
}
289+
290+
Debug.Log($"Unexpected hash for {desiredOrder}");
291+
292+
// Since the message at `desiredOrder` is not the expected one,
293+
// insert an empty placeholder and move the messages down
294+
var typesAsList = new List<Type>(m_ReverseTypeMap);
295+
296+
typesAsList.Insert(desiredOrder, null);
297+
var handlersAsList = new List<MessageHandler>(m_MessageHandlers);
298+
handlersAsList.Insert(desiredOrder, null);
299+
300+
// we added a dummy message, bump the end up
301+
m_HighMessageType++;
302+
303+
// Here, we rely on the server telling us about all messages, in order.
304+
// So, we know the handlers before desiredOrder are correct.
305+
// We start at desiredOrder to not shift them when we insert.
306+
int position = desiredOrder;
307+
bool found = false;
308+
while (position < typesAsList.Count)
309+
{
310+
if (typesAsList[position] != null &&
311+
XXHash.Hash32(typesAsList[position].FullName) == targetHash)
312+
{
313+
found = true;
314+
break;
315+
}
316+
317+
position++;
318+
}
319+
320+
if (found)
321+
{
322+
// Copy the handler and type to the right index
323+
324+
typesAsList[desiredOrder] = typesAsList[position];
325+
handlersAsList[desiredOrder] = handlersAsList[position];
326+
typesAsList.RemoveAt(position);
327+
handlersAsList.RemoveAt(position);
328+
329+
// we removed a copy after moving a message, reduce the high message index
330+
m_HighMessageType--;
331+
}
332+
333+
m_ReverseTypeMap = typesAsList.ToArray();
334+
m_MessageHandlers = handlersAsList.ToArray();
335+
}
336+
229337
public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp, int serializedHeaderSize)
230338
{
231339
if (header.MessageType >= m_HighMessageType)
@@ -259,18 +367,29 @@ public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulon
259367
var handler = m_MessageHandlers[header.MessageType];
260368
using (reader)
261369
{
262-
// No user-land message handler exceptions should escape the receive loop.
263-
// If an exception is throw, the message is ignored.
264-
// Example use case: A bad message is received that can't be deserialized and throws
265-
// an OverflowException because it specifies a length greater than the number of bytes in it
266-
// for some dynamic-length value.
267-
try
370+
// This will also log an exception is if the server knows about a message type the client doesn't know
371+
// about. In this case the handler will be null. It is still an issue the user must deal with: If the
372+
// two connecting builds know about different messages, the server should not send a message to a client
373+
// that doesn't know about it
374+
if (handler == null)
268375
{
269-
handler.Invoke(reader, ref context, this);
376+
Debug.LogException(new HandlerNotRegisteredException(header.MessageType.ToString()));
270377
}
271-
catch (Exception e)
378+
else
272379
{
273-
Debug.LogException(e);
380+
// No user-land message handler exceptions should escape the receive loop.
381+
// If an exception is throw, the message is ignored.
382+
// Example use case: A bad message is received that can't be deserialized and throws
383+
// an OverflowException because it specifies a length greater than the number of bytes in it
384+
// for some dynamic-length value.
385+
try
386+
{
387+
handler.Invoke(reader, ref context, this);
388+
}
389+
catch (Exception e)
390+
{
391+
Debug.LogException(e);
392+
}
274393
}
275394
}
276395
for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx)

0 commit comments

Comments
 (0)