using System; using NUnit.Framework; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Netcode; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Simulation { public interface ISerializableEntity { void Serialize(FastBufferWriter writer); void Deserialize(FastBufferReader reader); // @NOTE: -1 means use the default size and have hope it will fit. int MinimumSizeInBytes() { return -1; } } public interface IActorData : ISerializableEntity { } public class NoActorData : IActorData { public void Serialize(FastBufferWriter writer) { } public void Deserialize(FastBufferReader reader) { } } public static class DataSerializationUtils { public const int k_DefaultMessageSize = 256; public static NativeArray Serialize(TEntity entity, Allocator allocator = Allocator.Temp) where TEntity : ISerializableEntity { int size = entity.MinimumSizeInBytes(); if (size < 0) { size = k_DefaultMessageSize; } using FastBufferWriter writer = new FastBufferWriter(size, allocator); if (writer.TryBeginWrite(size)) { entity.Serialize(writer); int length = writer.Length; NativeArray data = new NativeArray(length, allocator); unsafe { void* dst = data.GetUnsafePtr(); void* src = writer.GetUnsafePtr(); Buffer.MemoryCopy(src, dst, length, length); } return data; } return default; } public static void Deserialize(NativeArray data, ref TEntity entity) where TEntity : ISerializableEntity { using FastBufferReader reader = new FastBufferReader(data, Allocator.Temp); if (reader.TryBeginRead(data.Length)) { entity.Deserialize(reader); } } } public struct ActorCommand : INetworkSerializable { public ulong ActorID; public ulong ClientID; public ushort CommandID; public NativeArray Data; public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref ActorID); serializer.SerializeValue(ref ClientID); serializer.SerializeValue(ref CommandID); if (serializer.IsWriter) { bool hasData = Data.IsCreated; serializer.SerializeValue(ref hasData); if (hasData) { serializer.SerializeValue(ref Data, Allocator.Temp); } } else if (serializer.IsReader) { bool hasData = false; serializer.SerializeValue(ref hasData); if (hasData) { serializer.SerializeValue(ref Data, Allocator.Temp); } } } } public struct ActorEvent : INetworkSerializable { public ulong ActorID; public ulong ClientID; public ushort EventID; public NativeArray Data; public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref ActorID); serializer.SerializeValue(ref ClientID); serializer.SerializeValue(ref EventID); if (serializer.IsWriter) { bool hasData = Data.IsCreated; serializer.SerializeValue(ref hasData); if (hasData) { serializer.SerializeValue(ref Data, Allocator.Temp); } } else if (serializer.IsReader) { bool hasData = false; serializer.SerializeValue(ref hasData); if (hasData) { serializer.SerializeValue(ref Data, Allocator.Temp); } } } } public abstract class Actor : MonoBehaviour { static readonly Logger s_ActorLogger = new Logger(nameof(Actor)); [field: SerializeField, TriInspector.ReadOnly] public string SourceActorPath { get; internal set; } = ""; [field: SerializeField, ReadOnly] public ulong ActorID { get; internal set; } [NonSerialized] internal IActorData Data; public bool IsDataDirty { get; protected internal set; } internal ActorsManager Manager; internal IActorData InternalCreateActorData() { return CreateActorData(); } internal void HandleActorCommand(ActorCommand actorCommand) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can handle actor commands. Actor: {name} (ID: {ActorID})"); return; } if (Manager is null) { s_ActorLogger.Error($"Cannot handle command because Manager is null for actor {name} (ID: {ActorID})"); return; } if (actorCommand.ActorID != ActorID) { s_ActorLogger .Error($"Actor command ActorID {actorCommand.ActorID} does not match this actor's ID {ActorID}"); return; } OnActorCommandServer(actorCommand); } internal void HandleActorEvent(ActorEvent actorEvent) { if (Manager is null) { s_ActorLogger.Error($"Cannot handle event because Manager is null for actor {name} (ID: {ActorID})"); return; } if (actorEvent.ActorID != ActorID) { s_ActorLogger .Error($"Actor event ActorID {actorEvent.ActorID} does not match this actor's ID {ActorID}"); return; } OnActorEventClient(actorEvent); } protected abstract IActorData CreateActorData(); // Override this method to implement server-side logic public virtual void ServerTick(float deltaTime) { } // Override this method to implement client-side logic public virtual void ClientTick(float deltaTime) { } // @NOTE: Server-side method to handle actor commands protected virtual void OnActorCommandServer(ActorCommand actorCommand) { } // @NOTE: Client-side method to handle actor events protected virtual void OnActorEventClient(ActorEvent actorEvent) { } protected void SendActorCommand(ushort commandID, ref TCmdData commandData) where TCmdData : struct, ISerializableEntity { NativeArray data = DataSerializationUtils.Serialize(commandData); SendActorCommand(commandID, data); } protected void SendActorCommand(ushort commandID, NativeArray data = default) { if (Manager is null) { s_ActorLogger.Error($"Cannot send command because Manager is null for actor {name} (ID: {ActorID})"); return; } ActorCommand command = new ActorCommand { ActorID = ActorID, ClientID = NetworkManager.Singleton.LocalClientId, CommandID = commandID, Data = data }; Manager.SendActorCommandToServerRpc(command); } protected void SendActorEvent(ushort eventID, NativeArray data = default) { if (!RR.IsServer()) { s_ActorLogger.Error($"Only the server can send actor events. Actor: {name} (ID: {ActorID})"); return; } if (Manager is null) { s_ActorLogger.Error($"Cannot send event because Manager is null for actor {name} (ID: {ActorID})"); return; } ActorEvent actorEvent = new ActorEvent { ActorID = ActorID, ClientID = NetworkManager.Singleton.LocalClientId, EventID = eventID, Data = data }; Manager.SendActorEventToClientsRpc(actorEvent); } protected T DataAs() where T : IActorData { if (Data is T data) { return data; } throw new System.InvalidCastException($"Actor data is not of type {typeof(T).Name}"); } void OnValidate() { if (ActorID == 0) { ActorID = UniqueID.NewULongFromGuid(); } } } }