diff --git a/LiteEntitySystem/ClientEntityManager.cs b/LiteEntitySystem/ClientEntityManager.cs index 1a6852e..efcadfe 100644 --- a/LiteEntitySystem/ClientEntityManager.cs +++ b/LiteEntitySystem/ClientEntityManager.cs @@ -511,11 +511,11 @@ private unsafe void GoToNextState() if (field.FieldType == FieldType.SyncableSyncVar) { var syncableField = RefMagic.RefFieldValue(entity, field.Offset); - field.TypeProcessor.SetFrom(syncableField, field.SyncableSyncVarOffset, predictedData + field.PredictedOffset); + field.TypeProcessor.SetFrom(syncableField, field.SyncableSyncVarOffset, predictedData + field.PredictedOffset, field.Name); } else { - field.TypeProcessor.SetFrom(entity, field.Offset, predictedData + field.PredictedOffset); + field.TypeProcessor.SetFrom(entity, field.Offset, predictedData + field.PredictedOffset, field.Name); } } } @@ -649,7 +649,7 @@ protected override unsafe void OnLogicTick() for(int i = 0; i < classData.InterpolatedCount; i++) { var field = classData.Fields[i]; - field.TypeProcessor.SetFrom(entity, field.Offset, currentDataPtr + field.FixedOffset); + field.TypeProcessor.SetFrom(entity, field.Offset, currentDataPtr + field.FixedOffset, field.Name); } //update @@ -1002,7 +1002,7 @@ private unsafe bool ReadEntityState(byte* rawData, bool fistSync) if (field.FieldType == FieldType.SyncableSyncVar) { var syncableField = RefMagic.RefFieldValue(entity, field.Offset); - field.TypeProcessor.SetFrom(syncableField, field.SyncableSyncVarOffset, readDataPtr); + field.TypeProcessor.SetFrom(syncableField, field.SyncableSyncVarOffset, readDataPtr, field.Name); } else { @@ -1023,7 +1023,7 @@ private unsafe bool ReadEntityState(byte* rawData, bool fistSync) } else { - field.TypeProcessor.SetFrom(entity, field.Offset, readDataPtr); + field.TypeProcessor.SetFrom(entity, field.Offset, readDataPtr, field.Name); } } //Logger.Log($"E {entity.Id} Field updated: {field.Name}"); diff --git a/LiteEntitySystem/Extensions/SyncNetSerializable.cs b/LiteEntitySystem/Extensions/SyncNetSerializable.cs index a8158b5..13d63ab 100644 --- a/LiteEntitySystem/Extensions/SyncNetSerializable.cs +++ b/LiteEntitySystem/Extensions/SyncNetSerializable.cs @@ -1,17 +1,20 @@ -using System; +using System; +using System.Collections.Generic; using K4os.Compression.LZ4; using LiteNetLib.Utils; namespace LiteEntitySystem.Extensions { - public class SyncNetSerializable : SyncableField where T : INetSerializable + public class SyncNetSerializable : SyncableField, ISyncFieldChanged where T : INetSerializable { private static readonly NetDataWriter WriterCache = new(); private static readonly NetDataReader ReaderCache = new(); private static byte[] CompressionBuffer; - + private T _value; + public event Action ValueChanged; + public T Value { get => _value; @@ -30,7 +33,7 @@ public SyncNetSerializable(Func constructor) { _constructor = constructor; } - + protected internal override void RegisterRPC(ref SyncableRPCRegistrator r) { r.CreateClientAction(this, Init, ref _initAction); @@ -45,8 +48,9 @@ protected internal override void OnSyncRequested() Logger.LogError("Too much sync data!"); return; } + int bufSize = LZ4Codec.MaximumOutputSize(WriterCache.Length) + 2; - if(CompressionBuffer == null || CompressionBuffer.Length < bufSize) + if (CompressionBuffer == null || CompressionBuffer.Length < bufSize) CompressionBuffer = new byte[bufSize]; FastBitConverter.GetBytes(CompressionBuffer, 0, (ushort)WriterCache.Length); int encodedLength = LZ4Codec.Encode( @@ -55,20 +59,36 @@ protected internal override void OnSyncRequested() WriterCache.Length, CompressionBuffer, 2, - CompressionBuffer.Length-2, + CompressionBuffer.Length - 2, LZ4Level.L00_FAST); - ExecuteRPC(_initAction, new ReadOnlySpan(CompressionBuffer, 0, encodedLength+2)); + ExecuteRPC(_initAction, new ReadOnlySpan(CompressionBuffer, 0, encodedLength + 2)); } private void Init(ReadOnlySpan data) { + // Read uncompressed size ushort origSize = BitConverter.ToUInt16(data); + if (CompressionBuffer == null || CompressionBuffer.Length < origSize) CompressionBuffer = new byte[origSize]; LZ4Codec.Decode(data[2..], new Span(CompressionBuffer)); ReaderCache.SetSource(CompressionBuffer, 0, origSize); - _value ??= _constructor(); - _value.Deserialize(ReaderCache); + + // Capture the old reference + T oldValue = _value; + + // Always create a fresh instance for deserialization + T newValue = _constructor(); + newValue.Deserialize(ReaderCache); + + // Update _value + _value = newValue; + + // Compare old and new. If changed, raise event. + if (oldValue == null || !EqualityComparer.Default.Equals(oldValue, _value)) + { + ValueChanged?.Invoke(oldValue, newValue); + } } public static implicit operator T(SyncNetSerializable field) diff --git a/LiteEntitySystem/Extensions/SyncString.cs b/LiteEntitySystem/Extensions/SyncString.cs index 78a0ea2..43b4589 100644 --- a/LiteEntitySystem/Extensions/SyncString.cs +++ b/LiteEntitySystem/Extensions/SyncString.cs @@ -1,9 +1,11 @@ using System; +using System.Runtime.CompilerServices; using System.Text; +using LiteEntitySystem.Internal; namespace LiteEntitySystem.Extensions { - public class SyncString : SyncableField + public class SyncString : SyncableField, ISyncFieldChanged { private static readonly UTF8Encoding Encoding = new(false, true); private byte[] _stringData; @@ -12,6 +14,7 @@ public class SyncString : SyncableField private static RemoteCallSpan _setStringClientCall; + public event Action ValueChanged; public string Value { get => _string; @@ -43,7 +46,9 @@ public static implicit operator string(SyncString s) private void SetNewString(ReadOnlySpan data) { - _string = Encoding.GetString(data); + var newString = Encoding.GetString(data); + ValueChanged?.Invoke(_string, newString); + _string = newString; } protected internal override void OnSyncRequested() diff --git a/LiteEntitySystem/Internal/EntityClassData.cs b/LiteEntitySystem/Internal/EntityClassData.cs index 6397ae6..1ddee70 100644 --- a/LiteEntitySystem/Internal/EntityClassData.cs +++ b/LiteEntitySystem/Internal/EntityClassData.cs @@ -17,7 +17,7 @@ public SyncableFieldInfo(int offset, SyncFlags executeFlags) RPCOffset = ushort.MaxValue; } } - + internal readonly struct RpcFieldInfo { public readonly int SyncableOffset; @@ -28,7 +28,7 @@ public RpcFieldInfo(MethodCallDelegate method) SyncableOffset = -1; Method = method; } - + public RpcFieldInfo(int syncableOffset, MethodCallDelegate method) { SyncableOffset = syncableOffset; @@ -49,7 +49,7 @@ public BaseTypeInfo(Type type) Id = ushort.MaxValue; } } - + internal struct EntityClassData { public readonly ushort ClassId; @@ -72,24 +72,28 @@ internal struct EntityClassData public readonly BaseTypeInfo[] BaseTypes; public RpcFieldInfo[] RemoteCallsClient; public readonly Type Type; - + private static readonly Type InternalEntityType = typeof(InternalEntity); internal static readonly Type SingletonEntityType = typeof(SingletonEntityLogic); private static readonly Type SyncableFieldType = typeof(SyncableField); private static readonly Type EntityLogicType = typeof(EntityLogic); - + private readonly Queue _dataCache; private readonly int _dataCacheSize; private readonly int _maxHistoryCount; private readonly int _historyStart; - public Span ClientInterpolatedPrevData(InternalEntity e) => new (e.IOBuffer, 0, InterpolatedFieldsSize); - public Span ClientInterpolatedNextData(InternalEntity e) => new (e.IOBuffer, InterpolatedFieldsSize, InterpolatedFieldsSize); - public Span ClientPredictedData(InternalEntity e) => new (e.IOBuffer, InterpolatedFieldsSize*2, PredictedSize); - + public Span ClientInterpolatedPrevData(InternalEntity e) => new(e.IOBuffer, 0, InterpolatedFieldsSize); + + public Span ClientInterpolatedNextData(InternalEntity e) => + new(e.IOBuffer, InterpolatedFieldsSize, InterpolatedFieldsSize); + + public Span ClientPredictedData(InternalEntity e) => + new(e.IOBuffer, InterpolatedFieldsSize * 2, PredictedSize); + public unsafe void WriteHistory(EntityLogic e, ushort tick) { - int historyOffset = ((tick % _maxHistoryCount)+1)*LagCompensatedSize; + int historyOffset = ((tick % _maxHistoryCount) + 1) * LagCompensatedSize; fixed (byte* history = &e.IOBuffer[_historyStart]) { for (int i = 0; i < LagCompensatedCount; i++) @@ -103,8 +107,8 @@ public unsafe void WriteHistory(EntityLogic e, ushort tick) public unsafe void LoadHistroy(NetPlayer player, EntityLogic e) { - int historyAOffset = ((player.StateATick % _maxHistoryCount)+1)*LagCompensatedSize; - int historyBOffset = ((player.StateBTick % _maxHistoryCount)+1)*LagCompensatedSize; + int historyAOffset = ((player.StateATick % _maxHistoryCount) + 1) * LagCompensatedSize; + int historyBOffset = ((player.StateBTick % _maxHistoryCount) + 1) * LagCompensatedSize; int historyCurrent = 0; fixed (byte* history = &e.IOBuffer[_historyStart]) { @@ -112,7 +116,7 @@ public unsafe void LoadHistroy(NetPlayer player, EntityLogic e) { ref var field = ref LagCompensatedFields[i]; field.TypeProcessor.LoadHistory( - e, + e, field.Offset, history + historyCurrent, history + historyAOffset, @@ -133,12 +137,12 @@ public unsafe void UndoHistory(EntityLogic e) for (int i = 0; i < LagCompensatedCount; i++) { ref var field = ref LagCompensatedFields[i]; - field.TypeProcessor.SetFrom(e, field.Offset, history + historyOffset); + field.TypeProcessor.SetFrom(e, field.Offset, history + historyOffset, field.Name); historyOffset += field.IntSize; } } } - + public byte[] AllocateDataCache() { if (_dataCache.Count > 0) @@ -147,6 +151,7 @@ public byte[] AllocateDataCache() Array.Clear(data, 0, data.Length); return data; } + return new byte[_dataCacheSize]; } @@ -180,33 +185,34 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp BaseTypes = new BaseTypeInfo[tempBaseTypes.Count]; for (int i = 0; i < BaseTypes.Length; i++) BaseTypes[i] = new BaseTypeInfo(tempBaseTypes.Pop()); - + var fields = new List(); var syncableFields = new List(); var lagCompensatedFields = new List(); - + var allTypesStack = Utils.GetBaseTypes(entType, InternalEntityType, true, true); - while(allTypesStack.Count > 0) + while (allTypesStack.Count > 0) { var baseType = allTypesStack.Pop(); - + var setFlagsAttribute = baseType.GetCustomAttribute(); Flags |= setFlagsAttribute != null ? setFlagsAttribute.Flags : 0; - + //cache fields foreach (var field in Utils.GetProcessedFields(baseType)) { var ft = field.FieldType; - if(Utils.IsRemoteCallType(ft) && !field.IsStatic) + if (Utils.IsRemoteCallType(ft) && !field.IsStatic) throw new Exception($"RemoteCalls should be static! (Class: {entType} Field: {field.Name})"); - - if(field.IsStatic) + + if (field.IsStatic) continue; - - var syncVarFlags = field.GetCustomAttribute() ?? baseType.GetCustomAttribute(); + + var syncVarFlags = field.GetCustomAttribute() ?? + baseType.GetCustomAttribute(); var syncFlags = syncVarFlags?.Flags ?? SyncFlags.None; int offset = Utils.GetFieldOffset(field); - + //syncvars if (ft.IsGenericType && !ft.IsArray && ft.GetGenericTypeDefinition() == typeof(SyncVar<>)) { @@ -219,18 +225,22 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp Logger.LogError($"Unregistered field type: {ft}"); continue; } + int fieldSize = valueTypeProcessor.Size; if (syncFlags.HasFlagFast(SyncFlags.Interpolated) && !ft.IsEnum) { InterpolatedFieldsSize += fieldSize; InterpolatedCount++; } - var fieldInfo = new EntityFieldInfo($"{baseType.Name}-{field.Name}", valueTypeProcessor, offset, syncVarFlags); + + var fieldInfo = new EntityFieldInfo(field.Name, valueTypeProcessor, offset, + syncVarFlags); if (syncFlags.HasFlagFast(SyncFlags.LagCompensated)) { lagCompensatedFields.Add(fieldInfo); LagCompensatedSize += fieldSize; } + if (syncFlags.HasFlagFast(SyncFlags.AlwaysRollback)) HasRemoteRollbackFields = true; @@ -239,41 +249,53 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp fields.Add(fieldInfo); FixedFieldsSize += fieldSize; + + if (syncVarFlags?.OnChangeCallback != null) + SyncVarCallbackRegistry.RegisterCallback(entType, field.Name, ft, + syncVarFlags.OnChangeCallback); } else if (ft.IsSubclassOf(SyncableFieldType)) { if (!field.IsInitOnly) - throw new Exception($"Syncable fields should be readonly! (Class: {entType} Field: {field.Name})"); - + throw new Exception( + $"Syncable fields should be readonly! (Class: {entType} Field: {field.Name})"); + syncableFields.Add(new SyncableFieldInfo(offset, syncFlags)); + var syncableFieldTypesWithBase = Utils.GetBaseTypes(ft, SyncableFieldType, true, true); - while(syncableFieldTypesWithBase.Count > 0) + while (syncableFieldTypesWithBase.Count > 0) { var syncableType = syncableFieldTypesWithBase.Pop(); //syncable fields foreach (var syncableField in Utils.GetProcessedFields(syncableType)) { var syncableFieldType = syncableField.FieldType; - if(Utils.IsRemoteCallType(syncableFieldType) && !syncableField.IsStatic) - throw new Exception($"RemoteCalls should be static! (Class: {syncableType} Field: {syncableField.Name})"); - - if (!syncableFieldType.IsValueType || - !syncableFieldType.IsGenericType || + if (Utils.IsRemoteCallType(syncableFieldType) && !syncableField.IsStatic) + throw new Exception( + $"RemoteCalls should be static! (Class: {syncableType} Field: {syncableField.Name})"); + + if (!syncableFieldType.IsValueType || + !syncableFieldType.IsGenericType || syncableFieldType.GetGenericTypeDefinition() != typeof(SyncVar<>) || - syncableField.IsStatic) + syncableField.IsStatic) continue; syncableFieldType = syncableFieldType.GetGenericArguments()[0]; if (syncableFieldType.IsEnum) syncableFieldType = syncableFieldType.GetEnumUnderlyingType(); - if (!ValueTypeProcessor.Registered.TryGetValue(syncableFieldType, out var valueTypeProcessor)) + if (!ValueTypeProcessor.Registered.TryGetValue(syncableFieldType, + out var valueTypeProcessor)) { Logger.LogError($"Unregistered field type: {syncableFieldType}"); continue; } + int syncvarOffset = Utils.GetFieldOffset(syncableField); - var fieldInfo = new EntityFieldInfo($"{baseType.Name}-{field.Name}:{syncableField.Name}", valueTypeProcessor, offset, syncvarOffset, syncVarFlags); + var fieldInfo = new EntityFieldInfo( + syncableField.Name, valueTypeProcessor, offset, + syncvarOffset, syncVarFlags); + fields.Add(fieldInfo); FixedFieldsSize += fieldInfo.IntSize; if (fieldInfo.IsPredicted) @@ -283,7 +305,7 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp } } } - + //sort by placing interpolated first fields.Sort((a, b) => { @@ -294,7 +316,7 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp Fields = fields.ToArray(); SyncableFields = syncableFields.ToArray(); FieldsCount = Fields.Length; - FieldsFlagsSize = (FieldsCount-1) / 8 + 1; + FieldsFlagsSize = (FieldsCount - 1) / 8 + 1; LagCompensatedFields = lagCompensatedFields.ToArray(); LagCompensatedCount = LagCompensatedFields.Length; @@ -315,7 +337,7 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp field.PredictedOffset = -1; } } - + _maxHistoryCount = (byte)entityManager.MaxHistorySize; int historySize = entType.IsSubclassOf(EntityLogicType) ? (_maxHistoryCount + 1) * LagCompensatedSize : 0; if (entityManager.IsServer) @@ -332,7 +354,8 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp Type = entType; } - public void PrepareBaseTypes(Dictionary registeredTypeIds, ref ushort singletonCount, ref ushort filterCount) + public void PrepareBaseTypes(Dictionary registeredTypeIds, ref ushort singletonCount, + ref ushort filterCount) { if (Type == null) return; diff --git a/LiteEntitySystem/Internal/ValueTypeProcessor.cs b/LiteEntitySystem/Internal/ValueTypeProcessor.cs index a15a9a9..f9bdb90 100644 --- a/LiteEntitySystem/Internal/ValueTypeProcessor.cs +++ b/LiteEntitySystem/Internal/ValueTypeProcessor.cs @@ -4,44 +4,63 @@ namespace LiteEntitySystem.Internal { public delegate T InterpolatorDelegateWithReturn(T prev, T current, float t) where T : unmanaged; - + internal abstract unsafe class ValueTypeProcessor { - public static readonly Dictionary Registered = new (); - + public static readonly Dictionary Registered = new(); + internal readonly int Size; protected ValueTypeProcessor(int size) => Size = size; internal abstract void InitSyncVar(InternalBaseClass obj, int offset, InternalEntity entity, ushort fieldId); - internal abstract void SetFrom(InternalBaseClass obj, int offset, byte* data); + internal abstract void SetFrom(InternalBaseClass obj, int offset, byte* data, string fieldName); internal abstract bool SetFromAndSync(InternalBaseClass obj, int offset, byte* data); internal abstract void WriteTo(InternalBaseClass obj, int offset, byte* data); - internal abstract void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer); - internal abstract void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime); + + internal abstract void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, + float fTimer); + + internal abstract void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, + byte* historyB, float lerpTime); + internal abstract int GetHashCode(InternalBaseClass obj, int offset); internal abstract string ToString(InternalBaseClass obj, int offset); } internal unsafe class ValueTypeProcessor : ValueTypeProcessor where T : unmanaged { - public ValueTypeProcessor() : base(sizeof(T)) { } + public ValueTypeProcessor() : base(sizeof(T)) + { + } - internal override void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => + internal override void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, + float fTimer) => throw new Exception($"This type: {typeof(T)} can't be interpolated"); internal override void InitSyncVar(InternalBaseClass obj, int offset, InternalEntity entity, ushort fieldId) => RefMagic.RefFieldValue>(obj, offset).Init(entity, fieldId); - internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) + internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, + byte* historyB, float lerpTime) { ref var a = ref RefMagic.RefFieldValue>(obj, offset); *(T*)tempHistory = a; a.SetDirect(*(T*)historyA); } - - internal override void SetFrom(InternalBaseClass obj, int offset, byte* data) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(*(T*)data); + + internal override void SetFrom(InternalBaseClass obj, int offset, byte* data, string fieldName) + { + ref var syncVar = ref RefMagic.RefFieldValue>(obj, offset); + T oldValue = syncVar.Value; + T newValue = *(T*)data; + + if (!Utils.FastEquals(ref oldValue, ref newValue)) + { + syncVar.SetDirect(newValue); + SyncVarCallbackRegistry.TryInvoke(obj.GetType(), fieldName, obj, newValue); + } + } internal override bool SetFromAndSync(InternalBaseClass obj, int offset, byte* data) => RefMagic.RefFieldValue>(obj, offset).SetFromAndSync(data); @@ -51,30 +70,36 @@ internal override void WriteTo(InternalBaseClass obj, int offset, byte* data) => internal override int GetHashCode(InternalBaseClass obj, int offset) => RefMagic.RefFieldValue>(obj, offset).GetHashCode(); - + internal override string ToString(InternalBaseClass obj, int offset) => RefMagic.RefFieldValue>(obj, offset).ToString(); } internal class ValueTypeProcessorInt : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(int*)prev, *(int*)current, fTimer)); - - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) + internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, + float fTimer) => + RefMagic.RefFieldValue>(obj, offset) + .SetDirect(Utils.Lerp(*(int*)prev, *(int*)current, fTimer)); + + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, + byte* historyB, float lerpTime) { ref var a = ref RefMagic.RefFieldValue>(obj, offset); *(int*)tempHistory = a; a.SetDirect(Utils.Lerp(*(int*)historyA, *(int*)historyB, lerpTime)); } } - + internal class ValueTypeProcessorLong : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(long*)prev, *(long*)current, fTimer)); - - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) + internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, + float fTimer) => + RefMagic.RefFieldValue>(obj, offset) + .SetDirect(Utils.Lerp(*(long*)prev, *(long*)current, fTimer)); + + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, + byte* historyB, float lerpTime) { ref var a = ref RefMagic.RefFieldValue>(obj, offset); *(long*)tempHistory = a; @@ -84,23 +109,29 @@ internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byt internal class ValueTypeProcessorFloat : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(float*)prev, *(float*)current, fTimer)); - - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) + internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, + float fTimer) => + RefMagic.RefFieldValue>(obj, offset) + .SetDirect(Utils.Lerp(*(float*)prev, *(float*)current, fTimer)); + + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, + byte* historyB, float lerpTime) { ref var a = ref RefMagic.RefFieldValue>(obj, offset); *(float*)tempHistory = a; a.SetDirect(Utils.Lerp(*(float*)historyA, *(float*)historyB, lerpTime)); } } - + internal class ValueTypeProcessorDouble : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(double*)prev, *(double*)current, fTimer)); - - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) + internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, + float fTimer) => + RefMagic.RefFieldValue>(obj, offset) + .SetDirect(Utils.Lerp(*(double*)prev, *(double*)current, fTimer)); + + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, + byte* historyB, float lerpTime) { ref var a = ref RefMagic.RefFieldValue>(obj, offset); *(double*)tempHistory = a; @@ -112,10 +143,13 @@ internal unsafe class UserTypeProcessor : ValueTypeProcessor where T : unm { private readonly InterpolatorDelegateWithReturn _interpDelegate; - internal override void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(_interpDelegate?.Invoke(*(T*)prev, *(T*)current, fTimer) ?? *(T*)prev); - - internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) + internal override void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, + float fTimer) => + RefMagic.RefFieldValue>(obj, offset) + .SetDirect(_interpDelegate?.Invoke(*(T*)prev, *(T*)current, fTimer) ?? *(T*)prev); + + internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, + byte* historyB, float lerpTime) { ref var a = ref RefMagic.RefFieldValue>(obj, offset); *(T*)tempHistory = a; diff --git a/LiteEntitySystem/SyncVar.cs b/LiteEntitySystem/SyncVar.cs index 0f733c9..c6e1e02 100644 --- a/LiteEntitySystem/SyncVar.cs +++ b/LiteEntitySystem/SyncVar.cs @@ -21,12 +21,24 @@ public class SyncVarFlags : Attribute { public readonly SyncFlags Flags; public readonly OnSyncExecutionOrder OnSyncExecutionOrder; + public readonly string OnChangeCallback; public SyncVarFlags(SyncFlags flags) { Flags = flags; } + public SyncVarFlags(string onChangeCallback) + { + OnChangeCallback = onChangeCallback; + } + + public SyncVarFlags(SyncFlags flags, string onChangeCallback) + { + Flags = flags; + OnChangeCallback = onChangeCallback; + } + public SyncVarFlags(OnSyncExecutionOrder executionOrder) { OnSyncExecutionOrder = executionOrder; @@ -37,8 +49,23 @@ public SyncVarFlags(SyncFlags flags, OnSyncExecutionOrder executionOrder) Flags = flags; OnSyncExecutionOrder = executionOrder; } + + public SyncVarFlags(SyncFlags flags, OnSyncExecutionOrder executionOrder, string onChangeCallback) + { + Flags = flags; + OnSyncExecutionOrder = executionOrder; + OnChangeCallback = onChangeCallback; + } } + + /// + /// A network-synced variable stored as a struct but able to raise events when changed. + /// The event handlers are stored in a static dictionary keyed by (InternalEntity, fieldId). + /// This avoids losing subscriptions when the struct is copied. + /// + /// NOTE: Make sure to remove subscriptions for destroyed entities to prevent memory leaks! + /// [StructLayout(LayoutKind.Sequential)] public struct SyncVar : IEquatable, IEquatable> where T : unmanaged { diff --git a/LiteEntitySystem/SyncVarCallbackRegistry.cs b/LiteEntitySystem/SyncVarCallbackRegistry.cs new file mode 100644 index 0000000..64bcf6c --- /dev/null +++ b/LiteEntitySystem/SyncVarCallbackRegistry.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace LiteEntitySystem.Internal +{ + // A typed callback delegate for open-instance methods: + // TOwner = the entity class, + // TValue = the field type + internal delegate void SyncVarCallbackDelegate(TOwner owner, TValue value); + + // The registry that stores (delegate + invoker) keyed by (entType, fieldName) + internal static class SyncVarCallbackRegistry + { + // The dictionary maps (EntityType, fieldName) -> a small record: + // typedDelegate: The strongly typed SyncVarCallbackDelegate + // invoker: Action that can call typedDelegate + private static readonly Dictionary<(Type, string), (Delegate typedDelegate, Action + invoker)> + Registry = new(); + + /// + /// Register a callback for a particular field on a given entity type. + /// + /// Entity class that declares the method. + /// The name of the field (unique in that class). + /// The SyncVar's value type (e.g. int). + /// The user’s callback method name in 'entType'. + public static void RegisterCallback(Type entType, string fieldName, Type fieldType, string methodName) + { + if (methodName == null) + throw new Exception("OnChangeCallback method name is null!"); + + var methodInfo = GetMethodInHierarchy(entType, methodName); + + if (methodInfo == null) + throw new Exception($"Method '{methodName}' not found in {entType}"); + + // Create the strongly typed delegate: e.g. SyncVarCallbackDelegate + var closedDelegateType = typeof(SyncVarCallbackDelegate<,>).MakeGenericType(entType, fieldType); + var typedDelegate = methodInfo.CreateDelegate(closedDelegateType); + + // Build or get the "invoker" that casts (object owner, object value) -> typed call + var invoker = CreateInvokerLambda(entType, fieldType); + + // Store in dictionary + Registry[(entType, fieldName)] = (typedDelegate, invoker); + } + + /// + /// Create the small "invoker lambda" that can call the typed delegate. + /// + private static Action CreateInvokerLambda(Type entType, Type fieldType) + { + // We'll reflect the 'GenericInvoker' method below. + MethodInfo genericInvokerMethod = typeof(SyncVarCallbackRegistry) + .GetMethod(nameof(GenericInvoker), BindingFlags.Static | BindingFlags.NonPublic); + + if (genericInvokerMethod == null) + throw new Exception("Cannot find GenericInvoker method!"); + + // Close it over (entType, fieldType) -> GenericInvoker, for example + var closedMethod = genericInvokerMethod.MakeGenericMethod(entType, fieldType); + + // Make a delegate of type 'Action' from that method + return (Action)Delegate.CreateDelegate( + typeof(Action), + closedMethod + ); + } + + /// + /// The method that does the actual cast + invocation: + /// (SyncVarCallbackDelegate)callback + /// Then calls typedCallback( (TOwner)owner, (TValue)newValue ). + /// + private static void GenericInvoker( + Delegate callback, + object owner, + object newValue) + { + var typedCallback = (SyncVarCallbackDelegate)callback; + typedCallback((TOwner)owner, (TValue)newValue); + } + + /// + /// Invoke the callback for the specified entity type + field name, + /// passing the entity instance (as object) and newValue (as object). + /// + public static bool TryInvoke(Type entType, string fieldName, object owner, object newValue) + { + if (Registry.TryGetValue((entType, fieldName), out var data)) + { + // data.typedDelegate is the strongly typed delegate + // data.invoker is the small Action that calls it + data.invoker(data.typedDelegate, owner, newValue); + return true; + } + + return false; + } + + private static MethodInfo GetMethodInHierarchy(Type type, string methodName) + { + while (type != null && type != typeof(object)) + { + // Search only declared methods on the current 'type' + var method = type.GetMethod( + methodName, + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly + ); + + if (method != null) + return method; + + type = type.BaseType; + } + + return null; + } + } +} \ No newline at end of file diff --git a/LiteEntitySystem/SyncableField.cs b/LiteEntitySystem/SyncableField.cs index 5f8ae08..bd6c8dd 100644 --- a/LiteEntitySystem/SyncableField.cs +++ b/LiteEntitySystem/SyncableField.cs @@ -3,6 +3,15 @@ namespace LiteEntitySystem { + + /// + /// Provides a standard interface for receiving SyncVar change notifications. + /// + public interface ISyncFieldChanged + { + event Action ValueChanged; + } + public abstract class SyncableField : InternalBaseClass { internal InternalEntity ParentEntityInternal;