Description
Summary
When multiple plugins register custom packet types via TryRegisterPacketType<T>(), the ushort keys assigned to those packets are non-deterministic across process restarts. This causes client-server packet deserialization failures (wrong type resolved for a given key), resulting in KeyNotFoundException or deserialization errors and client disconnections.
Affected Version
Intersect Engine (current main branch, .NET 8)
Root Cause
How Packet Keys Work
PackedIntersectPacket assigns sequential ushort keys to packet types in the order they appear in PacketTypeRegistry.TypesInternal. The key determines which Type is used to deserialize an incoming packet:
// PackedIntersectPacket.cs
public static readonly Dictionary<ushort, Type> KnownKeys = new();
public Type PacketType => KnownKeys[Key]; // throws if key mismatch
Built-in Packets: Deterministic ✅
Built-in packet types are sorted alphabetically by qualified name before registration in PacketTypeRegistry.TryRegisterBuiltIn():
// PacketTypeRegistry.cs:59-62
.OrderBy(
type => type.GetName(qualified: true),
CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.Ordinal)
)
This ensures built-in packets always get the same keys regardless of process.
Plugin Packets: Non-Deterministic ❌
Plugin packets are registered during OnBootstrap lifecycle. The bootstrap order depends on the iteration order of ConcurrentDictionary<string, Plugin> in PluginService:
// PluginService stores plugins in ConcurrentDictionary
Plugins.TryAdd(pluginEntry.Key, pluginEntry.Value);
ConcurrentDictionary iteration order is non-deterministic in .NET Core because string.GetHashCode() is randomized per process (a security feature introduced in .NET Core 2.1). This means:
- Same binaries, different process → different dictionary iteration order → different plugin bootstrap order → different
ushort key assignments
- Server and client are separate processes → they get different hash randomizations → different plugin iteration orders → mismatched packet keys
The Mismatch Scenario
| Key |
Server Process |
Client Process |
| 198 |
PluginA.Packet1 |
PluginB.Packet1 |
| 199 |
PluginB.Packet1 |
PluginA.Packet1 |
Client sends key 199 (meaning PluginA.Packet1), server looks up key 199 and finds PluginB.Packet1 → deserialization fails.
Proposed Fix
Sort PacketHelper.AvailablePacketTypes by qualified name before passing them to PackedIntersectPacket.AddKnownTypes, using the same sorting method already used for built-in types:
// ApplicationContext`2.cs - in Start() method, after BootstrapServices()
PackedIntersectPacket.AddKnownTypes(
PacketHelper.AvailablePacketTypes
.OrderBy(
type => type.GetName(qualified: true),
CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.Ordinal)
)
.ToList()
);
This requires adding:
using System.Globalization;
using System.Linq;
Why This Works
AvailablePacketTypes contains both built-in and plugin packet types
- Built-in types are already sorted (from
TryRegisterBuiltIn), but plugin types are appended in bootstrap order
- Sorting the entire list by qualified name ensures the same ordering regardless of bootstrap order
- Using the same
CultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.Ordinal) comparator as TryRegisterBuiltIn maintains stylistic consistency with the existing codebase
FIX
Steps to Reproduce
Reproduction Steps
- Create two plugins that each register at least one custom packet type via
context.Packet.TryRegisterPacketType<T>() in OnBootstrap
- Deploy both plugins to server and client
- Start the server, then start the client
- Client sends a plugin packet → server fails to deserialize it
- Observe
KeyNotFoundException or Failed to deserialize packet data in server logs
- Client is disconnected
Version with bug
v0.8.0-beta
Last version that worked well
Unknown
Affected platforms
Windows 11
Is this bug platform-specific?
Did you find any workaround?
- Run only one custom-packet plugin at a time (no key conflict possible)
- Use reflection to sort PacketTypeRegistry.TypesInternal by Type.FullName after all plugins have bootstrapped, before AddKnownTypes is called
Relevant log output
[ERR] Failed to deserialize packet data
System.Collections.Generic.KeyNotFoundException: The given key '199' was not present in the dictionary.
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at Intersect.Network.PackedIntersectPacket.get_PacketType() in PackedIntersectPacket.cs:line 50
at Intersect.Network.MessagePacker.Deserialize(Byte[] packetData, Boolean silent) in MessagePacker.cs:line 38
[INF] Disconnected [cf253c8f-5321-44af-b2bd-8673cd821642]
Duplicate Bug Check
Description
Summary
When multiple plugins register custom packet types via
TryRegisterPacketType<T>(), theushortkeys assigned to those packets are non-deterministic across process restarts. This causes client-server packet deserialization failures (wrong type resolved for a given key), resulting inKeyNotFoundExceptionor deserialization errors and client disconnections.Affected Version
Intersect Engine (current main branch, .NET 8)
Root Cause
How Packet Keys Work
PackedIntersectPacketassigns sequentialushortkeys to packet types in the order they appear inPacketTypeRegistry.TypesInternal. The key determines whichTypeis used to deserialize an incoming packet:Built-in Packets: Deterministic ✅
Built-in packet types are sorted alphabetically by qualified name before registration in
PacketTypeRegistry.TryRegisterBuiltIn():This ensures built-in packets always get the same keys regardless of process.
Plugin Packets: Non-Deterministic ❌
Plugin packets are registered during
OnBootstraplifecycle. The bootstrap order depends on the iteration order ofConcurrentDictionary<string, Plugin>inPluginService:ConcurrentDictionaryiteration order is non-deterministic in .NET Core becausestring.GetHashCode()is randomized per process (a security feature introduced in .NET Core 2.1). This means:ushortkey assignmentsThe Mismatch Scenario
Client sends key
199(meaningPluginA.Packet1), server looks up key199and findsPluginB.Packet1→ deserialization fails.Proposed Fix
Sort
PacketHelper.AvailablePacketTypesby qualified name before passing them toPackedIntersectPacket.AddKnownTypes, using the same sorting method already used for built-in types:This requires adding:
Why This Works
AvailablePacketTypescontains both built-in and plugin packet typesTryRegisterBuiltIn), but plugin types are appended in bootstrap orderCultureInfo.InvariantCulture.CompareInfo.GetStringComparer(CompareOptions.Ordinal)comparator asTryRegisterBuiltInmaintains stylistic consistency with the existing codebaseFIX
Steps to Reproduce
Reproduction Steps
context.Packet.TryRegisterPacketType<T>()inOnBootstrapKeyNotFoundExceptionorFailed to deserialize packet datain server logsVersion with bug
v0.8.0-beta
Last version that worked well
Unknown
Affected platforms
Windows 11
Is this bug platform-specific?
Did you find any workaround?
Relevant log output
Duplicate Bug Check