From 4399d69708e3f820fc061551920f1ddc9be319eb Mon Sep 17 00:00:00 2001
From: Az <56699741+AlchlcDvl@users.noreply.github.com>
Date: Wed, 7 Oct 2026 20:16:12 -0400
Subject: [PATCH 1/3] More networking improvements
- Removes deprecated code
- Improves the reading and writing `long` values
- Readds the Color write method (because we're better off having it than not tbh)
- Adds a new `INetSerializable` interface that allows types to define how they are read and written
- Improves on the generic message converter system to allow multiple type parameters of an arbitrary order
- Improves the boxed read method by consolidating a lot of the type checks into a Dictionary key read
---
.../Attributes/MethodRpcAttribute.cs | 9 +-
.../Extensions/ExtraMessageExtensions.cs | 38 ++-
Reactor/Networking/Rpc/CustomRpc.cs | 14 +-
Reactor/Networking/Rpc/MethodRpc.cs | 22 --
Reactor/Networking/Rpc/PlayerCustomRpc.cs | 10 +-
Reactor/Networking/Rpc/UnsafeCustomRpc.cs | 8 +-
.../Serialization/INetSerializable.cs | 21 ++
.../Serialization/MessageSerializer.cs | 311 ++++++++++++------
8 files changed, 285 insertions(+), 148 deletions(-)
create mode 100644 Reactor/Networking/Serialization/INetSerializable.cs
diff --git a/Reactor/Networking/Attributes/MethodRpcAttribute.cs b/Reactor/Networking/Attributes/MethodRpcAttribute.cs
index b0fa1b09..c39cd6d6 100644
--- a/Reactor/Networking/Attributes/MethodRpcAttribute.cs
+++ b/Reactor/Networking/Attributes/MethodRpcAttribute.cs
@@ -33,12 +33,6 @@ public sealed class MethodRpcAttribute : Attribute
///
public RpcLocalHandling LocalHandling { get; set; } = RpcLocalHandling.Before;
- ///
- /// Gets or sets a value indicating whether the rpc should be sent immediately.
- ///
- [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. This property will be removed in a future version.")]
- public bool SendImmediately { get; set; } = true;
-
///
/// Initializes a new instance of the class.
///
@@ -56,8 +50,7 @@ public MethodRpcAttribute(uint id)
/// The plugin to register the rpc to.
public static void Register(Assembly assembly, BasePlugin plugin)
{
- if (_registeredAssemblies.Contains(assembly)) return;
- _registeredAssemblies.Add(assembly);
+ if (!_registeredAssemblies.Add(assembly)) return;
var methods = assembly.GetTypes()
.SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
diff --git a/Reactor/Networking/Extensions/ExtraMessageExtensions.cs b/Reactor/Networking/Extensions/ExtraMessageExtensions.cs
index 439cf420..7d21f339 100644
--- a/Reactor/Networking/Extensions/ExtraMessageExtensions.cs
+++ b/Reactor/Networking/Extensions/ExtraMessageExtensions.cs
@@ -47,7 +47,17 @@ public static void Write(this MessageWriter writer, Vector2 value)
///
/// The to write to.
/// The to write.
- public static void Write(this MessageWriter writer, long value) => writer.Write(BitConverter.GetBytes(value));
+ public static void Write(this MessageWriter writer, long value)
+ {
+ writer.Write((byte) value);
+ writer.Write((byte) (value >> 8));
+ writer.Write((byte) (value >> 16));
+ writer.Write((byte) (value >> 24));
+ writer.Write((byte) (value >> 32));
+ writer.Write((byte) (value >> 40));
+ writer.Write((byte) (value >> 48));
+ writer.Write((byte) (value >> 56));
+ }
///
/// Writes a color value to the .
@@ -62,6 +72,19 @@ public static void Write(this MessageWriter writer, Color32 value)
writer.Write(value.a);
}
+ ///
+ /// Writes a color value to the .
+ ///
+ /// The to write to.
+ /// The to write.
+ public static void Write(this MessageWriter writer, Color value)
+ {
+ writer.Write(value.r);
+ writer.Write(value.g);
+ writer.Write(value.b);
+ writer.Write(value.a);
+ }
+
///
/// Reads a from the .
///
@@ -96,7 +119,18 @@ public static Vector2 ReadVector2(this MessageReader reader)
///
/// The to read from.
/// The resulting long value from the .
- public static long ReadInt64(this MessageReader reader) => BitConverter.ToInt64(reader.ReadBytes(8), 0);
+ public static long ReadInt64(this MessageReader reader)
+ {
+ var uval = (long) reader.ReadByte();
+ uval |= (long) reader.ReadByte() << 8;
+ uval |= (long) reader.ReadByte() << 16;
+ uval |= (long) reader.ReadByte() << 24;
+ uval |= (long) reader.ReadByte() << 32;
+ uval |= (long) reader.ReadByte() << 40;
+ uval |= (long) reader.ReadByte() << 48;
+ uval |= (long) reader.ReadByte() << 56;
+ return uval;
+ }
///
/// Reads a color value from a network message.
diff --git a/Reactor/Networking/Rpc/CustomRpc.cs b/Reactor/Networking/Rpc/CustomRpc.cs
index fe161eb0..cb70f556 100644
--- a/Reactor/Networking/Rpc/CustomRpc.cs
+++ b/Reactor/Networking/Rpc/CustomRpc.cs
@@ -56,11 +56,10 @@ public override void UnsafeHandle(InnerNetObject innerNetObject, object? data)
///
/// The to send the rpc on.
/// The data to send.
- /// Whether to send it immediately.
/// The callback to invoke when this packet is acknowledged.
- public void Send(InnerNetObject innerNetObject, TData data, bool immediately = false, Action? ackCallback = null)
+ public void Send(InnerNetObject innerNetObject, TData data, Action? ackCallback = null)
{
- UnsafeSend(innerNetObject, data, immediately, ackCallback: ackCallback);
+ UnsafeSend(innerNetObject, data, ackCallback: ackCallback);
}
///
@@ -72,7 +71,7 @@ public void Send(InnerNetObject innerNetObject, TData data, bool immediately = f
/// The callback to invoke when this packet is acknowledged.
public void SendTo(InnerNetObject innerNetObject, int targetClientId, TData data, Action? ackCallback = null)
{
- UnsafeSend(innerNetObject, data, true, targetClientId, ackCallback);
+ UnsafeSend(innerNetObject, data, targetClientId, ackCallback);
}
}
@@ -121,11 +120,10 @@ public override void UnsafeHandle(InnerNetObject innerNetObject, object? data)
/// Sends this custom rpc on the specified .
///
/// The to send the rpc on.
- /// Whether to send it immediately.
/// The callback to invoke when this packet is acknowledged.
- public void Send(InnerNetObject innerNetObject, bool immediately = false, Action? ackCallback = null)
+ public void Send(InnerNetObject innerNetObject, Action? ackCallback = null)
{
- UnsafeSend(innerNetObject, immediately, ackCallback: ackCallback);
+ UnsafeSend(innerNetObject, null, ackCallback: ackCallback);
}
///
@@ -136,6 +134,6 @@ public void Send(InnerNetObject innerNetObject, bool immediately = false, Action
/// The callback to invoke when this packet is acknowledged.
public void SendTo(InnerNetObject innerNetObject, int targetClientId, Action? ackCallback = null)
{
- UnsafeSend(innerNetObject, null, true, targetClientId, ackCallback);
+ UnsafeSend(innerNetObject, null, targetClientId, ackCallback);
}
}
diff --git a/Reactor/Networking/Rpc/MethodRpc.cs b/Reactor/Networking/Rpc/MethodRpc.cs
index 49016355..3f1bd61a 100644
--- a/Reactor/Networking/Rpc/MethodRpc.cs
+++ b/Reactor/Networking/Rpc/MethodRpc.cs
@@ -69,22 +69,6 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio
_handle = Hook(method, parameters, method.IsStatic);
}
- ///
- /// Initializes a new instance of the class.
- ///
- /// The plugin that the rpc is attached to.
- /// The method of the method rpc.
- /// The id of the rpc.
- /// The send option of the rpc.
- /// The local handling method of the rpc.
- /// The value indicating whether the rpc should be sent immediately.
- [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. Remove sendImmediately from the parameter list.")]
- public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption option, RpcLocalHandling localHandling, bool sendImmediately)
- : this(plugin, method, id, option, localHandling)
- {
- SendImmediately = sendImmediately;
- }
-
///
/// Gets the method of the method rpc.
///
@@ -102,12 +86,6 @@ public MethodRpc(BasePlugin plugin, MethodInfo method, uint id, SendOption optio
///
public override SendOption SendOption { get; }
- ///
- /// Gets a value indicating whether the method rpc should be sent immediately.
- ///
- [Obsolete("Non-immediate RPCs were removed in 2025.5.20. All RPCs are immediate. This property will be removed in a future version.")]
- public bool SendImmediately { get; } = true;
-
///
public override void UnsafeWrite(MessageWriter writer, object? data)
{
diff --git a/Reactor/Networking/Rpc/PlayerCustomRpc.cs b/Reactor/Networking/Rpc/PlayerCustomRpc.cs
index 47c9d8a1..77b26847 100644
--- a/Reactor/Networking/Rpc/PlayerCustomRpc.cs
+++ b/Reactor/Networking/Rpc/PlayerCustomRpc.cs
@@ -19,11 +19,10 @@ protected PlayerCustomRpc(TPlugin plugin, uint id) : base(plugin, id)
/// Sends this custom rpc on the local player with the specified .
///
/// The data to send.
- /// Whether to send it immediately.
/// The callback to invoke when this packet is acknowledged.
- public void Send(TData data, bool immediately = false, Action? ackCallback = null)
+ public void Send(TData data, Action? ackCallback = null)
{
- Send(PlayerControl.LocalPlayer, data, immediately, ackCallback);
+ Send(PlayerControl.LocalPlayer, data, ackCallback);
}
///
@@ -52,11 +51,10 @@ protected PlayerCustomRpc(TPlugin plugin, uint id) : base(plugin, id)
///
/// Sends this custom rpc on the local player.
///
- /// Whether to send it immediately.
/// The callback to invoke when this packet is acknowledged.
- public void Send(bool immediately = false, Action? ackCallback = null)
+ public void Send(Action? ackCallback = null)
{
- Send(PlayerControl.LocalPlayer, immediately, ackCallback);
+ Send(PlayerControl.LocalPlayer, ackCallback);
}
///
diff --git a/Reactor/Networking/Rpc/UnsafeCustomRpc.cs b/Reactor/Networking/Rpc/UnsafeCustomRpc.cs
index 67fefb40..4a37021f 100644
--- a/Reactor/Networking/Rpc/UnsafeCustomRpc.cs
+++ b/Reactor/Networking/Rpc/UnsafeCustomRpc.cs
@@ -91,10 +91,9 @@ protected UnsafeCustomRpc(BasePlugin plugin, uint id)
///
/// The to send the rpc on.
/// The data to send.
- /// Whether to send it immediately.
/// Target client id, defaults to broadcast.
/// The callback to invoke when this packet is acknowledged.
- public void UnsafeSend(InnerNetObject innerNetObject, object? data, bool immediately = false, int targetClientId = -1, Action? ackCallback = null)
+ public void UnsafeSend(InnerNetObject innerNetObject, object? data, int targetClientId = -1, Action? ackCallback = null)
{
ArgumentNullException.ThrowIfNull(innerNetObject);
@@ -108,11 +107,6 @@ public void UnsafeSend(InnerNetObject innerNetObject, object? data, bool immedia
UnsafeHandle(innerNetObject, data);
}
- if (immediately == false)
- {
- Warning("Non-immediate RPCs were removed in 2025.5.20! Reactor will now always send immediately!");
- }
-
var writer = AmongUsClient.Instance.StartRpcImmediately(
innerNetObject.NetId,
CustomRpcManager.CallId,
diff --git a/Reactor/Networking/Serialization/INetSerializable.cs b/Reactor/Networking/Serialization/INetSerializable.cs
new file mode 100644
index 00000000..768940c8
--- /dev/null
+++ b/Reactor/Networking/Serialization/INetSerializable.cs
@@ -0,0 +1,21 @@
+using Hazel;
+
+namespace Reactor.Networking.Serialization;
+
+///
+/// Provides a read-write method pair so that data types can define how to read and write themselves.
+///
+public interface INetSerializable
+{
+ ///
+ /// Writes the instance to the provided writer.
+ ///
+ /// The to write to.
+ void WriteTo(MessageWriter writer);
+
+ ///
+ /// Reads the instance's properties from the provided reader.
+ ///
+ /// The to read from.
+ void ReadFrom(MessageReader reader);
+}
diff --git a/Reactor/Networking/Serialization/MessageSerializer.cs b/Reactor/Networking/Serialization/MessageSerializer.cs
index 5a1236cd..8f0ad203 100644
--- a/Reactor/Networking/Serialization/MessageSerializer.cs
+++ b/Reactor/Networking/Serialization/MessageSerializer.cs
@@ -1,6 +1,9 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
using Hazel;
using Reactor.Networking.Extensions;
using UnityEngine;
@@ -12,10 +15,16 @@ namespace Reactor.Networking.Serialization;
///
public static class MessageSerializer
{
+ private static Dictionary MessageConverterMap { get; } = new();
private static List MessageConverters { get; } = new();
+ private static List GenericConverters { get; } = new();
- private static Dictionary MessageConverterMap { get; } = new();
- private static Dictionary GenericConvertersMap { get; } = new();
+ private enum SearchMode
+ {
+ Basic,
+ Generic,
+ Combined,
+ }
internal static void ClearMaps()
{
@@ -30,68 +39,197 @@ public static void Register(Type type)
{
if (type.IsGenericTypeDefinition)
{
- var baseType = type.BaseType!;
+ var baseType = type.BaseType;
+ var isUnsafeConverter = baseType == typeof(UnsafeMessageConverter);
+ var isMessageConverterT = baseType != null && baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(MessageConverter<>);
- if (!baseType.Name.Contains("MessageConverter"))
- throw new InvalidOperationException($"{type.Name} should directly inherit from MessageConverter");
+ if (!isMessageConverterT && !isUnsafeConverter)
+ throw new InvalidOperationException($"{type.Name} must directly inherit from either MessageConverter or UnsafeMessageConverter.");
- var generics = baseType.GetGenericArguments();
- var param = generics[0];
-
- GenericConvertersMap.Add(param.GetGenericTypeDefinition(), type);
+ GenericConverters.Add(type);
}
else
{
- var messageConverter = (UnsafeMessageConverter) Activator.CreateInstance(type)!;
+ var messageConverter = (UnsafeMessageConverter) CreateObject(type)!;
MessageConverters.Add(messageConverter);
}
}
///
- /// Finds a MessageConverter for the specified .
+ /// Finds a MessageConverter for the specified by checking generic converters first, then basic converters.
///
- /// The type of an object.
+ /// The type of the object.
/// A MessageConverter that can convert the specified .
public static UnsafeMessageConverter? FindConverter(Type type)
- {
- if (!MessageConverterMap.TryGetValue(type, out var value))
- {
- value = MessageConverters.SingleOrDefault(x => x.CanConvert(type));
-
- if (value == null)
- return null;
-
- MessageConverterMap.Add(type, value);
- }
+ => ResolveAndCache(type, SearchMode.Combined);
- return value;
- }
+ ///
+ /// Finds a MessageConverter for the specified using basic converters.
+ ///
+ /// The type of the object.
+ /// A MessageConverter that can convert the specified .
+ public static UnsafeMessageConverter? FindBasicConverter(Type type)
+ => ResolveAndCache(type, SearchMode.Basic);
///
/// Finds and builds a MessageConverter for the specified using a registered generic converter.
///
- /// The type of an object.
+ /// The type of the object.
/// A MessageConverter that can convert the specified .
public static UnsafeMessageConverter? FindGenericConverter(Type type)
+ => ResolveAndCache(type, SearchMode.Generic);
+
+ private static UnsafeMessageConverter? ResolveAndCache(Type type, SearchMode mode)
{
if (MessageConverterMap.TryGetValue(type, out var value))
return value;
- if (!type.IsGenericType)
+ value = mode switch
+ {
+ SearchMode.Basic => FindBasicConverterInternal(type),
+ SearchMode.Generic => FindGenericConverterInternal(type),
+ SearchMode.Combined => FindGenericConverterInternal(type) ?? FindBasicConverterInternal(type),
+ _ => null,
+ };
+
+ MessageConverterMap[type] = value;
+
+ return value;
+ }
+
+ private static UnsafeMessageConverter? FindBasicConverterInternal(Type type)
+ {
+ return MessageConverters.SingleOrDefault(x => x.CanConvert(type));
+ }
+
+ private static UnsafeMessageConverter? FindGenericConverterInternal(Type type)
+ {
+ foreach (var converterTypeDef in GenericConverters)
+ {
+ var mappedArgs = MapGenericArguments(converterTypeDef, type);
+
+ if (mappedArgs == null)
+ continue;
+
+ var converterParams = converterTypeDef.GetGenericArguments();
+ var allSatisfy = true;
+
+ for (var i = 0; i < mappedArgs.Length; i++)
+ {
+ if (SatisfiesConstraints(mappedArgs[i], converterParams[i]))
+ continue;
+
+ allSatisfy = false;
+ break;
+ }
+
+ if (!allSatisfy)
+ continue;
+
+ var genericConverterType = converterTypeDef.MakeGenericType(mappedArgs);
+ var converterInstance = (UnsafeMessageConverter) CreateObject(genericConverterType)!;
+
+ MessageConverters.Add(converterInstance);
+
+ return converterInstance;
+ }
+
+ return null;
+ }
+
+ private static Type[]? MapGenericArguments(Type converterTypeDef, Type targetType)
+ {
+ var baseType = converterTypeDef.BaseType;
+
+ if (baseType == null)
return null;
- var typeDef = type.GetGenericTypeDefinition();
+ if (baseType == typeof(UnsafeMessageConverter))
+ {
+ var converterParams = converterTypeDef.GetGenericArguments();
+
+ if (converterParams.Length == 1)
+ return new[] { targetType };
+
+ if (targetType.IsGenericType && converterParams.Length == targetType.GetGenericArguments().Length)
+ return targetType.GetGenericArguments();
- if (!GenericConvertersMap.TryGetValue(typeDef, out var builder))
return null;
+ }
- var generic = builder.MakeGenericType(type.GetGenericArguments());
- value = (UnsafeMessageConverter) Activator.CreateInstance(generic)!;
+ if (!baseType.IsGenericType)
+ return null;
- MessageConverters.Add(value);
- MessageConverterMap.Add(type, value);
+ var templateType = baseType.GetGenericArguments()[0];
- return value;
+ if (templateType.IsGenericParameter)
+ return new[] { targetType };
+
+ if (!templateType.IsGenericType || !targetType.IsGenericType)
+ {
+ return null;
+ }
+
+ var templateArgs = templateType.GetGenericArguments();
+ var targetArgs = targetType.GetGenericArguments();
+
+ if (templateArgs.Length != targetArgs.Length)
+ return null;
+
+ var mappedArgs = new Type[converterTypeDef.GetGenericArguments().Length];
+
+ for (var i = 0; i < templateArgs.Length; i++)
+ {
+ var templateArg = templateArgs[i];
+ var targetArg = targetArgs[i];
+
+ if (templateArg.IsGenericParameter)
+ mappedArgs[templateArg.GenericParameterPosition] = targetArg;
+ else if (templateArg != targetArg)
+ return null;
+ }
+
+ return mappedArgs.Any(t => t == null) ? null : mappedArgs;
+ }
+
+ private static bool SatisfiesConstraints(Type targetType, Type genericParam)
+ {
+ var attributes = genericParam.GenericParameterAttributes;
+ var constraints = genericParam.GetGenericParameterConstraints();
+
+ // struct constraint
+ if (attributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint) && !targetType.IsValueType)
+ return false;
+
+ // class constraint
+ if (attributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint) && targetType.IsValueType)
+ return false;
+
+ // new() constraint
+ if (attributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint) && !targetType.IsValueType && targetType.GetConstructor(Type.EmptyTypes) == null)
+ return false;
+
+ // notnull constraint
+ if (genericParam.CustomAttributes.Any(a => a.AttributeType.Name == "NullableAttribute"))
+ {
+ if (Nullable.GetUnderlyingType(targetType) != null)
+ return false;
+
+ // Nullable reference types cannot be distinguished, so we let them through here
+ }
+
+ // Explicit type constraints
+ foreach (var constraint in constraints)
+ {
+ var check = constraint == typeof(Enum)
+ ? !targetType.IsEnum
+ : !constraint.IsAssignableFrom(targetType);
+
+ if (check)
+ return false;
+ }
+
+ return true;
}
///
@@ -158,9 +296,12 @@ public static void Serialize(this MessageWriter writer, object @object)
case Enum i:
writer.Write(i);
break;
+ case INetSerializable i:
+ i.WriteTo(writer);
+ break;
default:
var type = @object.GetType();
- var converter = FindGenericConverter(type) ?? FindConverter(type);
+ var converter = FindConverter(type);
if (converter != null)
converter.UnsafeWrite(writer, @object);
@@ -179,6 +320,24 @@ public static void Serialize(this MessageWriter writer, object @object)
/// A generic value from the .
public static T Deserialize(this MessageReader reader) => (T) reader.Deserialize(typeof(T));
+ private static readonly Dictionary> _basicReaders = new()
+ {
+ [typeof(int)] = reader => reader.ReadPackedInt32(),
+ [typeof(uint)] = reader => reader.ReadPackedUInt32(),
+ [typeof(byte)] = reader => reader.ReadByte(),
+ [typeof(float)] = reader => reader.ReadSingle(),
+ [typeof(sbyte)] = reader => reader.ReadSByte(),
+ [typeof(ushort)] = reader => reader.ReadUInt16(),
+ [typeof(short)] = reader => reader.ReadInt16(),
+ [typeof(bool)] = reader => reader.ReadBoolean(),
+ [typeof(Vector2)] = reader => reader.ReadVector2(),
+ [typeof(string)] = reader => reader.ReadString(),
+ [typeof(ulong)] = reader => reader.ReadUInt64(),
+ [typeof(long)] = reader => reader.ReadInt64(),
+ [typeof(Color)] = reader => reader.ReadColor(),
+ [typeof(Color32)] = reader => reader.ReadColor32(),
+ };
+
///
/// Deserializes an of from the .
///
@@ -187,83 +346,45 @@ public static void Serialize(this MessageWriter writer, object @object)
/// An from the .
public static object Deserialize(this MessageReader reader, Type objectType)
{
- if (objectType == typeof(int))
- {
- return reader.ReadPackedInt32();
- }
-
- if (objectType == typeof(uint))
- {
- return reader.ReadPackedUInt32();
- }
-
- if (objectType == typeof(byte))
- {
- return reader.ReadByte();
- }
-
- if (objectType == typeof(float))
- {
- return reader.ReadSingle();
- }
-
- if (objectType == typeof(sbyte))
+ if (_basicReaders.TryGetValue(objectType, out var readDel))
{
- return reader.ReadSByte();
+ return readDel(reader);
}
- if (objectType == typeof(ushort))
- {
- return reader.ReadUInt16();
- }
-
- if (objectType == typeof(bool))
- {
- return reader.ReadBoolean();
- }
-
- if (objectType == typeof(Vector2))
- {
- return reader.ReadVector2();
- }
-
- if (objectType == typeof(string))
+ if (objectType.IsEnum)
{
- return reader.ReadString();
+ return reader.ReadEnum(objectType);
}
- if (objectType == typeof(ulong))
+ if (objectType.IsAssignableTo(typeof(INetSerializable)))
{
- return reader.ReadUInt64();
+ var instance = (INetSerializable) CreateObject(objectType);
+ instance.ReadFrom(reader);
+ return instance;
}
- if (objectType == typeof(long))
- {
- return reader.ReadInt64();
- }
+ var converter = FindConverter(objectType);
- if (objectType == typeof(Color))
+ if (converter != null)
{
- return reader.ReadColor();
+ return converter.UnsafeRead(reader, objectType);
}
- if (objectType == typeof(Color32))
- {
- return reader.ReadColor32();
- }
+ throw new NotSupportedException("Couldn't deserialize " + objectType);
+ }
- if (objectType.IsEnum)
- {
- return reader.ReadEnum(objectType);
- }
+ private static readonly ConcurrentDictionary _constructors = new();
- var converter = FindGenericConverter(objectType) ?? FindConverter(objectType);
+ private static object CreateObject(Type objectType)
+ {
+ var construct = _constructors.GetOrAdd(objectType, GetConstruct);
- if (converter != null)
- {
- return converter.UnsafeRead(reader, objectType);
- }
+ if (construct != null)
+ return construct.Invoke(null);
- throw new NotSupportedException("Couldn't deserialize " + objectType);
+ Warning($"{objectType.Name} does not have a parameterless constructor. It is highly recommended that one is added so that field initialisers are not skipped.");
+ return RuntimeHelpers.GetUninitializedObject(objectType);
}
+
+ private static ConstructorInfo? GetConstruct(Type type) => type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, Type.EmptyTypes);
}
From 78aa1338d32d24af7c4c85dc9d6a3ae0e3dfb191 Mon Sep 17 00:00:00 2001
From: Az <56699741+AlchlcDvl@users.noreply.github.com>
Date: Wed, 7 Oct 2026 20:19:24 -0400
Subject: [PATCH 2/3] Search for basic converters first before heavy reflection
---
Reactor/Networking/Serialization/MessageSerializer.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Reactor/Networking/Serialization/MessageSerializer.cs b/Reactor/Networking/Serialization/MessageSerializer.cs
index 8f0ad203..dffb9fcd 100644
--- a/Reactor/Networking/Serialization/MessageSerializer.cs
+++ b/Reactor/Networking/Serialization/MessageSerializer.cs
@@ -88,7 +88,7 @@ public static void Register(Type type)
{
SearchMode.Basic => FindBasicConverterInternal(type),
SearchMode.Generic => FindGenericConverterInternal(type),
- SearchMode.Combined => FindGenericConverterInternal(type) ?? FindBasicConverterInternal(type),
+ SearchMode.Combined => FindBasicConverterInternal(type) ?? FindGenericConverterInternal(type),
_ => null,
};
From d7811137a6eda49b023e8432368dbdc69c4239e2 Mon Sep 17 00:00:00 2001
From: Az <56699741+AlchlcDvl@users.noreply.github.com>
Date: Wed, 7 Oct 2026 23:51:19 -0400
Subject: [PATCH 3/3] Add packed Int64 and UInt64 reading and writing
---
.../Extensions/ExtraMessageExtensions.cs | 68 +++++++++++++++++++
.../Serialization/MessageSerializer.cs | 8 +--
2 files changed, 72 insertions(+), 4 deletions(-)
diff --git a/Reactor/Networking/Extensions/ExtraMessageExtensions.cs b/Reactor/Networking/Extensions/ExtraMessageExtensions.cs
index 7d21f339..f67fcfd0 100644
--- a/Reactor/Networking/Extensions/ExtraMessageExtensions.cs
+++ b/Reactor/Networking/Extensions/ExtraMessageExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.IO;
using Hazel;
using Hazel.Udp;
using Reactor.Networking.Serialization;
@@ -21,6 +22,34 @@ private static float ReverseLerp(float t)
return Mathf.Clamp01((t - MIN) / DIFF);
}
+ ///
+ /// Writes a packed long value to the .
+ ///
+ /// The to write to.
+ /// The to write.
+ public static void WritePacked(this MessageWriter writer, long value)
+ {
+ // From what I can tell from the publicly available source of Hazel, they just cast the int value to a uint??
+ // Seems like they never heard of ZigZag encoding
+ writer.WritePacked((ulong) ((value << 1) ^ (value >> 63)));
+ }
+
+ ///
+ /// Writes a packed ulong value to the .
+ ///
+ /// The to write to.
+ /// The to write.
+ public static void WritePacked(this MessageWriter writer, ulong value)
+ {
+ while (value >= 0x80)
+ {
+ writer.Write((byte) (value | 0x80));
+ value >>= 7;
+ }
+
+ writer.Write((byte) value);
+ }
+
///
/// Writes a to the .
///
@@ -85,6 +114,45 @@ public static void Write(this MessageWriter writer, Color value)
writer.Write(value.a);
}
+ ///
+ /// Reads a packed from the .
+ ///
+ /// The to read from.
+ /// A from the .
+ public static long ReadPackedInt64(this MessageReader reader)
+ {
+ // See comment in WritePacked(long)
+ var val = reader.ReadPackedUInt64();
+ return (long) (val >> 1) ^ -(long) (val & 1);
+ }
+
+ ///
+ /// Reads a packed from the .
+ ///
+ /// The to read from.
+ /// A from the .
+ public static ulong ReadPackedUInt64(this MessageReader reader)
+ {
+ var result = 0ul;
+ var shift = 0;
+
+ while (true)
+ {
+ var b = reader.ReadByte();
+ result |= (ulong) (b & 0x7F) << shift;
+
+ if ((b & 0x80) == 0)
+ break;
+
+ shift += 7;
+
+ if (shift >= 70)
+ throw new InvalidDataException("VarInt too long");
+ }
+
+ return result;
+ }
+
///
/// Reads a from the .
///
diff --git a/Reactor/Networking/Serialization/MessageSerializer.cs b/Reactor/Networking/Serialization/MessageSerializer.cs
index dffb9fcd..6f0e1cfb 100644
--- a/Reactor/Networking/Serialization/MessageSerializer.cs
+++ b/Reactor/Networking/Serialization/MessageSerializer.cs
@@ -276,10 +276,10 @@ public static void Serialize(this MessageWriter writer, object @object)
writer.Write(i);
break;
case ulong i:
- writer.Write(i);
+ writer.WritePacked(i);
break;
case long i:
- ExtraMessageExtensions.Write(writer, i); // For some reason this insists on referring the to float write method, so this is me taking precautions
+ writer.WritePacked(i);
break;
case Vector2 i:
writer.Write(i);
@@ -332,8 +332,8 @@ public static void Serialize(this MessageWriter writer, object @object)
[typeof(bool)] = reader => reader.ReadBoolean(),
[typeof(Vector2)] = reader => reader.ReadVector2(),
[typeof(string)] = reader => reader.ReadString(),
- [typeof(ulong)] = reader => reader.ReadUInt64(),
- [typeof(long)] = reader => reader.ReadInt64(),
+ [typeof(ulong)] = reader => reader.ReadPackedUInt64(),
+ [typeof(long)] = reader => reader.ReadPackedInt64(),
[typeof(Color)] = reader => reader.ReadColor(),
[typeof(Color32)] = reader => reader.ReadColor32(),
};