diff --git a/LiteEntitySystem/ClientEntityManager.cs b/LiteEntitySystem/ClientEntityManager.cs index 1a6852e..a33317f 100644 --- a/LiteEntitySystem/ClientEntityManager.cs +++ b/LiteEntitySystem/ClientEntityManager.cs @@ -6,6 +6,7 @@ using LiteEntitySystem.Transport; using LiteEntitySystem.Collections; using LiteNetLib; +using LiteNetLib.Utils; namespace LiteEntitySystem { @@ -257,6 +258,86 @@ internal T AddLocalEntity(Action initMethod = null) where T : EntityLogic return entity; } + internal void SendServerRPC(ushort entityId, ushort rpcId, T value) where T : unmanaged + { + // Prepare a NetDataWriter + NetDataWriter writer = new NetDataWriter(); + + // Add packet header and type + writer.Put(HeaderByte); // Header byte for this entity system + writer.Put((byte)InternalPackets.ClientRPC); // Denote it's a client->server RPC + + // Add entity ID and RPC ID + writer.Put(entityId); + writer.Put(rpcId); + + unsafe + { + byte[] byteArray = new byte[sizeof(T)]; + fixed (byte* byteArrayPtr = byteArray) + { + Buffer.MemoryCopy(&value, byteArrayPtr, sizeof(T), sizeof(T)); + } + + writer.Put(byteArray); + } + + _netPeer.SendReliableOrdered(writer.AsReadOnlySpan()); + } + + // For "RemoteCallSpan" + internal void SendServerRPC(ushort entityId, ushort rpcId, ReadOnlySpan data) where T : unmanaged + { + // Prepare a NetDataWriter + NetDataWriter writer = new NetDataWriter(); + + // Add packet header and type + writer.Put(HeaderByte); + writer.Put(InternalPackets.ClientRPC); + + // Add entity ID and RPC ID + writer.Put(entityId); + writer.Put(rpcId); + + byte[] byteArray; + // Convert span to byte array + unsafe + { + byteArray = new byte[data.Length * sizeof(T)]; + fixed (T* dataPtr = data) + fixed (byte* bytePtr = byteArray) + { + Buffer.MemoryCopy(dataPtr, bytePtr, byteArray.Length, byteArray.Length); + } + } + + // Write the byte array + writer.Put(byteArray); + + // Send the data + _netPeer.SendReliableOrdered(writer.AsReadOnlySpan()); + } + + // For "RemoteCallSerializable" + internal void SendServerRPC(ushort entityId, ushort rpcId, ReadOnlySpan data) + { + // Prepare a NetDataWriter + NetDataWriter writer = new NetDataWriter(); + + // Add packet header and type + writer.Put(HeaderByte); + writer.Put(InternalPackets.ClientRPC); + + // Add entity ID and RPC ID + writer.Put(entityId); + writer.Put(rpcId); + + // Convert span to byte array and write + writer.Put(data.ToArray()); + + // Send the data + _netPeer.SendReliableOrdered(writer.AsReadOnlySpan()); + } internal EntityLogic FindEntityByPredictedId(ushort tick, ushort parentId, ushort predictedId) { diff --git a/LiteEntitySystem/Internal/InternalEntity.cs b/LiteEntitySystem/Internal/InternalEntity.cs index 9ea3e56..6be1170 100644 --- a/LiteEntitySystem/Internal/InternalEntity.cs +++ b/LiteEntitySystem/Internal/InternalEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using LiteNetLib.Utils; namespace LiteEntitySystem.Internal { @@ -10,7 +11,7 @@ public readonly struct EntityDataHeader public readonly ushort ClassId; public readonly byte Version; public readonly int UpdateOrder; - + public EntityDataHeader(ushort id, ushort classId, byte version, int updateOrder) { Id = id; @@ -19,27 +20,27 @@ public EntityDataHeader(ushort id, ushort classId, byte version, int updateOrder UpdateOrder = updateOrder; } } - + public abstract class InternalEntity : InternalBaseClass, IComparable { [SyncVarFlags(SyncFlags.NeverRollBack)] internal SyncVar InternalOwnerId; - + internal byte[] IOBuffer; internal readonly int UpdateOrderNum; - + /// /// Entity class id /// public readonly ushort ClassId; - + /// /// Entity instance id /// public readonly ushort Id; - + /// /// Entity manager /// @@ -49,7 +50,7 @@ public abstract class InternalEntity : InternalBaseClass, IComparable public bool IsServer => EntityManager.IsServer; - + /// /// Is entity on server /// @@ -67,10 +68,10 @@ public abstract class InternalEntity : InternalBaseClass, IComparable _isDestroyed; - + /// /// Is entity is destroyed /// @@ -85,22 +86,22 @@ public abstract class InternalEntity : InternalBaseClass, IComparable public bool IsRemoteControlled => InternalOwnerId.Value != EntityManager.InternalPlayerId; - + /// /// Is entity is controlled by server /// public bool IsServerControlled => InternalOwnerId.Value == EntityManager.ServerPlayerId; - + /// /// ClientEntityManager that available only on client. Will throw exception if called on server /// public ClientEntityManager ClientManager => (ClientEntityManager)EntityManager; - + /// /// ServerEntityManager that available only on server. Will throw exception if called on client /// public ServerEntityManager ServerManager => (ServerEntityManager)EntityManager; - + /// /// Owner player id /// ServerPlayerId - 0 @@ -117,7 +118,7 @@ public abstract class InternalEntity : InternalBaseClass, IComparable public bool IsSingleton => ClassData.IsSingleton; - + internal ref EntityClassData ClassData => ref EntityManager.ClassDataDict[ClassId]; /// @@ -129,7 +130,7 @@ public void Destroy() return; DestroyInternal(); } - + private void OnDestroyChange(bool prevValue) { if (!prevValue && _isDestroyed) @@ -144,7 +145,6 @@ private void OnDestroyChange(bool prevValue) /// protected virtual void OnDestroy() { - } internal virtual void DestroyInternal() @@ -165,7 +165,7 @@ internal void SafeUpdate() catch (Exception e) { Logger.LogError($"Exception in entity({Id}) update:\n{e}"); - } + } } /// @@ -174,13 +174,12 @@ internal void SafeUpdate() protected internal virtual void Update() { } - + /// /// Called at rollback begin before all values reset to first frame in rollback queue. /// protected internal virtual void OnBeforeRollback() { - } /// @@ -188,7 +187,6 @@ protected internal virtual void OnBeforeRollback() /// protected internal virtual void OnRollback() { - } /// @@ -196,7 +194,6 @@ protected internal virtual void OnRollback() /// protected internal virtual void VisualUpdate() { - } /// @@ -209,7 +206,7 @@ protected internal virtual void OnConstructed() internal void RegisterRpcInternal() { ref var classData = ref EntityManager.ClassDataDict[ClassId]; - + //setup field ids for BindOnChange and pass on server this for OnChangedEvent to StateSerializer var onChangeTarget = EntityManager.IsServer && !IsLocal ? this : null; for (int i = 0; i < classData.FieldsCount; i++) @@ -222,18 +219,20 @@ internal void RegisterRpcInternal() else { var syncableField = RefMagic.RefFieldValue(this, field.Offset); - field.TypeProcessor.InitSyncVar(syncableField, field.SyncableSyncVarOffset, onChangeTarget, (ushort)i); + field.TypeProcessor.InitSyncVar(syncableField, field.SyncableSyncVarOffset, onChangeTarget, + (ushort)i); } } - + List rpcCahce = null; - if(classData.RemoteCallsClient == null) + if (classData.RemoteCallsClient == null) { rpcCahce = new List(); var rpcRegistrator = new RPCRegistrator(rpcCahce, classData.Fields); RegisterRPC(ref rpcRegistrator); //Logger.Log($"RegisterRPCs for class: {classData.ClassId}"); } + //setup id for later sync calls for (int i = 0; i < classData.SyncableFields.Length; i++) { @@ -258,11 +257,11 @@ internal void RegisterRpcInternal() syncField.RegisterRPC(ref syncablesRegistrator); } } + classData.RemoteCallsClient ??= rpcCahce.ToArray(); } - /// /// Method for registering RPCs and OnChange notifications /// @@ -271,7 +270,7 @@ protected virtual void RegisterRPC(ref RPCRegistrator r) { r.BindOnChange(this, ref _isDestroyed, OnDestroyChange); } - + protected void ExecuteRPC(in RemoteCall rpc) { if (IsServer) @@ -295,8 +294,25 @@ protected void ExecuteRPC(in RemoteCall rpc, T value) where T : unmanaged ServerManager.AddRemoteCall(this, new ReadOnlySpan(&value, 1), rpc.Id, rpc.Flags); } } - else if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnPrediction) && IsLocalControlled) - rpc.CachedAction(this, value); + else // we are a client + { + // check if we want to run local prediction + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnPrediction) && IsLocalControlled) + { + rpc.CachedAction(this, value); + } + + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnServer)) + { + if (EntityManager is ClientEntityManager cem) + { + unsafe + { + cem.SendServerRPC(Id, rpc.Id, new ReadOnlySpan(&value, 1)); + } + } + } + } } protected void ExecuteRPC(in RemoteCallSpan rpc, ReadOnlySpan value) where T : unmanaged @@ -307,8 +323,27 @@ protected void ExecuteRPC(in RemoteCallSpan rpc, ReadOnlySpan value) wh rpc.CachedAction(this, value); ServerManager.AddRemoteCall(this, value, rpc.Id, rpc.Flags); } - else if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnPrediction) && IsLocalControlled) - rpc.CachedAction(this, value); + else + { + // If client and ExecuteOnPrediction + local-controlled, call locally + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnPrediction) && IsLocalControlled) + rpc.CachedAction(this, value); + + // If this call is meant to execute on server... + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnServer)) + { + // ... we manually serialize and send to the server + if (EntityManager is ClientEntityManager cem) + { + // Send the serialized bytes to the server + cem.SendServerRPC( + Id, + rpc.Id, + value + ); + } + } + } } protected void ExecuteRPC(in RemoteCallSerializable rpc, T value) where T : struct, ISpanSerializable @@ -321,8 +356,68 @@ protected void ExecuteRPC(in RemoteCallSerializable rpc, T value) where T value.Serialize(ref writer); ServerManager.AddRemoteCall(this, writer.RawData.Slice(0, writer.Position), rpc.Id, rpc.Flags); } - else if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnPrediction) && IsLocalControlled) - rpc.CachedAction(this, value); + else + { + // If client and ExecuteOnPrediction + local-controlled, call locally + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnPrediction) && IsLocalControlled) + rpc.CachedAction(this, value); + + // If this call is meant to execute on server... + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnServer)) + { + // ... we manually serialize and send to the server + if (EntityManager is ClientEntityManager cem) + { + SpanWriter writer = new SpanWriter(stackalloc byte[value.MaxSize]); + value.Serialize(ref writer); + + // Send the serialized bytes to the server + cem.SendServerRPC( + Id, + rpc.Id, + writer.RawData.Slice(0, writer.Position) + ); + } + } + } + } + + + protected void ExecuteRPC(in RemoteCallNetSerializable rpc, T value) where T : INetSerializable, new() + { + if (IsServer) + { + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnServer)) + rpc.CachedAction(this, value); + + var writer = new NetDataWriter(); + value.Serialize(writer); + ServerManager.AddRemoteCall(this, writer.Data, rpc.Id, rpc.Flags); + } + else + { + // If client and ExecuteOnPrediction + local-controlled, call locally + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnPrediction) && IsLocalControlled) + rpc.CachedAction(this, value); + + // If this call is meant to execute on server... + if (rpc.Flags.HasFlagFast(ExecuteFlags.ExecuteOnServer)) + { + // ... we manually serialize and send to the server + if (EntityManager is ClientEntityManager cem) + { + var writer = new NetDataWriter(); + value.Serialize(writer); + + // Send the serialized bytes to the server + cem.SendServerRPC( + Id, + rpc.Id, + writer.CopyData() + ); + } + } + } } protected InternalEntity(EntityParams entityParams) @@ -336,7 +431,9 @@ protected InternalEntity(EntityParams entityParams) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int CompareTo(InternalEntity other) => UpdateOrderNum != other.UpdateOrderNum ? UpdateOrderNum - other.UpdateOrderNum : Id - other.Id; + public int CompareTo(InternalEntity other) => UpdateOrderNum != other.UpdateOrderNum + ? UpdateOrderNum - other.UpdateOrderNum + : Id - other.Id; public override int GetHashCode() => UpdateOrderNum; diff --git a/LiteEntitySystem/Internal/InternalPackets.cs b/LiteEntitySystem/Internal/InternalPackets.cs index 0859d70..43709f8 100644 --- a/LiteEntitySystem/Internal/InternalPackets.cs +++ b/LiteEntitySystem/Internal/InternalPackets.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace LiteEntitySystem.Internal { @@ -47,5 +47,6 @@ internal static class InternalPackets public const byte BaselineSync = 3; public const byte DiffSyncLast = 4; public const byte ClientRequest = 5; + public const byte ClientRPC = 6; } } \ No newline at end of file diff --git a/LiteEntitySystem/RPCRegistrator.cs b/LiteEntitySystem/RPCRegistrator.cs index 088fac4..dad3c35 100644 --- a/LiteEntitySystem/RPCRegistrator.cs +++ b/LiteEntitySystem/RPCRegistrator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using LiteEntitySystem.Internal; +using LiteNetLib.Utils; namespace LiteEntitySystem { @@ -69,6 +70,31 @@ internal RemoteCallSpan(SpanAction action, ushort rpcId, Exec } } + public readonly struct RemoteCallNetSerializable where T : INetSerializable, new() + { + internal static MethodCallDelegate CreateMCD(Action methodToCall) => + (classPtr, buffer) => + { + var t = new T(); + var dataReader = new NetDataReader(buffer.ToArray()); + t.Deserialize(dataReader); + methodToCall((TClass)classPtr, t); + }; + + internal readonly Action CachedAction; + internal readonly ushort Id; + internal readonly ExecuteFlags Flags; + internal readonly bool Initialized; + + internal RemoteCallNetSerializable(Action action, ushort rpcId, ExecuteFlags flags) + { + CachedAction = action; + Id = rpcId; + Flags = flags; + Initialized = true; + } + } + public readonly struct RemoteCallSerializable where T : struct, ISpanSerializable { internal static MethodCallDelegate CreateMCD(Action methodToCall) => @@ -110,7 +136,7 @@ internal static void CheckTarget(object ent, object target) if (ent != target) throw new Exception("You can call this only on this class methods"); } - + /// /// Bind notification of SyncVar changes to action /// @@ -137,6 +163,50 @@ public void BindOnChange(TEntity self, ref SyncVar syncVar, Actio BindOnChange(ref syncVar, onChangedAction.Method.CreateDelegateHelper>(), executionOrder); } + + public void CreateRPCAction( + TEntity self, + Action methodToCall, + ref RemoteCallNetSerializable remoteCallHandle, + ExecuteFlags flags + ) + where T : INetSerializable, new() + where TEntity : InternalEntity + { + CheckTarget(self, methodToCall.Target); + + if (!remoteCallHandle.Initialized) + { + var finalFlags = flags | ExecuteFlags.ExecuteOnServer; + remoteCallHandle = new RemoteCallNetSerializable( + (e, val) => methodToCall.Method.CreateDelegateHelper>()((TEntity)e, val), + (ushort)_calls.Count, + finalFlags + ); + } + + _calls.Add(new RpcFieldInfo(RemoteCallNetSerializable.CreateMCD( + methodToCall.Method.CreateDelegateHelper>() + ))); + } + + public void CreateRPCAction( + Action methodToCall, + ref RemoteCallNetSerializable remoteCallHandle, + ExecuteFlags flags + ) + where T : INetSerializable, new() + where TEntity : InternalEntity + { + if (!remoteCallHandle.Initialized) + remoteCallHandle = new RemoteCallNetSerializable( + (e, v) => methodToCall((TEntity)e, v), + (ushort)_calls.Count, + flags + ); + _calls.Add(new RpcFieldInfo(RemoteCallNetSerializable.CreateMCD(methodToCall))); + } + /// /// Creates cached rpc action /// diff --git a/LiteEntitySystem/ServerEntityManager.cs b/LiteEntitySystem/ServerEntityManager.cs index 9a62d2c..b50c626 100644 --- a/LiteEntitySystem/ServerEntityManager.cs +++ b/LiteEntitySystem/ServerEntityManager.cs @@ -22,27 +22,30 @@ public enum ServerSendRate : byte public sealed class ServerEntityManager : EntityManager { public const int MaxStoredInputs = 30; - + private readonly IdGeneratorUShort _entityIdQueue = new(1, MaxSyncedEntityCount); private readonly IdGeneratorByte _playerIdQueue = new(1, MaxPlayers); private readonly Queue _rpcPool = new(); private readonly Queue _pendingClientRequests = new(); - private byte[] _packetBuffer = new byte[(MaxParts+1) * NetConstants.MaxPacketSize + StateSerializer.MaxStateSize]; - private readonly SparseMap _netPlayers = new (MaxPlayers+1); + + private byte[] _packetBuffer = + new byte[(MaxParts + 1) * NetConstants.MaxPacketSize + StateSerializer.MaxStateSize]; + + private readonly SparseMap _netPlayers = new(MaxPlayers + 1); private readonly StateSerializer[] _stateSerializers = new StateSerializer[MaxSyncedEntityCount]; private readonly byte[] _inputDecodeBuffer = new byte[NetConstants.MaxUnreliableDataSize]; private readonly NetDataReader _requestsReader = new(); - + //use entity filter for correct sort (id+version+creationTime) private readonly AVLTree _changedEntities = new(); - + private byte[] _compressionBuffer = new byte[4096]; - + /// /// Network players count /// public int PlayersCount => _netPlayers.Count; - + /// /// Rate at which server will make and send packets /// @@ -54,7 +57,7 @@ public sealed class ServerEntityManager : EntityManager public bool SafeEntityUpdate = false; private ushort _minimalTick; - + private int _nextOrderNum; /// @@ -66,11 +69,11 @@ public sealed class ServerEntityManager : EntityManager /// Send rate of server (depends on fps) /// Maximum size of lag compensation history in ticks public ServerEntityManager( - EntityTypesMap typesMap, - byte packetHeader, + EntityTypesMap typesMap, + byte packetHeader, byte framesPerSecond, ServerSendRate sendRate, - MaxHistorySize maxHistorySize = MaxHistorySize.Size32) + MaxHistorySize maxHistorySize = MaxHistorySize.Size32) : base(typesMap, NetworkMode.Server, packetHeader, maxHistorySize) { InternalPlayerId = ServerPlayerId; @@ -100,9 +103,10 @@ public NetPlayer AddPlayer(AbstractNetPeer peer) Logger.LogWarning("Peer already has an assigned player"); return peer.AssignedPlayer; } + if (_netPlayers.Count == 0) _changedEntities.Clear(); - + var player = new NetPlayer(peer, _playerIdQueue.GetNewId()) { State = NetPlayerState.RequestBaseline, @@ -128,7 +132,7 @@ public NetPlayer GetPlayer(byte ownerId) => /// true if player removed successfully, false if player not found public bool RemovePlayer(AbstractNetPeer player) => RemovePlayer(player.AssignedPlayer); - + /// /// Remove player and it's owned entities /// @@ -153,7 +157,7 @@ public bool RemovePlayer(NetPlayer player) /// Instance if found, null if not public HumanControllerLogic GetPlayerController(AbstractNetPeer player) => GetPlayerController(player.AssignedPlayer); - + /// /// Returns controller owned by the player /// @@ -161,7 +165,7 @@ public HumanControllerLogic GetPlayerController(AbstractNetPeer player) => /// Instance if found, null if not public HumanControllerLogic GetPlayerController(byte playerId) => GetPlayerController(_netPlayers.TryGetValue(playerId, out var p) ? p : null); - + /// /// Returns controller owned by the player /// @@ -176,6 +180,7 @@ public HumanControllerLogic GetPlayerController(NetPlayer player) if (controller.InternalOwnerId.Value == player.Id) return controller; } + return null; } @@ -192,7 +197,7 @@ public T AddController(NetPlayer owner, Action initMethod = null) where T ent.InternalOwnerId.Value = owner.Id; initMethod?.Invoke(ent); }); - + /// /// Add new player controller entity and start controlling entityToControl /// @@ -201,21 +206,22 @@ public T AddController(NetPlayer owner, Action initMethod = null) where T /// Method that will be called after entity construction /// Entity type /// Created entity or null in case of limit - public T AddController(NetPlayer owner, PawnLogic entityToControl, Action initMethod = null) where T : HumanControllerLogic => + public T AddController(NetPlayer owner, PawnLogic entityToControl, Action initMethod = null) + where T : HumanControllerLogic => Add(ent => { ent.InternalOwnerId.Value = owner.Id; ent.StartControl(entityToControl); initMethod?.Invoke(ent); }); - + /// /// Add new AI controller entity /// /// Method that will be called after entity construction /// Entity type /// Created entity or null in case of limit - public T AddAIController(Action initMethod = null) where T : AiControllerLogic => + public T AddAIController(Action initMethod = null) where T : AiControllerLogic => Add(initMethod); /// @@ -224,7 +230,7 @@ public T AddAIController(Action initMethod = null) where T : AiControllerL /// Method that will be called after entity construction /// Entity type /// Created entity or null in case of limit - public T AddSingleton(Action initMethod = null) where T : SingletonEntityLogic => + public T AddSingleton(Action initMethod = null) where T : SingletonEntityLogic => Add(initMethod); /// @@ -233,9 +239,9 @@ public T AddSingleton(Action initMethod = null) where T : SingletonEntityL /// Method that will be called after entity construction /// Entity type /// Created entity or null in case of limit - public T AddEntity(Action initMethod = null) where T : EntityLogic => + public T AddEntity(Action initMethod = null) where T : EntityLogic => Add(initMethod); - + /// /// Add new entity and set parent entity /// @@ -268,16 +274,16 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan if (inData[0] != HeaderByte) return DeserializeResult.HeaderCheckFailed; inData = inData.Slice(1); - + if (inData.Length < 3) { Logger.LogWarning($"Invalid data received. Length < 3: {inData.Length}"); return DeserializeResult.Error; } - + byte packetType = inData[0]; inData = inData.Slice(1); - + if (packetType == InternalPackets.ClientRequest) { if (inData.Length < HumanControllerLogic.MinRequestSize) @@ -285,10 +291,17 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan Logger.LogError("size less than minRequest"); return DeserializeResult.Error; } + _pendingClientRequests.Enqueue(inData.ToArray()); return DeserializeResult.Done; } - + + if (packetType == InternalPackets.ClientRPC) + { + HandleClientRPC(player, inData); + return DeserializeResult.Done; + } + if (packetType != InternalPackets.ClientInput) { Logger.LogWarning($"[SEM] Unknown packet type: {packetType}"); @@ -299,21 +312,21 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan int minDeltaSize = 0; foreach (var humanControllerLogic in GetEntities()) { - if(humanControllerLogic.OwnerId != player.Id) + if (humanControllerLogic.OwnerId != player.Id) continue; minInputSize += humanControllerLogic.InputSize; minDeltaSize += humanControllerLogic.MinInputDeltaSize; } - + ushort clientTick = BitConverter.ToUInt16(inData); inData = inData.Slice(2); bool isFirstInput = true; while (inData.Length >= InputPacketHeader.Size) { - var inputInfo = new InputInfo{ Tick = clientTick }; + var inputInfo = new InputInfo { Tick = clientTick }; fixed (byte* rawData = inData) inputInfo.Header = *(InputPacketHeader*)rawData; - + inData = inData.Slice(InputPacketHeader.Size); bool correctInput = player.State == NetPlayerState.WaitingForFirstInput || Utils.SequenceDiff(inputInfo.Tick, player.LastReceivedTick) > 0; @@ -337,17 +350,20 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan Logger.LogError($"Bad input from: {player.Id} - {player.Peer} too small input"); return DeserializeResult.Error; } + if (!isFirstInput && inData.Length < minDeltaSize) { Logger.LogError($"Bad input from: {player.Id} - {player.Peer} too small delta"); return DeserializeResult.Error; } + if (Utils.SequenceDiff(inputInfo.Header.StateA, Tick) > 0 || Utils.SequenceDiff(inputInfo.Header.StateB, Tick) > 0) { Logger.LogError($"Bad input from: {player.Id} - {player.Peer} invalid sequence"); return DeserializeResult.Error; } + inputInfo.Header.LerpMsec = Math.Clamp(inputInfo.Header.LerpMsec, 0f, 1f); if (Utils.SequenceDiff(inputInfo.Header.StateB, player.CurrentServerTick) > 0) player.CurrentServerTick = inputInfo.Header.StateB; @@ -360,15 +376,15 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan //read input foreach (var controller in GetEntities()) { - if(controller.OwnerId != player.Id) + if (controller.OwnerId != player.Id) continue; - - if(removedTick >= 0) + + if (removedTick >= 0) controller.RemoveIncomingInput((ushort)removedTick); - + //decode delta ReadOnlySpan actualData; - + if (!isFirstInput) //delta { var decodedData = new Span(_inputDecodeBuffer, 0, controller.InputSize); @@ -378,7 +394,7 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan inData = inData.Slice(readBytes); } else //full - { + { isFirstInput = false; actualData = inData.Slice(0, controller.InputSize); controller.DeltaDecodeInit(actualData); @@ -396,16 +412,41 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan player.LastReceivedTick = inputInfo.Tick; } } - if(player.State == NetPlayerState.WaitingForFirstInput) + + if (player.State == NetPlayerState.WaitingForFirstInput) player.State = NetPlayerState.WaitingForFirstInputProcess; return DeserializeResult.Done; } - + + private void HandleClientRPC(NetPlayer player, ReadOnlySpan inData) + { + NetDataReader reader = new NetDataReader(inData.ToArray()); + + // Read entity ID and RPC ID + ushort entityId = reader.GetUShort(); + ushort rpcId = reader.GetUShort(); + + // Get the remaining payload + ReadOnlySpan payload = reader.GetRemainingBytesSpan(); + + // Find the entity and execute the RPC + var entity = EntitiesDict[entityId]; + if (entity == null) + return; + + ref var classData = ref ClassDataDict[entity.ClassId]; + if (rpcId >= classData.RemoteCallsClient.Length) + return; + + var rpcInfo = classData.RemoteCallsClient[rpcId]; + rpcInfo.Method(entity, payload); + } + private T Add(Action initMethod) where T : InternalEntity { if (EntityClassInfo.ClassId == 0) throw new Exception($"Unregistered entity type: {typeof(T)}"); - + //create entity data and filters ref var classData = ref ClassDataDict[EntityClassInfo.ClassId]; if (_entityIdQueue.AvailableIds == 0) @@ -413,6 +454,7 @@ private T Add(Action initMethod) where T : InternalEntity Logger.Log($"Cannot add entity. Max entity count reached: {MaxSyncedEntityCount}"); return null; } + ushort entityId = _entityIdQueue.GetNewId(); ref var stateSerializer = ref _stateSerializers[entityId]; @@ -421,7 +463,7 @@ private T Add(Action initMethod) where T : InternalEntity var entity = AddEntity(new EntityParams( new EntityDataHeader( entityId, - classData.ClassId, + classData.ClassId, stateSerializer.NextVersion, ++_nextOrderNum), this, @@ -430,11 +472,11 @@ private T Add(Action initMethod) where T : InternalEntity initMethod?.Invoke(entity); ConstructEntity(entity); _changedEntities.Add(entity); - + //Debug.Log($"[SEM] Entity create. clsId: {classData.ClassId}, id: {entityId}, v: {version}"); return entity; } - + protected override unsafe void OnLogicTick() { //read pending client requests @@ -443,22 +485,23 @@ protected override unsafe void OnLogicTick() _requestsReader.SetSource(_pendingClientRequests.Dequeue()); ushort controllerId = _requestsReader.GetUShort(); byte controllerVersion = _requestsReader.GetByte(); - if (TryGetEntityById(new EntitySharedReference(controllerId, controllerVersion), out var controller)) + if (TryGetEntityById(new EntitySharedReference(controllerId, controllerVersion), + out var controller)) controller.ReadClientRequest(_requestsReader); } - + int playersCount = _netPlayers.Count; for (int pidx = 0; pidx < playersCount; pidx++) { var player = _netPlayers.GetByIndex(pidx); - if (player.State == NetPlayerState.RequestBaseline) + if (player.State == NetPlayerState.RequestBaseline) continue; if (player.AvailableInput.Count == 0) { //Logger.LogWarning($"Inputs of player {pidx} is zero"); continue; } - + var inputFrame = player.AvailableInput.ExtractMin(); ref var inputData = ref inputFrame.Header; player.LastProcessedTick = inputFrame.Tick; @@ -488,14 +531,14 @@ protected override unsafe void OnLogicTick() foreach (var aliveEntity in AliveEntities) aliveEntity.Update(); } - + foreach (var lagCompensatedEntity in LagCompensatedEntities) ClassDataDict[lagCompensatedEntity.ClassId].WriteHistory(lagCompensatedEntity, _tick); - + //================================================================== //Sending part //================================================================== - if (playersCount == 0 || _tick % (int) SendRate != 0) + if (playersCount == 0 || _tick % (int)SendRate != 0) return; //calculate minimalTick and potential baseline size @@ -505,7 +548,9 @@ protected override unsafe void OnLogicTick() { var player = _netPlayers.GetByIndex(pidx); if (player.State != NetPlayerState.RequestBaseline) - _minimalTick = Utils.SequenceDiff(player.StateATick, _minimalTick) < 0 ? player.StateATick : _minimalTick; + _minimalTick = Utils.SequenceDiff(player.StateATick, _minimalTick) < 0 + ? player.StateATick + : _minimalTick; else if (maxBaseline == 0) { maxBaseline = sizeof(BaselineDataHeader); @@ -513,7 +558,8 @@ protected override unsafe void OnLogicTick() maxBaseline += _stateSerializers[e.Id].GetMaximumSize(_tick); if (_packetBuffer.Length < maxBaseline) _packetBuffer = new byte[maxBaseline]; - int maxCompressedSize = LZ4Codec.MaximumOutputSize(_packetBuffer.Length) + sizeof(BaselineDataHeader); + int maxCompressedSize = + LZ4Codec.MaximumOutputSize(_packetBuffer.Length) + sizeof(BaselineDataHeader); if (_compressionBuffer.Length < maxCompressedSize) _compressionBuffer = new byte[maxCompressedSize]; } @@ -521,149 +567,158 @@ protected override unsafe void OnLogicTick() //make packets fixed (byte* packetBuffer = _packetBuffer, compressionBuffer = _compressionBuffer) - // ReSharper disable once BadChildStatementIndent - for (int pidx = 0; pidx < playersCount; pidx++) - { - var player = _netPlayers.GetByIndex(pidx); - if (player.State == NetPlayerState.RequestBaseline) + // ReSharper disable once BadChildStatementIndent + for (int pidx = 0; pidx < playersCount; pidx++) { - int originalLength = 0; - foreach (var e in GetEntities()) - _stateSerializers[e.Id].MakeBaseline(player.Id, _tick, packetBuffer, ref originalLength); - - //set header - *(BaselineDataHeader*)compressionBuffer = new BaselineDataHeader + var player = _netPlayers.GetByIndex(pidx); + if (player.State == NetPlayerState.RequestBaseline) { - UserHeader = HeaderByte, - PacketType = InternalPackets.BaselineSync, - OriginalLength = originalLength, - Tick = _tick, - PlayerId = player.Id, - SendRate = (byte)SendRate, - Tickrate = Tickrate - }; - - //compress - int encodedLength = LZ4Codec.Encode( - packetBuffer, - originalLength, - compressionBuffer + sizeof(BaselineDataHeader), - _compressionBuffer.Length - sizeof(BaselineDataHeader), - LZ4Level.L00_FAST); - - player.Peer.SendReliableOrdered(new ReadOnlySpan(_compressionBuffer, 0, sizeof(BaselineDataHeader) + encodedLength)); - player.StateATick = _tick; - player.CurrentServerTick = _tick; - player.State = NetPlayerState.WaitingForFirstInput; - Logger.Log($"[SEM] SendWorld to player {player.Id}. orig: {originalLength}, bytes, compressed: {encodedLength}, ExecutedTick: {_tick}"); - continue; - } - if (player.State != NetPlayerState.Active) - { - //waiting to load initial state - continue; - } + int originalLength = 0; + foreach (var e in GetEntities()) + _stateSerializers[e.Id].MakeBaseline(player.Id, _tick, packetBuffer, ref originalLength); - var playerController = GetPlayerController(player); - - //Partial diff sync - var header = (DiffPartHeader*)packetBuffer; - header->UserHeader = HeaderByte; - header->Part = 0; - header->Tick = _tick; - int writePosition = sizeof(DiffPartHeader); - - ushort maxPartSize = (ushort)(player.Peer.GetMaxUnreliablePacketSize() - sizeof(LastPartData)); - foreach (var entity in _changedEntities) - { - ref var stateSerializer = ref _stateSerializers[entity.Id]; - - //all players has actual state so remove from sync - if (Utils.SequenceDiff(stateSerializer.LastChangedTick, _minimalTick) <= 0) - { - //remove from changed list - _changedEntities.Remove(entity); - - //if entity destroyed - free it - if (entity.IsDestroyed) + //set header + *(BaselineDataHeader*)compressionBuffer = new BaselineDataHeader { - if (entity.UpdateOrderNum == _nextOrderNum) - { - //this was highest - _nextOrderNum = GetEntities().TryGetMax(out var highestEntity) - ? highestEntity.UpdateOrderNum - : 0; - //Logger.Log($"Removed highest order entity: {e.UpdateOrderNum}, new highest: {_nextOrderNum}"); - } - _entityIdQueue.ReuseId(entity.Id); - stateSerializer.Free(); - //Logger.Log($"[SRV] RemoveEntity: {e.Id}"); - - RemoveEntity(entity); - } + UserHeader = HeaderByte, + PacketType = InternalPackets.BaselineSync, + OriginalLength = originalLength, + Tick = _tick, + PlayerId = player.Id, + SendRate = (byte)SendRate, + Tickrate = Tickrate + }; + + //compress + int encodedLength = LZ4Codec.Encode( + packetBuffer, + originalLength, + compressionBuffer + sizeof(BaselineDataHeader), + _compressionBuffer.Length - sizeof(BaselineDataHeader), + LZ4Level.L00_FAST); + + player.Peer.SendReliableOrdered(new ReadOnlySpan(_compressionBuffer, 0, + sizeof(BaselineDataHeader) + encodedLength)); + player.StateATick = _tick; + player.CurrentServerTick = _tick; + player.State = NetPlayerState.WaitingForFirstInput; + Logger.Log( + $"[SEM] SendWorld to player {player.Id}. orig: {originalLength}, bytes, compressed: {encodedLength}, ExecutedTick: {_tick}"); continue; } - //skip known - if (Utils.SequenceDiff(stateSerializer.LastChangedTick, player.StateATick) <= 0) + + if (player.State != NetPlayerState.Active) + { + //waiting to load initial state continue; - - if (stateSerializer.MakeDiff( - player.Id, - _tick, - _minimalTick, - player.CurrentServerTick, - packetBuffer, - ref writePosition, - playerController)) + } + + var playerController = GetPlayerController(player); + + //Partial diff sync + var header = (DiffPartHeader*)packetBuffer; + header->UserHeader = HeaderByte; + header->Part = 0; + header->Tick = _tick; + int writePosition = sizeof(DiffPartHeader); + + ushort maxPartSize = (ushort)(player.Peer.GetMaxUnreliablePacketSize() - sizeof(LastPartData)); + foreach (var entity in _changedEntities) { - int overflow = writePosition - maxPartSize; - while (overflow > 0) + ref var stateSerializer = ref _stateSerializers[entity.Id]; + + //all players has actual state so remove from sync + if (Utils.SequenceDiff(stateSerializer.LastChangedTick, _minimalTick) <= 0) { - if (header->Part == MaxParts-1) + //remove from changed list + _changedEntities.Remove(entity); + + //if entity destroyed - free it + if (entity.IsDestroyed) { - Logger.Log($"P:{pidx} Request baseline {_tick}"); - player.State = NetPlayerState.RequestBaseline; - break; + if (entity.UpdateOrderNum == _nextOrderNum) + { + //this was highest + _nextOrderNum = GetEntities().TryGetMax(out var highestEntity) + ? highestEntity.UpdateOrderNum + : 0; + //Logger.Log($"Removed highest order entity: {e.UpdateOrderNum}, new highest: {_nextOrderNum}"); + } + + _entityIdQueue.ReuseId(entity.Id); + stateSerializer.Free(); + //Logger.Log($"[SRV] RemoveEntity: {e.Id}"); + + RemoveEntity(entity); } - header->PacketType = InternalPackets.DiffSync; - //Logger.LogWarning($"P:{pidx} Sending diff part {*partCount}: {_tick}"); - player.Peer.SendUnreliable(new ReadOnlySpan(packetBuffer, maxPartSize)); - header->Part++; - - //repeat in next packet - RefMagic.CopyBlock(packetBuffer + sizeof(DiffPartHeader), packetBuffer + maxPartSize, (uint)overflow); - writePosition = sizeof(DiffPartHeader) + overflow; - overflow = writePosition - maxPartSize; + + continue; + } + + //skip known + if (Utils.SequenceDiff(stateSerializer.LastChangedTick, player.StateATick) <= 0) + continue; + + if (stateSerializer.MakeDiff( + player.Id, + _tick, + _minimalTick, + player.CurrentServerTick, + packetBuffer, + ref writePosition, + playerController)) + { + int overflow = writePosition - maxPartSize; + while (overflow > 0) + { + if (header->Part == MaxParts - 1) + { + Logger.Log($"P:{pidx} Request baseline {_tick}"); + player.State = NetPlayerState.RequestBaseline; + break; + } + + header->PacketType = InternalPackets.DiffSync; + //Logger.LogWarning($"P:{pidx} Sending diff part {*partCount}: {_tick}"); + player.Peer.SendUnreliable(new ReadOnlySpan(packetBuffer, maxPartSize)); + header->Part++; + + //repeat in next packet + RefMagic.CopyBlock(packetBuffer + sizeof(DiffPartHeader), packetBuffer + maxPartSize, + (uint)overflow); + writePosition = sizeof(DiffPartHeader) + overflow; + overflow = writePosition - maxPartSize; + } + + //if request baseline break entity loop + if (player.State == NetPlayerState.RequestBaseline) + break; } - //if request baseline break entity loop - if(player.State == NetPlayerState.RequestBaseline) - break; + //else skip } - //else skip - } - - //if request baseline continue to other players - if(player.State == NetPlayerState.RequestBaseline) - continue; - //Debug.Log($"PARTS: {partCount} {_netDataWriter.Data[4]}"); - header->PacketType = InternalPackets.DiffSyncLast; - //put mtu at last packet - *(LastPartData*)(packetBuffer + writePosition) = new LastPartData - { - LastProcessedTick = player.LastProcessedTick, - LastReceivedTick = player.LastReceivedTick, - Mtu = maxPartSize, - BufferedInputsCount = (byte)player.AvailableInput.Count - }; - writePosition += sizeof(LastPartData); - player.Peer.SendUnreliable(new ReadOnlySpan(_packetBuffer, 0, writePosition)); - } + //if request baseline continue to other players + if (player.State == NetPlayerState.RequestBaseline) + continue; + + //Debug.Log($"PARTS: {partCount} {_netDataWriter.Data[4]}"); + header->PacketType = InternalPackets.DiffSyncLast; + //put mtu at last packet + *(LastPartData*)(packetBuffer + writePosition) = new LastPartData + { + LastProcessedTick = player.LastProcessedTick, + LastReceivedTick = player.LastReceivedTick, + Mtu = maxPartSize, + BufferedInputsCount = (byte)player.AvailableInput.Count + }; + writePosition += sizeof(LastPartData); + player.Peer.SendUnreliable(new ReadOnlySpan(_packetBuffer, 0, writePosition)); + } //trigger only when there is data _netPlayers.GetByIndex(0).Peer.TriggerSend(); } - + internal override void EntityFieldChanged(InternalEntity entity, ushort fieldId, ref T newValue) { if (entity.IsDestroyed && _stateSerializers[entity.Id].Entity != entity) @@ -671,10 +726,11 @@ internal override void EntityFieldChanged(InternalEntity entity, ushort field //old freed entity return; } + _changedEntities.Add(entity); _stateSerializers[entity.Id].MarkFieldChanged(fieldId, _tick, ref newValue); } - + internal void ForceEntitySync(InternalEntity entity) { _changedEntities.Add(entity); @@ -683,7 +739,7 @@ internal void ForceEntitySync(InternalEntity entity) internal void PoolRpc(RemoteCallPacket rpcNode) => _rpcPool.Enqueue(rpcNode); - + internal void AddRemoteCall(InternalEntity entity, ushort rpcId, ExecuteFlags flags) { if (PlayersCount == 0) @@ -693,8 +749,9 @@ internal void AddRemoteCall(InternalEntity entity, ushort rpcId, ExecuteFlags fl _stateSerializers[entity.Id].AddRpcPacket(rpc); _changedEntities.Add(entity); } - - internal unsafe void AddRemoteCall(InternalEntity entity, ReadOnlySpan value, ushort rpcId, ExecuteFlags flags) where T : unmanaged + + internal unsafe void AddRemoteCall(InternalEntity entity, ReadOnlySpan value, ushort rpcId, + ExecuteFlags flags) where T : unmanaged { if (PlayersCount == 0) return; @@ -705,12 +762,13 @@ internal unsafe void AddRemoteCall(InternalEntity entity, ReadOnlySpan val Logger.LogError($"DataSize on rpc: {rpcId}, entity: {entity} is more than {ushort.MaxValue}"); return; } + rpc.Init(_tick, (ushort)dataSize, rpcId, flags); - if(value.Length > 0) - fixed(void* rawValue = value, rawData = rpc.Data) + if (value.Length > 0) + fixed (void* rawValue = value, rawData = rpc.Data) RefMagic.CopyBlock(rawData, rawValue, (uint)dataSize); _stateSerializers[entity.Id].AddRpcPacket(rpc); _changedEntities.Add(entity); } } -} +} \ No newline at end of file