From 8eb7c636f3cf993c8e494e6ec16883c379502f6a Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Mon, 22 Dec 2025 21:17:12 +0300 Subject: [PATCH 1/4] Fix assets --- .github/workflows/dotnet.yaml | 2 +- src/Hexecs.Tests/Assets/AssetFilter1Should.cs | 77 ++++++ src/Hexecs.Tests/Hexecs.Tests.csproj | 3 + src/Hexecs.Tests/Mocks/CarAsset.cs | 12 + src/Hexecs/Actors/ActorContext.cs | 2 +- src/Hexecs/Actors/ActorError.cs | 12 +- src/Hexecs/Assets/AssetContext.Dictionary.cs | 1 + src/Hexecs/Assets/AssetContext.Entry.cs | 2 +- src/Hexecs/Assets/AssetContext.cs | 11 +- src/Hexecs/Assets/AssetError.cs | 6 + src/Hexecs/Assets/AssetFilter1.cs | 22 +- src/Hexecs/Assets/AssetFilter2.cs | 22 +- src/Hexecs/Assets/AssetFilter3.cs | 22 +- .../Assets/Components/AssetComponentPool.cs | 222 ++++++++++++++---- .../Assets/Components/IAssetComponentPool.cs | 4 +- 15 files changed, 351 insertions(+), 69 deletions(-) create mode 100644 src/Hexecs.Tests/Assets/AssetFilter1Should.cs create mode 100644 src/Hexecs.Tests/Mocks/CarAsset.cs diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml index a90180a..2075ea4 100644 --- a/.github/workflows/dotnet.yaml +++ b/.github/workflows/dotnet.yaml @@ -25,7 +25,7 @@ jobs: - name: Run tests with coverage run: | cd ./src/Hexecs.Tests/ - dotnet test -c Release --no-build /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=opencover + dotnet test -c Release --no-build /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=opencover /p:Exclude="[*Tests*]*|[*Benchmarks*]*|[*Demo*]* - name: Publish coverage report if: matrix.os == 'ubuntu-latest' && matrix.dotnet-version == '10.0.x' uses: codecov/codecov-action@v3 diff --git a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs new file mode 100644 index 0000000..3e712ed --- /dev/null +++ b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs @@ -0,0 +1,77 @@ +using Hexecs.Tests.Mocks; + +namespace Hexecs.Tests.Assets; + +public sealed class AssetFilter1Should(AssetTestFixture fixture) : IClassFixture +{ + [Fact(DisplayName = "Фильтр ассетов должен содержать все созданные ассеты")] + public void ContainsAllAssets() + { + // arrange + var assetIds = new List(); + + var (context, world) = fixture.CreateAssetContext(loader => + { + for (int i = 1; i < 100; i++) + { + var asset = loader.CreateAsset(new CarAsset(i, i)); + assetIds.Add(asset.Id); + } + }); + + var expectedAssets = assetIds.Select(id => context.GetAsset(id)).ToArray(); + + // act + + var filter = context.Filter(); + var actualActors = filter.ToArray(); + + // assert + + actualActors + .Should() + .Contain(expectedAssets); + + world.Dispose(); + } + + [Fact(DisplayName = "Фильтр ассетов можно перебирать как AssetRef")] + public void AssetFilterShouldEnumerable() + { + // arrange + var expectedIds = new List(); + + var (context, world) = fixture.CreateAssetContext(loader => + { + for (int i = 0; i < 100; i++) + { + var asset = loader.CreateAsset(new CarAsset(i, i)); + expectedIds.Add(asset.Id); + } + }); + + // act + + var filter = context.Filter(); + var actualIds = new List(); + foreach (var asset in filter) + { + actualIds.Add(asset.Id); + } + + // assert + + filter.Length + .Should().Be(expectedIds.Count); + + actualIds + .Should() + .HaveCount(expectedIds.Count); + + actualIds + .Should() + .Contain(expectedIds); + + world.Dispose(); + } +} \ No newline at end of file diff --git a/src/Hexecs.Tests/Hexecs.Tests.csproj b/src/Hexecs.Tests/Hexecs.Tests.csproj index 09067bc..1bb875f 100644 --- a/src/Hexecs.Tests/Hexecs.Tests.csproj +++ b/src/Hexecs.Tests/Hexecs.Tests.csproj @@ -3,6 +3,9 @@ false true + true + opencover + [*Tests*]*,[*Benchmarks*]*,[*Demo*]* diff --git a/src/Hexecs.Tests/Mocks/CarAsset.cs b/src/Hexecs.Tests/Mocks/CarAsset.cs new file mode 100644 index 0000000..ac26ddc --- /dev/null +++ b/src/Hexecs.Tests/Mocks/CarAsset.cs @@ -0,0 +1,12 @@ +using Hexecs.Assets; + +namespace Hexecs.Tests.Mocks; + +public readonly struct CarAsset(int price, int speed) : IAssetComponent +{ + public const string Alias1 = "Alias1"; + public const string Alias2 = "Alias2"; + + public readonly int Price = price; + public readonly int Speed = speed; +} \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.cs b/src/Hexecs/Actors/ActorContext.cs index 90d45a8..f1bc97f 100644 --- a/src/Hexecs/Actors/ActorContext.cs +++ b/src/Hexecs/Actors/ActorContext.cs @@ -171,7 +171,7 @@ public Actor Clone(uint actorId, bool withParent = true) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Actor CreateActor(uint? expectedId = null) { - if (expectedId == Actor.EmptyId) ActorError.WrongId(); + if (expectedId == Actor.EmptyId) ActorError.InvalidId(); var actorId = expectedId ?? GetNextActorId(); AddEntry(actorId); diff --git a/src/Hexecs/Actors/ActorError.cs b/src/Hexecs/Actors/ActorError.cs index 3646140..49d1fc8 100644 --- a/src/Hexecs/Actors/ActorError.cs +++ b/src/Hexecs/Actors/ActorError.cs @@ -142,6 +142,12 @@ public static T QueueNotFound() throw new Exception($"System {TypeOf.GetTypeName()} isn't found"); } + [DoesNotReturn] + public static void InvalidId() + { + throw new Exception("Invalid actor id"); + } + /// /// Генерирует исключение, когда актёр с указанным ключом не найден. /// @@ -255,10 +261,4 @@ public static void ValueNotFound(string name, Type type) { throw new Exception($"Value '{name}' (type {TypeOf.GetTypeName(type)}) isn't found"); } - - [DoesNotReturn] - public static void WrongId() - { - throw new Exception("Wrong actor id"); - } } \ No newline at end of file diff --git a/src/Hexecs/Assets/AssetContext.Dictionary.cs b/src/Hexecs/Assets/AssetContext.Dictionary.cs index 8cf53e4..46a8fda 100644 --- a/src/Hexecs/Assets/AssetContext.Dictionary.cs +++ b/src/Hexecs/Assets/AssetContext.Dictionary.cs @@ -12,6 +12,7 @@ public int Length private ref Entry AddEntry(uint id) { + if (id == Asset.EmptyId) AssetError.InvalidId(); ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_entries, id, out var exists); if (exists) AssetError.AlreadyExists(id); return ref entry; diff --git a/src/Hexecs/Assets/AssetContext.Entry.cs b/src/Hexecs/Assets/AssetContext.Entry.cs index 632cb23..48b8688 100644 --- a/src/Hexecs/Assets/AssetContext.Entry.cs +++ b/src/Hexecs/Assets/AssetContext.Entry.cs @@ -12,7 +12,7 @@ public void Add(ushort componentId) ArrayUtils.InsertOrCreate(ref _array, _length, componentId); _length++; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ReadOnlySpan AsReadOnlySpan() => _length == 0 ? ReadOnlySpan.Empty diff --git a/src/Hexecs/Assets/AssetContext.cs b/src/Hexecs/Assets/AssetContext.cs index df7d5ea..ca9fb36 100644 --- a/src/Hexecs/Assets/AssetContext.cs +++ b/src/Hexecs/Assets/AssetContext.cs @@ -76,7 +76,14 @@ public Asset GetAsset() where T1 : struct, IAssetComponent { var pool = GetComponentPool(); - if (pool is { Length: > 0 }) return pool.First(); + if (pool != null) + { + var firstId = pool.FirstId(); + if (firstId != Asset.EmptyId) + { + return new Asset(this, firstId); + } + } AssetError.NotFound(); return Asset.Empty; @@ -92,8 +99,6 @@ public Asset GetAsset() public Asset GetAsset(uint assetId) where T1 : struct, IAssetComponent { - Debug.Assert(ExistsAsset(assetId), $"Asset {assetId} isn't found"); - var pool = GetComponentPool(); if (pool == null || !pool.Has(assetId)) AssetError.ComponentNotFound(assetId); diff --git a/src/Hexecs/Assets/AssetError.cs b/src/Hexecs/Assets/AssetError.cs index 1fac443..c9aee92 100644 --- a/src/Hexecs/Assets/AssetError.cs +++ b/src/Hexecs/Assets/AssetError.cs @@ -97,6 +97,12 @@ public static void ConstraintExists() throw new Exception($"Constraint for {TypeOf.GetTypeName()} already exists"); } + [DoesNotReturn] + public static void InvalidId() + { + throw new Exception("Invalid asset id"); + } + /// /// Генерирует исключение при попытке использовать загрузчик ассетов, который уже освобожден. /// diff --git a/src/Hexecs/Assets/AssetFilter1.cs b/src/Hexecs/Assets/AssetFilter1.cs index 2eaa3cb..90618a3 100644 --- a/src/Hexecs/Assets/AssetFilter1.cs +++ b/src/Hexecs/Assets/AssetFilter1.cs @@ -42,6 +42,25 @@ public AssetRef Get(uint assetId) ref _pool1.GetByIndex(entry.Index1)); } + public Asset[] ToArray() + { + var dictionary = _dictionary; + + var count = dictionary.Count; + if (count == 0) return []; + + var assets = new Asset[count]; + var ctx = Context; + + var index = 0; + foreach (var assetId in dictionary.Keys) + { + assets[index++] = new Asset(ctx, assetId); + } + + return assets; + } + private static FrozenDictionary Collect( AssetContext context, AssetComponentPool pool1, @@ -73,7 +92,8 @@ private static FrozenDictionary Collect( length++; } - var result = buffer.ToFrozenDictionary(); + var segment = new ArraySegment>(buffer, 0, length); + var result = segment.ToFrozenDictionary(); bufferPool.Return(buffer, true); diff --git a/src/Hexecs/Assets/AssetFilter2.cs b/src/Hexecs/Assets/AssetFilter2.cs index eeca4eb..5d74b22 100644 --- a/src/Hexecs/Assets/AssetFilter2.cs +++ b/src/Hexecs/Assets/AssetFilter2.cs @@ -46,6 +46,25 @@ ref _pool1.GetByIndex(entry.Index1), ref _pool2.GetByIndex(entry.Index2)); } + public Asset[] ToArray() + { + var dictionary = _dictionary; + + var count = dictionary.Count; + if (count == 0) return []; + + var assets = new Asset[count]; + var ctx = Context; + + var index = 0; + foreach (var assetId in dictionary.Keys) + { + assets[index++] = new Asset(ctx, assetId); + } + + return assets; + } + private static FrozenDictionary Collect( AssetContext context, AssetComponentPool pool1, @@ -81,7 +100,8 @@ private static FrozenDictionary Collect( length++; } - var result = buffer.ToFrozenDictionary(); + var segment = new ArraySegment>(buffer, 0, length); + var result = segment.ToFrozenDictionary(); bufferPool.Return(buffer, true); diff --git a/src/Hexecs/Assets/AssetFilter3.cs b/src/Hexecs/Assets/AssetFilter3.cs index 3c5334d..279d0c9 100644 --- a/src/Hexecs/Assets/AssetFilter3.cs +++ b/src/Hexecs/Assets/AssetFilter3.cs @@ -50,6 +50,25 @@ ref _pool2.GetByIndex(entry.Index2), ref _pool3.GetByIndex(entry.Index3)); } + public Asset[] ToArray() + { + var dictionary = _dictionary; + + var count = dictionary.Count; + if (count == 0) return []; + + var assets = new Asset[count]; + var ctx = Context; + + var index = 0; + foreach (var assetId in dictionary.Keys) + { + assets[index++] = new Asset(ctx, assetId); + } + + return assets; + } + private static FrozenDictionary Collect( AssetContext context, AssetComponentPool pool1, @@ -89,7 +108,8 @@ private static FrozenDictionary Collect( length++; } - var result = buffer.ToFrozenDictionary(); + var segment = new ArraySegment>(buffer, 0, length); + var result = segment.ToFrozenDictionary(); bufferPool.Return(buffer, true); diff --git a/src/Hexecs/Assets/Components/AssetComponentPool.cs b/src/Hexecs/Assets/Components/AssetComponentPool.cs index 3c7bfd0..873144f 100644 --- a/src/Hexecs/Assets/Components/AssetComponentPool.cs +++ b/src/Hexecs/Assets/Components/AssetComponentPool.cs @@ -4,7 +4,11 @@ namespace Hexecs.Assets.Components; internal sealed class AssetComponentPool : IAssetComponentPool where T : struct, IAssetComponent { - private const int EmptySlot = 0; + //private const int EmptySlot = 0; + + private const int PageBits = 12; + private const int PageSize = 1 << PageBits; // 4096 + private const int PageMask = PageSize - 1; public readonly AssetContext Context; @@ -17,103 +21,217 @@ public ushort Id public int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _length; + get => _count; } - private T[] _components; - private int[] _ids; - private int _nextFreeSlot; - private int _length; + private uint[]?[] _sparsePages; + private uint[] _dense; + private T[] _values; + private int _count; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public AssetComponentPool(AssetContext context, int capacity = 8) { Context = context; - capacity = HashHelper.GetPrime(capacity); - _components = ArrayUtils.Create(capacity); - _ids = new int[capacity]; + _sparsePages = new uint[1][]; + _dense = new uint[capacity]; + _values = new T[capacity]; } - public AssetRef First() - { - for (uint i = 0; i < _ids.Length; i++) - { - var slot = _ids[i]; - if (slot == EmptySlot) continue; - - return new AssetRef(Context, i, ref _components[slot]); - } - - return AssetRef.Empty; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint FirstId() => _count > 0 + ? _dense[0] + : Asset.EmptyId; - public ref T Get(uint ownerId) + public ref T Get(uint assetId) { - var slot = TryGetIndex(ownerId); - if (slot != -1 && slot <= _components.Length) + var pageIndex = (int)(assetId >> PageBits); + if ((uint)pageIndex < (uint)_sparsePages.Length) { - return ref _components[slot]; + var page = _sparsePages[pageIndex]; + if (page != null) + { + var denseIndexPlusOne = page[assetId & PageMask]; + if (denseIndexPlusOne != 0) + { + var index = (int)denseIndexPlusOne - 1; + if (_dense[index] == assetId) + { + return ref _values[index]; + } + } + } } - AssetError.ComponentNotFound(ownerId); return ref Unsafe.NullRef(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref T GetByIndex(int index) => ref _components[index]; + public ref T GetByIndex(int index) => ref _values[index]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Has(uint ownerId) => ownerId <= _ids.Length && _ids[ownerId] != EmptySlot; - - public ref T Set(uint ownerId, in T component) + public bool Has(uint assetId) { - var idValue = (int)ownerId; - ArrayUtils.EnsureCapacity(ref _ids, idValue + 1); - - ref var id = ref _ids[idValue]; - if (id != EmptySlot) AssetError.ComponentAlreadyExists(ownerId); + var pageIndex = (int)(assetId >> PageBits); + var pages = _sparsePages; - var slot = Interlocked.Increment(ref _nextFreeSlot); - ArrayUtils.EnsureCapacity(ref _components, _nextFreeSlot); + if ((uint)pageIndex < (uint)pages.Length) + { + var page = pages[pageIndex]; + if (page != null) + { + var denseIndexPlusOne = page[assetId & PageMask]; + return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == assetId; + } + } - id = slot; + return false; + } - ref var reference = ref _components[slot]; - reference = component; + public ref T Set(uint assetId, in T component) + { + var pageIndex = (int)(assetId >> PageBits); + var pages = _sparsePages; - Interlocked.Increment(ref _length); + // Максимально компактная проверка на готовность страницы и места + if ((uint)pageIndex < (uint)pages.Length) + { + var page = pages[pageIndex]; + if (page != null && (uint)_count < (uint)_dense.Length) + { + ref var slot = ref page[assetId & PageMask]; + if (slot == 0) // Чистая вставка (самый частый случай в ECS) + { + var idx = (uint)_count; + slot = idx + 1; + _dense[idx] = assetId; + ref var internalRef = ref _values[idx]; + + _values[idx] = component; + _count++; + + return ref internalRef; + } + + // Если не 0, проверяем на дубликат (чуть медленнее) + if (_dense[slot - 1] == assetId) + { + AssetError.ComponentAlreadyExists(assetId); + } + } + } - return ref reference; + return ref SetSlow(assetId, in component); } - public ref T TryGet(uint ownerId) + public ref T TryGet(uint assetId) { - var slot = TryGetIndex(ownerId); - if (slot != -1 && slot <= _components.Length) + var pageIndex = (int)(assetId >> PageBits); + if ((uint)pageIndex < (uint)_sparsePages.Length) { - return ref _components[slot]; + var page = _sparsePages[pageIndex]; + if (page != null) + { + var denseIndexPlusOne = page[assetId & PageMask]; + if (denseIndexPlusOne != 0) + { + var index = (int)denseIndexPlusOne - 1; + if (_dense[index] == assetId) + { + return ref _values[index]; + } + } + } } return ref Unsafe.NullRef(); } - public int TryGetIndex(uint ownerId) + public int TryGetIndex(uint assetId) { - // ReSharper disable once InvertIf - if (ownerId < _ids.Length) + var pageIndex = (int)(assetId >> PageBits); + var pages = _sparsePages; + + if ((uint)pageIndex < (uint)pages.Length) { - var slot = _ids[ownerId]; - if (slot != EmptySlot) return slot; + var page = pages[pageIndex]; + if (page != null) + { + var slot = page[assetId & PageMask]; + if (slot != 0) + { + var denseIndex = (int)slot - 1; + if (_dense[denseIndex] == assetId) + { + return denseIndex; + } + } + } } return -1; } + private void EnsureDenseCapacity() + { + if (_count >= _dense.Length) + { + var newSize = _dense.Length * 2; + Array.Resize(ref _dense, newSize); + Array.Resize(ref _values, newSize); + } + } + + private void EnsurePageArraySize(int pageIndex) + { + if (pageIndex >= _sparsePages.Length) + { + var newSize = Math.Max(_sparsePages.Length * 2, pageIndex + 1); + Array.Resize(ref _sparsePages, newSize); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private ref T SetSlow(uint assetId, in T component) + { + EnsureDenseCapacity(); + var pageIndex = (int)(assetId >> PageBits); + EnsurePageArraySize(pageIndex); + + ref var page = ref _sparsePages[pageIndex]; + if (page == null) + { + page = ArrayUtils.Create(PageSize); + Array.Clear(page, 0, page.Length); + } + + ref var denseIndexPlusOne = ref page[assetId & PageMask]; + if (denseIndexPlusOne != 0) + { + if (_dense[denseIndexPlusOne - 1] == assetId) + { + AssetError.ComponentAlreadyExists(assetId); + } + } + + var denseIndex = (uint)_count; + denseIndexPlusOne = denseIndex + 1; + _dense[denseIndex] = assetId; + + ref var internalRef = ref _values[denseIndex]; + internalRef = component; + + _count++; + + return ref internalRef; + } + AssetContext IAssetComponentPool.Context { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Context; } - IAssetComponent IAssetComponentPool.Get(uint ownerId) => Get(ownerId); + IAssetComponent IAssetComponentPool.Get(uint assetId) => Get(assetId); } \ No newline at end of file diff --git a/src/Hexecs/Assets/Components/IAssetComponentPool.cs b/src/Hexecs/Assets/Components/IAssetComponentPool.cs index 0fd0a6f..ab571fc 100644 --- a/src/Hexecs/Assets/Components/IAssetComponentPool.cs +++ b/src/Hexecs/Assets/Components/IAssetComponentPool.cs @@ -8,7 +8,7 @@ public interface IAssetComponentPool ushort Id { get; } - IAssetComponent Get(uint ownerId); + IAssetComponent Get(uint assetId); - bool Has(uint ownerId); + bool Has(uint assetId); } \ No newline at end of file From 2caf6a43f3f446ce94379e40069849095148aaf4 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Mon, 22 Dec 2025 21:27:57 +0300 Subject: [PATCH 2/4] Fix assets --- src/Hexecs.Tests/Assets/AssetFilter1Should.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs index 3e712ed..87c08b6 100644 --- a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs +++ b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs @@ -39,38 +39,44 @@ public void ContainsAllAssets() public void AssetFilterShouldEnumerable() { // arrange - var expectedIds = new List(); + var expectedIds = new Dictionary(); var (context, world) = fixture.CreateAssetContext(loader => { - for (int i = 0; i < 100; i++) + for (var i = 0; i < 100; i++) { - var asset = loader.CreateAsset(new CarAsset(i, i)); - expectedIds.Add(asset.Id); + var component = new CarAsset(i, i); + var asset = loader.CreateAsset(component); + + expectedIds.Add(asset.Id, component); } }); // act var filter = context.Filter(); + + // assert + var actualIds = new List(); foreach (var asset in filter) { actualIds.Add(asset.Id); + asset + .Component1 + .Should().Be(expectedIds[asset.Id]); } - // assert - filter.Length .Should().Be(expectedIds.Count); - + actualIds .Should() .HaveCount(expectedIds.Count); actualIds .Should() - .Contain(expectedIds); + .Contain(expectedIds.Keys); world.Dispose(); } From 1453c93c29cc467cf83c638eb04a14301ff2966b Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Mon, 22 Dec 2025 21:31:47 +0300 Subject: [PATCH 3/4] Fix assets --- .github/workflows/dotnet.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml index 2075ea4..a90180a 100644 --- a/.github/workflows/dotnet.yaml +++ b/.github/workflows/dotnet.yaml @@ -25,7 +25,7 @@ jobs: - name: Run tests with coverage run: | cd ./src/Hexecs.Tests/ - dotnet test -c Release --no-build /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=opencover /p:Exclude="[*Tests*]*|[*Benchmarks*]*|[*Demo*]* + dotnet test -c Release --no-build /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=opencover - name: Publish coverage report if: matrix.os == 'ubuntu-latest' && matrix.dotnet-version == '10.0.x' uses: codecov/codecov-action@v3 From 5e6571d063db6a621a3813e3a9bbadc89554ad1d Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Mon, 22 Dec 2025 21:32:09 +0300 Subject: [PATCH 4/4] Fix assets --- .github/workflows/dotnet.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml index a90180a..e238e8d 100644 --- a/.github/workflows/dotnet.yaml +++ b/.github/workflows/dotnet.yaml @@ -25,7 +25,7 @@ jobs: - name: Run tests with coverage run: | cd ./src/Hexecs.Tests/ - dotnet test -c Release --no-build /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=opencover + dotnet test -c Release --no-build /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=opencover /p:Exclude="[*Tests*]*|[*Benchmarks*]*|[*Demo*]*" - name: Publish coverage report if: matrix.os == 'ubuntu-latest' && matrix.dotnet-version == '10.0.x' uses: codecov/codecov-action@v3