From ef075d414af63625a5959ff91d253c9e94ff3a62 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Wed, 29 Nov 2023 23:01:15 +0100 Subject: [PATCH] Move entity init support into base And other refactorings and fixes --- TechbloxModdingAPI/Block.cs | 36 ++++---- TechbloxModdingAPI/Blocks/Wire.cs | 2 +- TechbloxModdingAPI/Common/EcsObjectBase.cs | 82 ++++++++++++++----- .../Common/EcsObjectBaseEngine.cs | 26 +++++- .../Common/Engines/EngineManager.cs | 2 + .../Common/Traits/HasPhysics.cs | 3 +- TechbloxModdingAPI/Common/Utils/EcsUtils.cs | 23 ++++++ TechbloxModdingAPI/Main.cs | 2 + TechbloxModdingAPI/Player.cs | 6 +- TechbloxModdingAPI/SimBody.cs | 2 +- .../Utility/ECS/ManagedApiExtensions.cs | 6 +- .../Utility/ECS/NativeApiExtensions.cs | 11 ++- TechbloxModdingAPI/Utility/OptionalRef.cs | 10 ++- 13 files changed, 163 insertions(+), 48 deletions(-) create mode 100644 TechbloxModdingAPI/Common/Utils/EcsUtils.cs diff --git a/TechbloxModdingAPI/Block.cs b/TechbloxModdingAPI/Block.cs index 44eb099..acccabe 100644 --- a/TechbloxModdingAPI/Block.cs +++ b/TechbloxModdingAPI/Block.cs @@ -28,7 +28,7 @@ namespace TechbloxModdingAPI /// A single (perhaps scaled) block. Properties may return default values if the block is removed and then setting them is ignored. /// For specific block type operations, use the specialised block classes in the TechbloxModdingAPI.Blocks namespace. /// - public class Block : EcsObjectBase, IHasPhysics, IEquatable, IEquatable + public class Block : EcsObjectBase, IHasPhysics, IEquatable, IEquatable { protected static readonly PlacementEngine PlacementEngine = new(); protected static readonly RemovalEngine RemovalEngine = new(); @@ -54,9 +54,7 @@ namespace TechbloxModdingAPI if (PlacementEngine.IsInGame && GameClient.IsBuildMode) { var initializer = PlacementEngine.PlaceBlock(block, position, player, autoWire); - var egid = initializer.EGID; - var bl = New(egid); - bl.InitData = initializer; + var bl = New(initializer); Placed += bl.OnPlacedInit; return bl; } @@ -123,20 +121,31 @@ namespace TechbloxModdingAPI /// Whether the block is definitely a signaling block /// internal static Block New(EGID egid, bool signaling = false) + { + return New(egid, default, signaling); + } + + private static Block New(EcsInitData initData, bool signaling = false) + { + return New(initData.EGID, initData, signaling); + } + + private static Block New(EGID egid, EcsInitData initData, bool signaling) { if (egid == default) return null; + Func constructor; + Type type = null; if (GroupToConstructor.TryGetValue(egid.groupID, out var value)) - { - var (constructor, type) = value; - return GetInstance(egid, constructor, type); - } + (constructor, type) = value; + else + constructor = signaling ? e => new SignalingBlock(e) : e => new Block(e); - return signaling - ? GetInstance(egid, e => new SignalingBlock(e)) - : GetInstance(egid, e => new Block(e)); + return initData != default + ? GetInstanceNew(initData, constructor, type) + : GetInstanceExisting(egid, constructor, type); } - public Block(EGID id) : base(id, typeof(BlockEntityDescriptor)) + public Block(EGID id) : base(id) { Type expectedType; if (GroupToConstructor.ContainsKey(id.groupID) && @@ -375,7 +384,7 @@ namespace TechbloxModdingAPI var bgec = GetComponent(); return blockGroup = bgec.currentBlockGroup == -1 ? null - : GetInstance(new EGID((uint)bgec.currentBlockGroup, BlockGroupExclusiveGroups.BlockGroupEntityGroup), + : GetInstanceExisting(new EGID((uint)bgec.currentBlockGroup, BlockGroupExclusiveGroups.BlockGroupEntityGroup), egid => new BlockGroup((int)egid.entityID, this)); } set @@ -466,7 +475,6 @@ namespace TechbloxModdingAPI { //Member method instead of lambda to avoid constantly creating delegates if (e.ID != Id) return; Placed -= OnPlacedInit; //And we can reference it - InitData = default; //Remove initializer as it's no longer valid - if the block gets removed it shouldn't be used again if (copiedFrom != default) BlockCloneEngine.CopyBlockStats(copiedFrom, Id); } diff --git a/TechbloxModdingAPI/Blocks/Wire.cs b/TechbloxModdingAPI/Blocks/Wire.cs index 740883d..bd9019a 100644 --- a/TechbloxModdingAPI/Blocks/Wire.cs +++ b/TechbloxModdingAPI/Blocks/Wire.cs @@ -218,7 +218,7 @@ namespace TechbloxModdingAPI.Blocks /// A copy of the wire object. public Wire OutputToInputCopy() { - return GetInstance(wireEGID, egid => new Wire(egid)); + return GetInstanceExisting(wireEGID, egid => new Wire(egid)); } /// diff --git a/TechbloxModdingAPI/Common/EcsObjectBase.cs b/TechbloxModdingAPI/Common/EcsObjectBase.cs index 01d6c10..4f20981 100644 --- a/TechbloxModdingAPI/Common/EcsObjectBase.cs +++ b/TechbloxModdingAPI/Common/EcsObjectBase.cs @@ -10,8 +10,25 @@ using TechbloxModdingAPI.Utility; namespace TechbloxModdingAPI.Common; -public abstract class EcsObjectBase +public abstract class EcsObjectBase : EcsObjectBase where TDescriptor : IEntityDescriptor, new() { + protected EcsObjectBase(EGID id) : base(id, typeof(TDescriptor)) + { + } + + protected EcsObjectBase(EntityReference reference) : base(reference, typeof(TDescriptor)) + { + } + + protected bool RemoveEntity() + { + if (!Exists) return false; + _engine.Functions.RemoveEntity(Id); + return true; + } +} + +public abstract class EcsObjectBase { public EGID Id => _engine.GetEgid(Reference); /// /// A reference to a specific entity that persists through group swaps and such. @@ -23,9 +40,12 @@ public abstract class EcsObjectBase /// Whether the entity reference is still valid. Returns false if this object no longer exists. /// public bool Exists => Id != default; // TODO: Might need extra code to support IDs during init + + public readonly Type EntityDescriptorType; + public readonly Type[] AllowedEntityComponents; private static readonly Dictionary> _instances = new(); - private static readonly EcsObjectBaseEngine _engine = new(); + internal static readonly EcsObjectBaseEngine _engine = new(); private static WeakDictionary GetInstances(Type type) { @@ -34,13 +54,14 @@ public abstract class EcsObjectBase /// /// Returns a cached instance if there's an actively used instance of the object already. - /// Objects still get garbage collected and then they will be removed from the cache. + /// Objects still get garbage collected and then they will be removed from the cache.
+ /// Only use for existing entities! Use the other overload for newly created entities. ///
/// The EGID of the entity /// The constructor to construct the object /// The object type /// - internal static T GetInstance(EGID egid, Func constructor, Type type = null) where T : EcsObjectBase + internal static T GetInstanceExisting(EGID egid, Func constructor, Type type = null) where T : EcsObjectBase { var instances = GetInstances(type ?? typeof(T)); if (instances == null || !instances.TryGetValue(_engine.GetEntityReference(egid), out var instance)) @@ -48,6 +69,33 @@ public abstract class EcsObjectBase return (T)instance; } + /// + /// Returns a cached instance if there's an actively used instance of the object already. + /// Objects still get garbage collected and then they will be removed from the cache.
+ /// Only use for newly created entities! Use the other overload for existing entities. + ///
+ /// The EGID of the entity + /// The constructor to construct the object + /// The object type + /// + internal static T GetInstanceNew(EcsInitData initData, Func constructor, Type type = null) where T : EcsObjectBase + { + var instances = GetInstances(type ?? typeof(T)); + if (instances == null || !instances.TryGetValue(initData.Reference, out var instance)) + { + var ret = constructor(initData.EGID); + ret.InitData = initData; + return ret; // It will be added by the constructor + } + + return (T)instance; + } + + protected static V CreateEntity(EGID egid, Func constructor, Type type = null) where U : IEntityDescriptor, new() where V : EcsObjectBase + { + return GetInstanceNew(_engine.Factory.BuildEntity(egid), constructor, type); + } + protected EcsObjectBase(EGID id, Type entityDescriptorType) : this(_engine.GetEntityReference(id), entityDescriptorType) { } @@ -62,9 +110,11 @@ public abstract class EcsObjectBase if (!dict.ContainsKey(reference)) // Multiple instances may be created dict.Add(reference, this); Reference = reference; + EntityDescriptorType = entityDescriptorType; + AllowedEntityComponents = EcsUtils.GetValidEntityComponents(entityDescriptorType); + // Remove init data once the entity gets submitted so that it won't be used again once the entity is removed + if (InitData != default) _engine.TrackNewEntity(this, obj => obj.InitData = default); } - - protected internal OptionalRef GetComponentOptional() where T : unmanaged, IEntityComponent { @@ -91,28 +141,22 @@ public abstract class EcsObjectBase _engine.SetComponent(this, type, name, value); } - protected bool RemoveEntity() - { - // TODO: _entityFunctions.Remove...() - } - #region ECS initializer stuff - protected internal EcsInitData InitData; + internal EcsInitData InitData { get; private set; } /// /// Holds information needed to construct a component initializer. /// Necessary because the initializer is a ref struct which cannot be assigned to a field. /// - protected internal struct EcsInitData + protected internal readonly record struct EcsInitData(FasterDictionary group, EntityReference Reference, EGID EGID) { - private FasterDictionary group; - private EntityReference reference; - - public static implicit operator EcsInitData(EntityInitializer initializer) => new() - { group = GetInitGroup(initializer), reference = initializer.reference }; + public static implicit operator EcsInitData(EntityInitializer initializer) => new(GetInitGroup(initializer), initializer.reference, initializer.EGID); - public EntityInitializer Initializer(EGID id) => new(id, group, reference); + private readonly FasterDictionary group = group; + public readonly EntityReference Reference = Reference; + public readonly EGID EGID = EGID; + public EntityInitializer Initializer(EGID id = default) => new(id == default ? EGID : id, group, Reference); public bool Valid => group != null; } diff --git a/TechbloxModdingAPI/Common/EcsObjectBaseEngine.cs b/TechbloxModdingAPI/Common/EcsObjectBaseEngine.cs index 34a23e9..06ac738 100644 --- a/TechbloxModdingAPI/Common/EcsObjectBaseEngine.cs +++ b/TechbloxModdingAPI/Common/EcsObjectBaseEngine.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; using HarmonyLib; +using RobocraftX.Schedulers; using Svelto.ECS; using Svelto.ECS.Hybrid; +using Svelto.Tasks; +using Svelto.Tasks.Lean; using TechbloxModdingAPI.Blocks.Engines; using TechbloxModdingAPI.Common.Engines; using TechbloxModdingAPI.Utility; @@ -9,7 +13,7 @@ using TechbloxModdingAPI.Utility.ECS; namespace TechbloxModdingAPI.Common; -public class EcsObjectBaseEngine : IApiEngine +public class EcsObjectBaseEngine : IFactoryEngine, IFunEngine { public void Ready() { @@ -68,4 +72,24 @@ public class EcsObjectBaseEngine : IApiEngine AccessTools.Field(str.GetType(), name).SetValue(str, value); prop.SetValue(opt, str); } + + private readonly Dictionary> _waitingForSubmission = new(); + + public void TrackNewEntity(EcsObjectBase obj, Action done) + { + if (_waitingForSubmission.ContainsKey(obj)) + throw new InvalidOperationException("Something has gone horribly wrong here"); + _waitingForSubmission.Add(obj, done); + WaitUntilEntitySubmission().RunOn(ClientLean.UIScheduler); // TODO: Pick the right scheduler + } + + private IEnumerator WaitUntilEntitySubmission() + { + // TODO: Get the scheduler instance based on the engine (inject in engine manager) + yield return new WaitForSubmissionEnumerator(FullGameFields._mainGameEnginesRoot.scheduler).Continue(); + foreach (var (obj, done) in _waitingForSubmission) done(obj); + } + + public IEntityFactory Factory { get; set; } + public IEntityFunctions Functions { get; set; } } \ No newline at end of file diff --git a/TechbloxModdingAPI/Common/Engines/EngineManager.cs b/TechbloxModdingAPI/Common/Engines/EngineManager.cs index 8248896..ea297e7 100644 --- a/TechbloxModdingAPI/Common/Engines/EngineManager.cs +++ b/TechbloxModdingAPI/Common/Engines/EngineManager.cs @@ -17,6 +17,8 @@ public class EngineManager /// The types to register to public static void AddEngine(IApiEngine engine, params ApiEngineType[] types) { + if (types.Length == 0) + Logging.LogWarning($"Engine {engine.GetType().FullName} added without any types! This doesn't do anything."); foreach (var type in types) { if (!_engines.ContainsKey(type)) diff --git a/TechbloxModdingAPI/Common/Traits/HasPhysics.cs b/TechbloxModdingAPI/Common/Traits/HasPhysics.cs index 560d984..f0daffb 100644 --- a/TechbloxModdingAPI/Common/Traits/HasPhysics.cs +++ b/TechbloxModdingAPI/Common/Traits/HasPhysics.cs @@ -11,7 +11,8 @@ public interface IHasPhysics public static class HasPhysicsExtensions { - internal static void UpdatePhysicsUECSComponent(this O obj, T componentData) where O : EcsObjectBase, IHasPhysics where T : struct, IComponentData + internal static void UpdatePhysicsUECSComponent(this O obj, T componentData) + where O : EcsObjectBase, IHasPhysics where T : struct, IComponentData { var phyStruct = obj.GetComponentOptional(); if (phyStruct) //It exists diff --git a/TechbloxModdingAPI/Common/Utils/EcsUtils.cs b/TechbloxModdingAPI/Common/Utils/EcsUtils.cs new file mode 100644 index 0000000..82895cc --- /dev/null +++ b/TechbloxModdingAPI/Common/Utils/EcsUtils.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using HarmonyLib; +using Svelto.ECS; + +namespace TechbloxModdingAPI.Common.Utils +{ + public static class EcsUtils + { + public static Type[] GetValidEntityComponents(Type entityDescriptorType) + { + // TODO: Cache + var templateType = typeof(EntityDescriptorTemplate<>).MakeGenericType(entityDescriptorType); + var templateDescriptor = AccessTools.Property(templateType, "descriptor"); + var getDescriptorExpr = Expression.MakeMemberAccess(null, templateDescriptor ?? throw new InvalidOperationException()); + var getTemplateDescriptorExpr = Expression.Lambda>(getDescriptorExpr); + var getTemplateDescriptor = getTemplateDescriptorExpr.Compile(); + var builders = getTemplateDescriptor().componentsToBuild; + return builders.Select(builder => builder.GetEntityComponentType()).ToArray(); + } + } +} \ No newline at end of file diff --git a/TechbloxModdingAPI/Main.cs b/TechbloxModdingAPI/Main.cs index 3507053..ea40f02 100644 --- a/TechbloxModdingAPI/Main.cs +++ b/TechbloxModdingAPI/Main.cs @@ -8,6 +8,7 @@ using Svelto.Context; using TechbloxModdingAPI.App; using TechbloxModdingAPI.Blocks; +using TechbloxModdingAPI.Common; using TechbloxModdingAPI.Tasks; using TechbloxModdingAPI.Utility; @@ -61,6 +62,7 @@ namespace TechbloxModdingAPI // init input Input.FakeInput.Init(); // init object-oriented classes + EcsObjectBase.Init(); Player.Init(); Block.Init(); BlockGroup.Init(); diff --git a/TechbloxModdingAPI/Player.cs b/TechbloxModdingAPI/Player.cs index 6e645a2..1e1fad3 100644 --- a/TechbloxModdingAPI/Player.cs +++ b/TechbloxModdingAPI/Player.cs @@ -84,7 +84,7 @@ namespace TechbloxModdingAPI internal static Player GetInstance(uint id) { - return EcsObjectBase.GetInstance(new EGID(id, CharacterExclusiveGroups.OnFootGroup), + return EcsObjectBase.GetInstanceExisting(new EGID(id, CharacterExclusiveGroups.OnFootGroup), e => new Player(e.entityID)); } @@ -469,7 +469,7 @@ namespace TechbloxModdingAPI { var egid = playerEngine.GetThingLookedAt(Id, maxDistance); return egid != default && egid.groupID == CommonExclusiveGroups.SIMULATION_BODIES_GROUP - ? EcsObjectBase.GetInstance(egid, e => new SimBody(e)) + ? EcsObjectBase.GetInstanceExisting(egid, e => new SimBody(e)) : null; } @@ -482,7 +482,7 @@ namespace TechbloxModdingAPI { var egid = playerEngine.GetThingLookedAt(Id, maxDistance); return egid != default && egid.groupID == WiresGUIExclusiveGroups.WireGroup - ? EcsObjectBase.GetInstance(new EGID(egid.entityID, BuildModeWiresGroups.WiresGroup.Group), + ? EcsObjectBase.GetInstanceExisting(new EGID(egid.entityID, BuildModeWiresGroups.WiresGroup.Group), e => new Wire(e)) : null; } diff --git a/TechbloxModdingAPI/SimBody.cs b/TechbloxModdingAPI/SimBody.cs index 31227df..f219b84 100644 --- a/TechbloxModdingAPI/SimBody.cs +++ b/TechbloxModdingAPI/SimBody.cs @@ -20,7 +20,7 @@ namespace TechbloxModdingAPI ///
public Cluster Cluster => cluster ??= clusterId == uint.MaxValue // Return cluster or if it's null then set it ? Block.BlockEngine.GetCluster(Id.entityID) // If we don't have a clusterId set then get it from the game - : GetInstance(new EGID(clusterId, ClustersExclusiveGroups.SIMULATION_CLUSTERS_GROUP), + : GetInstanceExisting(new EGID(clusterId, ClustersExclusiveGroups.SIMULATION_CLUSTERS_GROUP), egid => new Cluster(egid)); // Otherwise get the cluster from the ID private Cluster cluster; diff --git a/TechbloxModdingAPI/Utility/ECS/ManagedApiExtensions.cs b/TechbloxModdingAPI/Utility/ECS/ManagedApiExtensions.cs index 8ebe6b4..4a52727 100644 --- a/TechbloxModdingAPI/Utility/ECS/ManagedApiExtensions.cs +++ b/TechbloxModdingAPI/Utility/ECS/ManagedApiExtensions.cs @@ -1,3 +1,4 @@ +using System.Linq; using Svelto.ECS; using Svelto.ECS.Hybrid; using TechbloxModdingAPI.Common; @@ -54,7 +55,10 @@ namespace TechbloxModdingAPI.Utility.ECS EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); var opt = QueryEntityOptional(entitiesDB, id); if (opt) return ref opt.Get(); - if (obj.InitData.Valid) return ref obj.InitData.Initializer(id).GetOrAdd(); + // If initializing the entity, check if the component is allowed by the descriptor, otherwise it could cause + // issues in the game with Add() calls running unexpectedly + if (obj.InitData.Valid && obj.AllowedEntityComponents.Contains(typeof(T))) + return ref obj.InitData.Initializer(id).GetOrAdd(); return ref opt.Get(); //Default value } diff --git a/TechbloxModdingAPI/Utility/ECS/NativeApiExtensions.cs b/TechbloxModdingAPI/Utility/ECS/NativeApiExtensions.cs index 8b6d9f0..0f763d2 100644 --- a/TechbloxModdingAPI/Utility/ECS/NativeApiExtensions.cs +++ b/TechbloxModdingAPI/Utility/ECS/NativeApiExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Svelto.ECS; using Svelto.Tasks; using Svelto.Tasks.Lean; @@ -58,12 +59,10 @@ namespace TechbloxModdingAPI.Utility.ECS EGID id = group == ExclusiveGroupStruct.Invalid ? obj.Id : new EGID(obj.Id.entityID, group); var opt = QueryEntityOptional(entitiesDB, id); if (opt) return ref opt.Get(); - if (obj.InitData.Valid) return ref obj.InitData.Initializer(id).GetOrAdd(); - /*if (!obj.InitData.Valid) return ref opt.Get(); //Default value - var init = obj.InitData.Initializer(id); - // Do not create the component if missing, as that can trigger Add() listeners that, in some cases, may be - // invalid if (ab)using the classes in an unusual way - TODO: Check entity descriptor or something - if (init.Has()) return ref init.Get();*/ + // If initializing the entity, check if the component is allowed by the descriptor, otherwise it could cause + // issues in the game with Add() calls running unexpectedly + if (obj.InitData.Valid && obj.AllowedEntityComponents.Contains(typeof(T))) + return ref obj.InitData.Initializer(id).GetOrAdd(); return ref opt.Get(); //Default value } diff --git a/TechbloxModdingAPI/Utility/OptionalRef.cs b/TechbloxModdingAPI/Utility/OptionalRef.cs index bf4886c..c5ebfab 100644 --- a/TechbloxModdingAPI/Utility/OptionalRef.cs +++ b/TechbloxModdingAPI/Utility/OptionalRef.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Svelto.DataStructures; using Svelto.ECS; using TechbloxModdingAPI.Common; @@ -14,6 +15,7 @@ namespace TechbloxModdingAPI.Utility private MB managedArray; private readonly EntityInitializer initializer; //The possible fields are: (index && (array || managedArray)) || initializer + private readonly EcsObjectBase obj; public OptionalRef(NB array, uint index, EGID entityId = default) { @@ -23,6 +25,7 @@ namespace TechbloxModdingAPI.Utility this.entityId = entityId; initializer = default; managedArray = default; + obj = default; } public OptionalRef(MB array, uint index, EGID entityId = default) @@ -33,6 +36,7 @@ namespace TechbloxModdingAPI.Utility this.entityId = entityId; initializer = default; this.array = default; + obj = default; } /// @@ -56,6 +60,7 @@ namespace TechbloxModdingAPI.Utility array = default; index = default; managedArray = default; + this.obj = obj; } /// @@ -66,7 +71,10 @@ namespace TechbloxModdingAPI.Utility { CompRefCache.Default = default; //The default value can be changed by mods if (state == State.Empty) return ref CompRefCache.Default; - if ((state & State.Initializer) != State.Empty) return ref initializer.GetOrAdd(); + // If initializing the entity, check if the component is allowed by the descriptor, otherwise it could cause + // issues in the game with Add() calls running unexpectedly + if ((state & State.Initializer) != State.Empty && obj.AllowedEntityComponents.Contains(typeof(T))) + return ref initializer.GetOrAdd(); if ((state & State.Native) != State.Empty) return ref array[index]; return ref managedArray[index]; }