Skip to content

bug: Non-deterministic packet key assignment causes client disconnections when multiple plugins register custom packets #2818

@KaTOxDev

Description

@KaTOxDev

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

  1. Create two plugins that each register at least one custom packet type via context.Packet.TryRegisterPacketType<T>() in OnBootstrap
  2. Deploy both plugins to server and client
  3. Start the server, then start the client
  4. Client sends a plugin packet → server fails to deserialize it
  5. Observe KeyNotFoundException or Failed to deserialize packet data in server logs
  6. 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?

  • Yes this is platform-specific issue

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

  • This bug report is not a duplicate to the best of my knowledge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds verificationPending confirmation that the bug exists by another user.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions