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