diff --git a/Assets/Mirror/Components/Experimental.meta b/Assets/Mirror/Components/Experimental.meta
new file mode 100644
index 0000000000..01cc033262
--- /dev/null
+++ b/Assets/Mirror/Components/Experimental.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 4d6df8943f4e4eeab8a968736e4dd301
+timeCreated: 1733866184
\ No newline at end of file
diff --git a/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase.meta b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase.meta
new file mode 100644
index 0000000000..a01635c9a7
--- /dev/null
+++ b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: bc19b3fc397b426ab29a5444cdeb4974
+timeCreated: 1733866201
\ No newline at end of file
diff --git a/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/IsExternalInit.cs b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/IsExternalInit.cs
new file mode 100644
index 0000000000..9062ad430f
--- /dev/null
+++ b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/IsExternalInit.cs
@@ -0,0 +1,4 @@
+namespace System.Runtime.CompilerServices{
+ internal static class IsExternalInit{
+ }
+}
\ No newline at end of file
diff --git a/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/IsExternalInit.cs.meta b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/IsExternalInit.cs.meta
new file mode 100644
index 0000000000..d1e43188d4
--- /dev/null
+++ b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/IsExternalInit.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 4cfe6c2c82a04406b8368d171a7fa013
+timeCreated: 1733953738
\ No newline at end of file
diff --git a/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/NetworkPlayerInputs.cs b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/NetworkPlayerInputs.cs
new file mode 100644
index 0000000000..178b778547
--- /dev/null
+++ b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/NetworkPlayerInputs.cs
@@ -0,0 +1,397 @@
+using System.Runtime.CompilerServices; // do not remove, required to add init support for net4.9 or lower
+using System.Collections.Generic;
+using System.Linq;
+using System;
+using UnityEngine;
+
+#nullable enable
+namespace Mirror.Components.Experimental{
+ public struct NetworkPlayerInputs{
+ /// Initializes a new instance of the struct. Optionally takes defaults to initialize the instance.
+ /// Default inputs to copy values from, or null for default initialization.
+ public NetworkPlayerInputs(NetworkPlayerInputs? defaults = null) {
+ _tickNumber = defaults?._tickNumber;
+ _serverTickOffset = defaults?._serverTickOffset;
+ _movementVector = defaults?._movementVector;
+ _joystickVector = defaults?._joystickVector;
+ _mouseVectorX = defaults?._mouseVectorX;
+ _mouseVectorY = defaults?._mouseVectorY;
+ _additionalInputs = defaults?._additionalInputs;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with specific values.
+ /// This private constructor allows internal use for creating instances with selected fields.
+ ///
+ /// The tick number to set.
+ /// The server tick offset, or null if unset.
+ /// The movement vector, or null if unset.
+ /// The joystick vector, or null if unset.
+ /// The X component of the mouse vector, or null if unset.
+ /// The Y component of the mouse vector, or null if unset.
+ /// The additional inputs, or null if unset.
+ private NetworkPlayerInputs(int? tickNumber, byte? serverTickOffset, ushort? movementVector, ushort? joystickVector, ushort? mouseVectorX,
+ ushort? mouseVectorY, ReadOnlyMemory? additionalInputs) {
+ _tickNumber = tickNumber;
+ _serverTickOffset = serverTickOffset;
+ _movementVector = movementVector;
+ _joystickVector = joystickVector;
+ _mouseVectorX = mouseVectorX;
+ _mouseVectorY = mouseVectorY;
+ _additionalInputs = additionalInputs;
+ }
+
+ /* Inputs Compare and Extend methods */
+
+ #region Inputs Compare and Extend methods
+
+ ///
+ /// Compares the current NetworkPlayerInputs with another and returns the differences as a new instance.
+ /// Only fields that differ will be set in the returned instance; others will remain null.
+ ///
+ /// The other NetworkPlayerInputs to compare with.
+ /// A new NetworkPlayerInputs instance with only differing values, or null if there are no differences.
+ public NetworkPlayerInputs? GetChangedInputsComparedTo(NetworkPlayerInputs inputs) {
+ // Compare each field and set values that differ, leaving others as null
+ byte? serverTickOffset = _serverTickOffset != inputs._serverTickOffset ? _serverTickOffset : null;
+ ushort? movementVector = _movementVector != inputs._movementVector ? _movementVector : null;
+ ushort? joystickVector = _joystickVector != inputs._joystickVector ? _joystickVector : null;
+ ReadOnlyMemory? additionalInputs = !ByteArraysEqual(_additionalInputs, inputs._additionalInputs) ? _additionalInputs : null;
+
+ // If any of these are different we need to send both
+ bool mouseVectorDiff = _mouseVectorX != inputs._mouseVectorX || _mouseVectorY != inputs._mouseVectorY;
+ ushort? mouseVectorX = mouseVectorDiff ? _mouseVectorX : null;
+ ushort? mouseVectorY = mouseVectorDiff ? _mouseVectorY : null;
+
+ // If no differences exist, return null
+ return serverTickOffset is null && movementVector is null && joystickVector is null &&
+ mouseVectorX is null && mouseVectorY is null && additionalInputs is null
+ ? null
+ : new NetworkPlayerInputs(
+ tickNumber: _tickNumber,
+ serverTickOffset: serverTickOffset,
+ movementVector: movementVector,
+ joystickVector: joystickVector,
+ mouseVectorX: mouseVectorX,
+ mouseVectorY: mouseVectorY,
+ additionalInputs: additionalInputs
+ );
+ }
+
+ ///
+ /// Creates a new NetworkPlayerInputs instance by overriding non-null fields from the given `overrides` instance.
+ /// Fields that are null in `overrides` remain unchanged from the current instance.
+ ///
+ /// The NetworkPlayerInputs instance providing the overriding values.
+ /// A new NetworkPlayerInputs instance with fields overridden by non-null values from `overrides`.
+ public NetworkPlayerInputs OverrideWith(NetworkPlayerInputs overrides) => new(
+ tickNumber: overrides._tickNumber ?? _tickNumber,
+ serverTickOffset: overrides._serverTickOffset ?? _serverTickOffset,
+ movementVector: overrides._movementVector ?? _movementVector,
+ joystickVector: overrides._joystickVector ?? _joystickVector,
+ mouseVectorX: overrides._mouseVectorX ?? _mouseVectorX,
+ mouseVectorY: overrides._mouseVectorY ?? _mouseVectorY,
+ additionalInputs: overrides._additionalInputs ?? _additionalInputs
+ );
+
+ #endregion
+
+ /* Tick Number Handling */
+
+ #region Tick Number Handling
+
+ // Stores the validated tick number for the player's inputs.
+ private int? _tickNumber;
+
+ /// Represents the tick number for the player's inputs, validated to be within the range [0, 2047].
+ /// Thrown if the tick number is outside the range [0, 2047].
+ public int? TickNumber {
+ get => _tickNumber;
+ set => _tickNumber = value is null or < 0 or > 2047
+ ? throw new ArgumentOutOfRangeException(nameof(value), $"Invalid TickNumber: {value}. It must be between 0 and 2047.")
+ : value % 2048;
+ }
+
+ #endregion
+
+ /* Server Tick Offset handling */
+
+ #region Server Tick Offset handling
+
+ // Stores the server tick offset as a byte, representing a value between 0 and 255.
+ private readonly byte? _serverTickOffset;
+
+ /// Represents the server tick offset, ensuring it is within the range of 0 to 255. Throws an exception if the value is out of range.
+ /// Thrown if the X or Y components are outside the range [-1, 1].
+ public readonly int? ServerTickOffset {
+ get => _serverTickOffset;
+ init => _serverTickOffset = value is null
+ ? _serverTickOffset
+ : value is < 0 or > 255
+ ? throw new ArgumentOutOfRangeException(nameof(value), $"Invalid ServerTickOffset: {value}. It must be between 0 and 255.")
+ : (byte)value;
+ }
+
+ #endregion
+
+ /* Movement Vector2 Normals handling */
+
+ #region Movement Vector2 Normals handling
+
+ // Stores the movement input as a serialized Vector2.
+ private readonly ushort? _movementVector;
+
+ /// Represents the movement input as a Vector2 with components clamped to the range [-1, 1].
+ /// Thrown if the X or Y components are outside the range [-1, 1].
+ public readonly Vector2? MovementVector {
+ get => _movementVector is not null
+ ? DecompressUshortNormalsToVector2(_movementVector.Value)
+ : null;
+ init => _movementVector = value is null
+ ? _movementVector
+ : value.Value.x is < -1 or > 1 || value.Value.y is < -1 or > 1
+ ? throw new ArgumentOutOfRangeException(nameof(value), $"Invalid MovementVector: {value}. Components must be between -1 and 1.")
+ : CompressVector2NormalsToUshort(value.Value);
+ }
+
+ #endregion
+
+ /* Joystick Vector2 Normals handling */
+
+ #region Joystick Vector2 Normals handling
+
+ // Stores the joystick vector as a normalized Vector2.
+ private readonly ushort? _joystickVector;
+
+ /// Represents the joystick input as a normalized Vector2 with components in the range [-1, 1].
+ /// Thrown if the vector components are outside the range [-1, 1].
+ public readonly Vector2? JoystickVector {
+ get => _joystickVector is not null
+ ? DecompressUshortNormalsToVector2(_joystickVector.Value)
+ : null;
+ init => _joystickVector = value is null
+ ? _joystickVector
+ : value.Value.x is < -1 or > 1 || value.Value.y is < -1 or > 1
+ ? throw new ArgumentOutOfRangeException(nameof(value), $"Invalid JoystickVector: {value}. Components must be between -1 and 1.")
+ : CompressVector2NormalsToUshort(value.Value);
+ }
+
+ #endregion
+
+ /* Mouse Vector2 handling */
+
+ #region Mouse Vector2 handling
+
+ // Stores the mouse vector components as half-precision floats for efficient storage.
+ private readonly ushort? _mouseVectorX;
+ private readonly ushort? _mouseVectorY;
+
+ /// Represents the mouse input as a Vector2, with components stored as half-precision floats.
+ public readonly Vector2? MouseVector {
+ get => _mouseVectorX.HasValue && _mouseVectorY.HasValue
+ ? new Vector2(Mathf.HalfToFloat(_mouseVectorX.Value), Mathf.HalfToFloat(_mouseVectorY.Value))
+ : null;
+ init {
+ _mouseVectorX = value is null ? _mouseVectorX : Mathf.FloatToHalf(value.Value.x);
+ _mouseVectorY = value is null ? _mouseVectorY : Mathf.FloatToHalf(value.Value.y);
+ }
+ }
+
+ #endregion
+
+ /* Additional inputs handling */
+
+ #region Additional inputs handling
+
+ //Stores additional inputs defined by the developer, allowing custom byte data.
+ private ReadOnlyMemory? _additionalInputs;
+
+ /// Stores additional inputs defined by the developer, allowing custom byte data. Empty byte arrays are not allowed.
+ /// Thrown if the byte array is empty.
+ public ReadOnlyMemory? AdditionalInputs {
+ get => _additionalInputs;
+ init => _additionalInputs = value is null
+ ? _additionalInputs
+ : value is { Length: 0 }
+ ? throw new ArgumentException("AdditionalInputs cannot be an empty byte array.", nameof(value))
+ : value;
+ }
+
+ #endregion
+
+ /* Serialization and Deserialization */
+
+ #region Serialization and DeserializatioMyRegion
+
+ /// Serializes the NetworkPlayerInputs by encoding presence of inputs in a 16-bit header and writes relevant fields conditionally.
+ /// The NetworkWriter to write to.
+ /// The NetworkPlayerInputs to serialize.
+ public static void WriteNetworkPlayerInputs(NetworkWriter writer, NetworkPlayerInputs inputs) {
+ // Ensure tick number is set; otherwise, we may send the wrong tick number here
+ if (inputs._tickNumber is null)
+ throw new InvalidOperationException("TickNumber must be set before serialization.");
+
+ // Create header of 16 bits ( 5 bits for payload and 11 bits for the tick number )
+ ushort header = 0;
+ // First 5 bits represent the presence (null or non-null) of specific inputs
+ if (inputs._serverTickOffset is not null) header |= (1 << 0); // Bit 0: ServerTickOffset presence
+ if (inputs._movementVector is not null) header |= (1 << 1); // Bit 1: MovementVector presence
+ if (inputs._joystickVector is not null) header |= (1 << 2); // Bit 2: JoystickVector presence
+ if (inputs._mouseVectorX is not null && inputs._mouseVectorY is not null) header |= (1 << 3); // Bit 3: MouseVector presence
+ if (inputs._additionalInputs is not null) header |= (1 << 4); // Bit 4: AdditionalInputs non-empty
+
+ // Next 11 bits represent the tick number (masking to ensure only lower 11 bits are used)
+ header |= (ushort)((inputs._tickNumber & 0x7FF) << 5);
+
+ //Write header first
+ writer.WriteUShort(header);
+
+ // Write server tick offset if its not null
+ if (inputs._serverTickOffset is not null)
+ writer.WriteByte(inputs._serverTickOffset.Value);
+
+ // Write compressed movement vector if its not null
+ if (inputs._movementVector is not null)
+ writer.WriteUShort(inputs._movementVector.Value);
+
+ // Write compressed joystick vector if its not null
+ if (inputs._joystickVector is not null)
+ writer.WriteUShort(inputs._joystickVector.Value);
+
+ // Write Half (fp16) mouse vector if its not null
+ if (inputs._mouseVectorX is not null && inputs._mouseVectorY is not null) {
+ writer.WriteUShort(inputs._mouseVectorX.Value);
+ writer.WriteUShort(inputs._mouseVectorY.Value);
+ }
+
+ // Write additional inputs bytes
+ if (inputs._additionalInputs is not null)
+ writer.WriteBytesAndSize(inputs._additionalInputs.Value.ToArray(), 0, inputs._additionalInputs.Value.Length);
+ }
+
+
+ /// Deserializes NetworkPlayerInputs by reading a 16-bit header to determine which fields are present and reads them conditionally.
+ /// The NetworkReader to read from.
+ /// A deserialized instance of NetworkPlayerInputs.
+ public static NetworkPlayerInputs ReadNetworkPlayerInputs(NetworkReader reader) {
+ // Read the header first
+ ushort header = reader.ReadUShort();
+
+ // Extract presence bits from the header
+ bool hasServerTickOffset = (header & (1 << 0)) != 0; // Bit 0: ServerTickOffset presence
+ bool hasMovementVector = (header & (1 << 1)) != 0; // Bit 1: MovementVector presence
+ bool hasJoystickVector = (header & (1 << 2)) != 0; // Bit 2: JoystickVector presence
+ bool hasMouseVector = (header & (1 << 3)) != 0; // Bit 3: MouseVector presence
+ bool hasAdditionalInputs = (header & (1 << 4)) != 0; // Bit 4: AdditionalInputs non-empty
+
+ // Extract the tick number from the header (last 11 bits)
+ int tickNumber = (header >> 5) & 0x7FF;
+
+ // Initialize fields
+ byte? serverTickOffset = hasServerTickOffset ? reader.ReadByte() : null;
+ ushort? movementVector = hasMovementVector ? reader.ReadUShort() : null;
+ ushort? joystickVector = hasJoystickVector ? reader.ReadUShort() : null;
+ ushort? mouseVectorX = hasMouseVector ? reader.ReadUShort() : null;
+ ushort? mouseVectorY = hasMouseVector ? reader.ReadUShort() : null;
+
+ // Compiler nonsense requires me to use explicit if-else to avoid getting byte[0] instead of null
+ ReadOnlyMemory? additionalInputs;
+ if (hasAdditionalInputs)
+ additionalInputs = new ReadOnlyMemory(reader.ReadBytesAndSize());
+ else
+ additionalInputs = null;
+
+ // Construct and return the NetworkPlayerInputs object
+ return new NetworkPlayerInputs(
+ tickNumber: tickNumber,
+ serverTickOffset: serverTickOffset,
+ movementVector: movementVector,
+ joystickVector: joystickVector,
+ mouseVectorX: mouseVectorX,
+ mouseVectorY: mouseVectorY,
+ additionalInputs: additionalInputs
+ );
+ }
+
+ #endregion
+
+
+ /* Utility functions */
+
+ #region Utility functions
+
+ /// Compares two byte arrays for equality, including handling null values.
+ /// The first ReadOnlyMemory? to compare, can be null.
+ /// The second ReadOnlyMemory? to compare, can be null.
+ /// Comparison by value
+ private static bool ByteArraysEqual(in ReadOnlyMemory? byteArray1, in ReadOnlyMemory? byteArray2) {
+ // If both are null, they are equal
+ if (byteArray1 == null && byteArray2 == null) return true;
+
+ // If one is null but not the other, they are not equal
+ if (byteArray1 == null || byteArray2 == null) return false;
+
+ return byteArray1.Value.Span.SequenceEqual(byteArray2.Value.Span);
+ }
+
+ /// Decompresses a back into normalized Vector2 values (X and Y).
+ /// The compressed value.
+ /// A tuple containing the normalized Vector2 values in the range [-1, 1].
+ public static Vector2 DecompressUshortNormalsToVector2(ushort compressedValue) {
+ // Extract byteX and byteY from the compressed ushort
+ byte byteX = (byte)(compressedValue >> 8);
+ byte byteY = (byte)(compressedValue & 0xFF);
+ // Convert byte values back to normalized Vector2 in the range [-1, 1]
+ return new Vector2() { x = (byteX / 127f) - 1f, y = (byteY / 127f) - 1f };
+ }
+
+
+ /// Compresses normalized Vector2 values (X and Y) into a single .
+ /// The normalized X and Y axis values in the range [-1, 1].
+ /// A representing the compressed Vector2 values.
+ public static ushort CompressVector2NormalsToUshort(Vector2 vector) {
+ // Scale and shift values from [-1, 1] to [0, 254]
+ byte byteX = (byte)((Mathf.Clamp(vector.x, -1f, 1f) + 1f) * 127f);
+ byte byteY = (byte)((Mathf.Clamp(vector.y, -1f, 1f) + 1f) * 127f);
+ // Combine byteX and byteY into a single ushort
+ return (ushort)((byteX << 8) | byteY);
+ }
+
+ #endregion
+ }
+
+ // Serializers for the NetworkPlayerInputs Struct
+ public static class NetworkPlayerInputsSerializer{
+ public static void WriteNetworkPlayerInputs(this NetworkWriter writer, NetworkPlayerInputs value) {
+ NetworkPlayerInputs.WriteNetworkPlayerInputs(writer, value);
+ }
+
+ public static NetworkPlayerInputs ReadNetworkPlayerInputs(this NetworkReader reader) {
+ return NetworkPlayerInputs.ReadNetworkPlayerInputs(reader);
+ }
+
+ public static void WriteNetworkPlayerInputsListArray(this NetworkWriter writer, NetworkPlayerInputs[] value) {
+ if (value.Length > 255)
+ throw new ArgumentOutOfRangeException(nameof(value), "NetworkPlayerInputs[]: Max supported length is 255");
+ writer.WriteByte((byte)value.Length);
+ Array.ForEach(value, item => NetworkPlayerInputs.WriteNetworkPlayerInputs(writer, item));
+ }
+
+ public static NetworkPlayerInputs[] ReadNetworkPlayerInputsArray(this NetworkReader reader) =>
+ Enumerable.Range(0, reader.ReadByte())
+ .Select(_ => NetworkPlayerInputs.ReadNetworkPlayerInputs(reader))
+ .ToArray();
+
+ public static void WriteNetworkPlayerInputsListList(this NetworkWriter writer, List value) {
+ if (value.Count > 255)
+ throw new ArgumentOutOfRangeException(nameof(value), "List: Max supported length is 255");
+ writer.WriteByte((byte)value.Count);
+ value.ForEach(item => NetworkPlayerInputs.WriteNetworkPlayerInputs(writer, item));
+ }
+
+ public static List ReadNetworkPlayerInputsList(this NetworkReader reader) =>
+ Enumerable.Range(0, reader.ReadByte())
+ .Select(_ => NetworkPlayerInputs.ReadNetworkPlayerInputs(reader))
+ .ToList();
+ }
+}
diff --git a/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/NetworkPlayerInputs.cs.meta b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/NetworkPlayerInputs.cs.meta
new file mode 100644
index 0000000000..0a1347b33e
--- /dev/null
+++ b/Assets/Mirror/Components/Experimental/NetworkPlayerControllerBase/NetworkPlayerInputs.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 41be264d2f7f4f9085e02932602b2ff2
+timeCreated: 1733866224
\ No newline at end of file