From 84ec16220268b2ff209cac6d93a22afe0d4a9efa Mon Sep 17 00:00:00 2001 From: Julek Kopczewski Date: Sat, 7 Jun 2025 02:40:26 +0200 Subject: [PATCH 1/2] .NETStandard 2.0 support --- src/ECS.Boost/Friflo.Engine.ECS.Boost.csproj | 2 +- src/ECS/Archetype/EntityStore.Archetype.cs | 1 + src/ECS/Base/SchemaUtils.cs | 4 +++ src/ECS/Compat/HashSet.cs | 18 +++++++++++++ src/ECS/Entity/MemberPath.cs | 28 +++++++++++++++++--- src/ECS/Friflo.Engine.ECS.csproj | 2 +- src/ECS/Systems/Extensions/SystemPerf.cs | 4 +++ src/ECS/Utils/TreeUtils.cs | 5 ++++ src/Hub/Engine.Hub.csproj | 2 +- src/Tests-NativeAOT/Tests-NativeAOT.csproj | 1 - 10 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/ECS/Compat/HashSet.cs diff --git a/src/ECS.Boost/Friflo.Engine.ECS.Boost.csproj b/src/ECS.Boost/Friflo.Engine.ECS.Boost.csproj index 2ac010fa0..2ab3c2542 100644 --- a/src/ECS.Boost/Friflo.Engine.ECS.Boost.csproj +++ b/src/ECS.Boost/Friflo.Engine.ECS.Boost.csproj @@ -2,7 +2,7 @@ library - net8.0;net7.0;net6.0;netstandard2.1 + net8.0;net7.0;net6.0;netstandard2.1;netstandard2.0 disable disable Friflo.Engine.ECS diff --git a/src/ECS/Archetype/EntityStore.Archetype.cs b/src/ECS/Archetype/EntityStore.Archetype.cs index 59c150a13..145313b31 100644 --- a/src/ECS/Archetype/EntityStore.Archetype.cs +++ b/src/ECS/Archetype/EntityStore.Archetype.cs @@ -4,6 +4,7 @@ // Hard rule: this file MUST NOT use type: Entity using System; +using Friflo.Engine.ECS.Compat; // ReSharper disable InlineTemporaryVariable // ReSharper disable ArrangeTrailingCommaInMultilineLists diff --git a/src/ECS/Base/SchemaUtils.cs b/src/ECS/Base/SchemaUtils.cs index e089fc71e..99808b1bc 100644 --- a/src/ECS/Base/SchemaUtils.cs +++ b/src/ECS/Base/SchemaUtils.cs @@ -18,7 +18,11 @@ private static bool RegisterComponentTypesByReflection() { if (Platform.IsUnityRuntime) { return true; } +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + return true; +#else return System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled; +#endif } [ExcludeFromCodeCoverage] diff --git a/src/ECS/Compat/HashSet.cs b/src/ECS/Compat/HashSet.cs new file mode 100644 index 000000000..5fd4fc017 --- /dev/null +++ b/src/ECS/Compat/HashSet.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Friflo.Engine.ECS.Compat; + +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER +public static class HashSet +{ + public static bool TryGetValue(this HashSet set, T value, out T actualValue) + { + if (set.Contains(value)) { + actualValue = value; + return true; + } + actualValue = default; + return false; + } +} +#endif diff --git a/src/ECS/Entity/MemberPath.cs b/src/ECS/Entity/MemberPath.cs index 555fa53b7..2bfb58b2a 100644 --- a/src/ECS/Entity/MemberPath.cs +++ b/src/ECS/Entity/MemberPath.cs @@ -104,6 +104,10 @@ private MemberPath(MemberPathKey key, ComponentType componentType, int structInd private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.GetProperty; +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + private static readonly char[] SplitDotArray = ['.']; +#endif + /// /// Returns a identifying a specific field / property by its /// within the passed . @@ -122,7 +126,11 @@ public static MemberPath Get(Type type, string path) if (Map.TryGetValue(key, out var componentFieldInfo)) { return componentFieldInfo; } +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + var pathItems = path.Split(SplitDotArray, StringSplitOptions.RemoveEmptyEntries); +#else var pathItems = path.Split('.', StringSplitOptions.RemoveEmptyEntries); +#endif var memberInfos = new MemberInfo[pathItems.Length]; var memberType = type; bool canWrite = true; @@ -188,10 +196,24 @@ public static MemberPath Get(Type type, string path) // See ThrowIfTypeNeverValidGenericArgument() at: // https://github.com/dotnet/runtime/blob/4f5c6938d09e935830492c006aa8381611b65ad8/src/libraries/System.Private.CoreLib/src/System/RuntimeType.cs#L736 - private static bool IsInvalidType(Type type) { - return type.IsPointer || type.IsByRef || type == typeof(void) || - type.IsByRefLike; // type is a ref struct. E.g. Span<> +private static bool IsInvalidType(Type type) +{ + if (type.IsPointer || type.IsByRef || type == typeof(void)) + return true; +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + if (type.IsByRef) + return true; + if (type.IsGenericType) + { + var genericTypeDef = type.GetGenericTypeDefinition(); + if (genericTypeDef == typeof(Span<>) || genericTypeDef == typeof(ReadOnlySpan<>)) + return true; } + return false; +#else + return type.IsByRefLike; // type is a ref struct. E.g. Span<> +#endif +} // ReSharper disable UnusedMember.Local [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Not called for NativeAOT")] diff --git a/src/ECS/Friflo.Engine.ECS.csproj b/src/ECS/Friflo.Engine.ECS.csproj index 1fd4ceb4e..fa5b2068b 100644 --- a/src/ECS/Friflo.Engine.ECS.csproj +++ b/src/ECS/Friflo.Engine.ECS.csproj @@ -2,7 +2,7 @@ library - net8.0;net7.0;net6.0;netstandard2.1 + net8.0;net7.0;net6.0;netstandard2.1;netstandard2.0 disable disable Friflo.Engine.ECS diff --git a/src/ECS/Systems/Extensions/SystemPerf.cs b/src/ECS/Systems/Extensions/SystemPerf.cs index e5db3d5c9..20e843005 100644 --- a/src/ECS/Systems/Extensions/SystemPerf.cs +++ b/src/ECS/Systems/Extensions/SystemPerf.cs @@ -92,7 +92,11 @@ public PerfResource () { [ExcludeFromCodeCoverage] internal static long GetAllocatedBytes() { +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + return 0; +#else return Platform.IsUnityRuntime ? 0 : GC.GetAllocatedBytesForCurrentThread(); +#endif } } diff --git a/src/ECS/Utils/TreeUtils.cs b/src/ECS/Utils/TreeUtils.cs index fa94f27aa..27d292f42 100644 --- a/src/ECS/Utils/TreeUtils.cs +++ b/src/ECS/Utils/TreeUtils.cs @@ -80,7 +80,12 @@ private static void DuplicateChildren(Entity entity, Entity clone, EntityStore s public static AddDataEntitiesResult AddDataEntitiesToEntity(Entity targetEntity, IReadOnlyList dataEntities) { var entityCount = dataEntities.Count; +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + var childEntities = new HashSet (); + childEntities.EnsureCapacity(entityCount); +#else var childEntities = new HashSet (entityCount); +#endif var oldToNewPid = new Dictionary(entityCount); var newToOldPid = new Dictionary(entityCount); var store = targetEntity.Store; diff --git a/src/Hub/Engine.Hub.csproj b/src/Hub/Engine.Hub.csproj index e772da1cc..a61ccf6fd 100644 --- a/src/Hub/Engine.Hub.csproj +++ b/src/Hub/Engine.Hub.csproj @@ -2,7 +2,7 @@ library - net8.0;netstandard2.1 + net8.0;netstandard2.1;netstandard2.0 disable disable Friflo.Engine.Hub diff --git a/src/Tests-NativeAOT/Tests-NativeAOT.csproj b/src/Tests-NativeAOT/Tests-NativeAOT.csproj index a856e958c..6f94e6d56 100644 --- a/src/Tests-NativeAOT/Tests-NativeAOT.csproj +++ b/src/Tests-NativeAOT/Tests-NativeAOT.csproj @@ -2,7 +2,6 @@ Exe - true From 4cc171ae8cd0aff060a7fe303cc737d3548d6257 Mon Sep 17 00:00:00 2001 From: Julek Kopczewski Date: Sun, 8 Jun 2025 01:53:41 +0200 Subject: [PATCH 2/2] Replacing HashSet with Dictionary on .NET Standard 2.0 --- src/ECS/Archetype/EntityStore.Archetype.cs | 5 ++++- src/ECS/Archetype/EntityStore.cs | 8 ++++++++ src/ECS/Compat/HashSet.cs | 18 ------------------ 3 files changed, 12 insertions(+), 19 deletions(-) delete mode 100644 src/ECS/Compat/HashSet.cs diff --git a/src/ECS/Archetype/EntityStore.Archetype.cs b/src/ECS/Archetype/EntityStore.Archetype.cs index 145313b31..273def6f2 100644 --- a/src/ECS/Archetype/EntityStore.Archetype.cs +++ b/src/ECS/Archetype/EntityStore.Archetype.cs @@ -4,7 +4,6 @@ // Hard rule: this file MUST NOT use type: Entity using System; -using Friflo.Engine.ECS.Compat; // ReSharper disable InlineTemporaryVariable // ReSharper disable ArrangeTrailingCommaInMultilineLists @@ -187,7 +186,11 @@ internal static void AddArchetype (EntityStoreBase store, Archetype archetype) } store.archs[store.archsCount] = archetype; store.archsCount++; +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + store.archSet.Add(archetype.key, archetype.key); +#else store.archSet.Add(archetype.key); +#endif } #endregion } diff --git a/src/ECS/Archetype/EntityStore.cs b/src/ECS/Archetype/EntityStore.cs index 6c1a32fca..a6a5ba753 100644 --- a/src/ECS/Archetype/EntityStore.cs +++ b/src/ECS/Archetype/EntityStore.cs @@ -78,7 +78,11 @@ public abstract partial class EntityStoreBase // --- archetypes [Browse(Never)] internal Archetype[] archs; // 8 - array of all archetypes. never null [Browse(Never)] private int archsCount; // 4 - number of archetypes +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + [Browse(Never)] private readonly Dictionary archSet; // 8 - Set<> to get archetypes by key +#else [Browse(Never)] private readonly HashSet archSet; // 8 - Set<> to get archetypes by key +#endif /// The default has no and .
/// Its . is always 0 ().
[Browse(Never)] internal readonly Archetype defaultArchetype; // 8 - default archetype. has no components & tags @@ -142,7 +146,11 @@ internal static class Static protected EntityStoreBase() { archs = new Archetype[2]; +#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER + archSet = new Dictionary(ArchetypeKeyEqualityComparer.Instance); +#else archSet = new HashSet(ArchetypeKeyEqualityComparer.Instance); +#endif var config = GetArchetypeConfig(this); defaultArchetype = new Archetype(config); searchKey = new ArchetypeKey(); diff --git a/src/ECS/Compat/HashSet.cs b/src/ECS/Compat/HashSet.cs deleted file mode 100644 index 5fd4fc017..000000000 --- a/src/ECS/Compat/HashSet.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; - -namespace Friflo.Engine.ECS.Compat; - -#if NETSTANDARD && !NETSTANDARD2_1_OR_GREATER -public static class HashSet -{ - public static bool TryGetValue(this HashSet set, T value, out T actualValue) - { - if (set.Contains(value)) { - actualValue = value; - return true; - } - actualValue = default; - return false; - } -} -#endif