Compare commits
No commits in common. "5594515a53fcc0142fc7b39f7d9c286ab3c0e357" and "339e850e10e609cfa195025680a04876c98b9a0e" have entirely different histories.
5594515a53
...
339e850e10
|
|
@ -15,7 +15,6 @@ x86/
|
|||
!**/[Pp]ackages/build/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
nupkgs/
|
||||
|
||||
# IDE
|
||||
.vs/
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<!--
|
||||
Directory.Build.props for Godot C# projects consuming OECS.
|
||||
Godot regenerates .csproj files on certain editor actions, which can
|
||||
wipe custom <ItemGroup> entries. This file is never touched by Godot.
|
||||
|
||||
Placement: put this file in the root of your Godot project (next to
|
||||
the .sln file), or in any parent directory.
|
||||
|
||||
You also need a nuget.config next to your .sln with the local feed:
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="local-oecs" value="path/to/oecs-sharp/nupkgs" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
-->
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OECS" Version="0.1.*" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
61
OECS.sln
61
OECS.sln
|
|
@ -1,84 +1,25 @@
|
|||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.SourceGen", "src\OECS.SourceGen\OECS.SourceGen.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS", "src\OECS\OECS.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.Tests", "tests\OECS.Tests\OECS.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blackjack", "examples\Blackjack\Blackjack.csproj", "{85631EDE-F984-4DA6-8EE9-0715AF1204E6}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ namespace OECS;
|
|||
public readonly struct Entity : IEquatable<Entity>
|
||||
{
|
||||
public static Entity Null { get; } // ID=0, Version=0
|
||||
public uint Id { get; } // lower 24-bit identifier
|
||||
public uint Version { get; } // upper 8-bit generation
|
||||
public uint Id { get; } // 24-bit identifier
|
||||
public uint Version { get; } // 8-bit generation
|
||||
public bool IsNull { get; } // true for Entity.Null
|
||||
|
||||
public bool Equals(Entity other);
|
||||
|
|
@ -37,9 +37,6 @@ namespace OECS;
|
|||
|
||||
public class World : IDisposable
|
||||
{
|
||||
// --- Singleton entity ---
|
||||
public static Entity SingletonEntity { get; } // ID=1, Version=1
|
||||
|
||||
// --- Lifecycle ---
|
||||
public World();
|
||||
|
||||
|
|
@ -52,14 +49,11 @@ public class World : IDisposable
|
|||
public void AddComponent<T>(Entity entity, T component) where T : struct;
|
||||
public void RemoveComponent<T>(Entity entity) where T : struct;
|
||||
public ref T GetComponent<T>(Entity entity) where T : struct;
|
||||
public T ReadComponent<T>(Entity entity) where T : struct;
|
||||
public bool TryGetComponent<T>(Entity entity, out T value) where T : struct;
|
||||
public bool HasComponent<T>(Entity entity) where T : struct;
|
||||
|
||||
// --- Singleton ---
|
||||
public void SetSingleton<T>(T component) where T : struct;
|
||||
public ref T GetSingleton<T>() where T : struct;
|
||||
public T ReadSingleton<T>() where T : struct;
|
||||
public bool HasSingleton<T>() where T : struct;
|
||||
public void RemoveSingleton<T>() where T : struct;
|
||||
|
||||
|
|
@ -69,19 +63,12 @@ public class World : IDisposable
|
|||
// --- Query Execution ---
|
||||
public void ForEach<T1>(
|
||||
QueryDescriptor query,
|
||||
ForEachAction<T1> action) where T1 : struct;
|
||||
Action<Entity, ref T1> action) where T1 : struct;
|
||||
|
||||
// Overloads for 2–6 component types:
|
||||
public void ForEach<T1, T2>(QueryDescriptor, ForEachAction<T1, T2>)
|
||||
public void ForEach<T1, T2>(QueryDescriptor, Action<Entity, ref T1, ref T2>)
|
||||
where T1 : struct where T2 : struct;
|
||||
public void ForEach<T1, T2, T3>(QueryDescriptor, ForEachAction<T1, T2, T3>)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
public void ForEach<T1, T2, T3, T4>(QueryDescriptor, ForEachAction<T1, T2, T3, T4>)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
|
||||
public void ForEach<T1, T2, T3, T4, T5>(QueryDescriptor, ForEachAction<T1, T2, T3, T4, T5>)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct;
|
||||
public void ForEach<T1, T2, T3, T4, T5, T6>(QueryDescriptor, ForEachAction<T1, T2, T3, T4, T5, T6>)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct;
|
||||
// ... up to T6
|
||||
|
||||
// --- Commands ---
|
||||
public CommandQueue Commands { get; }
|
||||
|
|
@ -91,10 +78,10 @@ public class World : IDisposable
|
|||
public void MarkModified<T>(Entity entity) where T : struct;
|
||||
public void PostChanges();
|
||||
|
||||
public Observable<EntityChange> ObserveEntityChanges();
|
||||
public Observable<EntityChange> ObserveComponentChanges<T>()
|
||||
public IObservable<EntityChange> ObserveEntityChanges();
|
||||
public IObservable<EntityChange> ObserveComponentChanges<T>()
|
||||
where T : struct;
|
||||
public Observable<EntityChange> ObserveQuery(QueryDescriptor query);
|
||||
public IObservable<EntityChange> ObserveQuery(QueryDescriptor query);
|
||||
|
||||
// --- Relationships ---
|
||||
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
|
||||
|
|
@ -107,70 +94,6 @@ public class World : IDisposable
|
|||
|
||||
---
|
||||
|
||||
## ForEachAction Delegates
|
||||
|
||||
Custom delegate types for query iteration with `ref` parameters. The built-in
|
||||
`Action<...>` delegates do not support `ref` parameters.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public delegate void ForEachAction<T1>(Entity entity, ref T1 c1) where T1 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2>(Entity entity, ref T1 c1, ref T2 c2)
|
||||
where T1 : struct where T2 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3, T4>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3, T4, T5>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct;
|
||||
|
||||
public delegate void ForEachAction<T1, T2, T3, T4, T5, T6>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5, ref T6 c6)
|
||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EntityIterator
|
||||
|
||||
Extension methods on `World` for `foreach`-style iteration over queries.
|
||||
Returns `ref struct` iterators that support zero-allocation iteration with
|
||||
`ref` access to components. Supports 1–3 component types.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public static class EntityIterator
|
||||
{
|
||||
public static Select1<T1> Select<T1>(
|
||||
this World world, QueryDescriptor query) where T1 : struct;
|
||||
|
||||
public static Select2<T1, T2> Select<T1, T2>(
|
||||
this World world, QueryDescriptor query)
|
||||
where T1 : struct where T2 : struct;
|
||||
|
||||
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
|
||||
this World world, QueryDescriptor query)
|
||||
where T1 : struct where T2 : struct where T3 : struct;
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```csharp
|
||||
using var iter = world.Select<Position, Velocity>(query);
|
||||
foreach (ref var item in iter)
|
||||
{
|
||||
item.Current1.X += item.Current2.X * dt;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## QueryBuilder
|
||||
|
||||
```csharp
|
||||
|
|
@ -257,7 +180,6 @@ namespace OECS;
|
|||
|
||||
public class SystemGroup
|
||||
{
|
||||
public SystemGroup(World world);
|
||||
public void Add(ISystem system);
|
||||
public void Remove(ISystem system);
|
||||
public void RunTimed(float deltaTime);
|
||||
|
|
@ -290,8 +212,6 @@ public class CommandQueue
|
|||
{
|
||||
public void Enqueue<T>(T command) where T : struct, ICommand;
|
||||
public void ExecuteAll(World world);
|
||||
public void Clear();
|
||||
public void ClearErrors();
|
||||
public int Count { get; }
|
||||
public IReadOnlyList<Exception> Errors { get; }
|
||||
}
|
||||
|
|
@ -313,31 +233,12 @@ public interface IRelationship
|
|||
|
||||
---
|
||||
|
||||
## Relationship\<TSelf, TTarget\>
|
||||
|
||||
A convenience base struct for relationship components. The type parameters are
|
||||
phantom types that differentiate relationship kinds at the type level, enabling
|
||||
type-safe queries and reverse lookups.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
[MessagePackObject]
|
||||
public struct Relationship<TSelf, TTarget> : IRelationship
|
||||
{
|
||||
[Key(0)] public Entity Source { get; set; }
|
||||
[Key(1)] public Entity Target { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## EntityChange
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public readonly struct EntityChange : IEquatable<EntityChange>
|
||||
public readonly struct EntityChange
|
||||
{
|
||||
public Entity Entity { get; }
|
||||
public ChangeKind Kind { get; }
|
||||
|
|
@ -356,56 +257,6 @@ public enum ChangeKind
|
|||
|
||||
---
|
||||
|
||||
## WorldSerializer
|
||||
|
||||
Saves and loads a `World` to/from a MessagePack stream. Components are
|
||||
serialized by their runtime type.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public static class WorldSerializer
|
||||
{
|
||||
public static void Save(World world, Stream stream);
|
||||
public static void Load(World world, Stream stream);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WorldSnapshot / EntitySnapshot / ComponentEntry
|
||||
|
||||
Serialization types used by `WorldSerializer`. These are public but intended
|
||||
primarily for serialization infrastructure.
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
[MessagePackObject]
|
||||
public class WorldSnapshot
|
||||
{
|
||||
[Key(0)] public EntitySnapshot[] Entities { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class EntitySnapshot
|
||||
{
|
||||
[Key(0)] public uint Id { get; set; }
|
||||
[Key(1)] public uint Version { get; set; }
|
||||
[Key(2)] public ComponentEntry[] Components { get; set; }
|
||||
public Entity Entity { get; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class ComponentEntry
|
||||
{
|
||||
[Key(0)] public string TypeName { get; set; }
|
||||
[Key(1)] public byte[] Data { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Example
|
||||
|
||||
```csharp
|
||||
|
|
@ -418,19 +269,13 @@ public struct Position : IMessagePackSerializationCallbackReceiver
|
|||
{
|
||||
[Key(0)] public float X;
|
||||
[Key(1)] public float Y;
|
||||
|
||||
public void OnBeforeSerialize() { }
|
||||
public void OnAfterDeserialize() { }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public struct Velocity : IMessagePackSerializationCallbackReceiver
|
||||
public struct Velocity
|
||||
{
|
||||
[Key(0)] public float X;
|
||||
[Key(1)] public float Y;
|
||||
|
||||
public void OnBeforeSerialize() { }
|
||||
public void OnAfterDeserialize() { }
|
||||
}
|
||||
|
||||
// Define a system
|
||||
|
|
@ -446,8 +291,6 @@ public class MovementSystem : ITickedSystem
|
|||
.Build();
|
||||
}
|
||||
|
||||
public void Run(World world) => Run(world, Tick.Logical());
|
||||
|
||||
public void Run(World world, Tick tick)
|
||||
{
|
||||
float dt = tick.DeltaTime;
|
||||
|
|
@ -463,7 +306,7 @@ public class MovementSystem : ITickedSystem
|
|||
|
||||
// Wire it up
|
||||
var world = new World();
|
||||
var group = new SystemGroup(world);
|
||||
var group = new SystemGroup();
|
||||
group.Add(new MovementSystem(world));
|
||||
|
||||
// Observe changes
|
||||
|
|
@ -491,7 +334,5 @@ These types are implementation details and may change without notice:
|
|||
| `SparseSet<T>` | Dense/sparse array pair for component storage. |
|
||||
| `ComponentStore` | Registry of `SparseSet<T>` instances by type. |
|
||||
| `ChangeBuffer` | Accumulates `EntityChange` during system run, posts to R3 subjects. |
|
||||
| `ChangeSet` | Deduplicated change accumulator within a single batch. |
|
||||
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
|
||||
| `EntityAllocator` | Free-list + bump allocator for entity IDs. |
|
||||
| `QueryExecutor` | Query iteration logic with smallest-set driver optimization. |
|
||||
|
|
@ -45,8 +45,8 @@ signature) and sparse sets (one dense array per component type).
|
|||
**Context:** Entities need to be cheap to copy, comparable, and safe against
|
||||
use-after-free (accessing a recycled entity ID).
|
||||
|
||||
**Decision:** `readonly struct Entity` wrapping a `uint`. Lower 24 bits are the
|
||||
ID, upper 8 bits are the version.
|
||||
**Decision:** `readonly struct Entity` wrapping a `uint`. Upper 24 bits are the
|
||||
ID, lower 8 bits are the version.
|
||||
|
||||
**Rationale:**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Blackjack</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OECS.SourceGen\OECS.SourceGen.csproj"
|
||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Player hits: draws one card from the deck to the player's hand.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct HitCommand : ICommand
|
||||
{
|
||||
public void Execute(World world)
|
||||
{
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
if (state.Phase != GamePhase.PlayerTurn)
|
||||
return;
|
||||
|
||||
DrawCard<PlayerHand>(world);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the top card from the deck and adds it to the given hand.
|
||||
/// </summary>
|
||||
internal static void DrawCard<THand>(World world)
|
||||
where THand : struct
|
||||
{
|
||||
var singletonEntity = World.SingletonEntity;
|
||||
var deckEntity = FindEntity<Deck>(world, singletonEntity);
|
||||
var handEntity = FindEntity<THand>(world, singletonEntity);
|
||||
|
||||
if (deckEntity == Entity.Null || handEntity == Entity.Null)
|
||||
return;
|
||||
|
||||
// Find a card still in the deck.
|
||||
var cardsInDeck = world.GetSources<InDeck>(deckEntity);
|
||||
if (cardsInDeck.Count == 0)
|
||||
return;
|
||||
|
||||
var cardEntity = cardsInDeck.First();
|
||||
|
||||
// Remove from deck, add to hand.
|
||||
world.RemoveComponent<InDeck>(cardEntity);
|
||||
world.AddComponent(cardEntity, new Holds { Source = cardEntity, Target = handEntity });
|
||||
}
|
||||
|
||||
internal static Entity FindEntity<T>(World world, Entity singletonEntity)
|
||||
where T : struct
|
||||
{
|
||||
using var iter = world.Select<T>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != singletonEntity)
|
||||
return iter.CurrentEntity;
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new round after the previous one ended.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct NewRoundCommand : ICommand
|
||||
{
|
||||
public void Execute(World world)
|
||||
{
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
if (state.Phase != GamePhase.RoundOver)
|
||||
return;
|
||||
|
||||
// Clear hands from previous round.
|
||||
var singletonEntity = World.SingletonEntity;
|
||||
var handEntities = new List<Entity>();
|
||||
using (var iter = world.Select<PlayerHand>())
|
||||
{
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != singletonEntity)
|
||||
handEntities.Add(iter.CurrentEntity);
|
||||
}
|
||||
}
|
||||
using (var iter = world.Select<DealerHand>())
|
||||
{
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != singletonEntity)
|
||||
handEntities.Add(iter.CurrentEntity);
|
||||
}
|
||||
}
|
||||
foreach (var hand in handEntities)
|
||||
{
|
||||
var cards = world.GetSources<Holds>(hand);
|
||||
var deckEntity = HitCommand.FindEntity<Deck>(world, singletonEntity);
|
||||
foreach (var card in cards)
|
||||
{
|
||||
world.RemoveComponent<Holds>(card);
|
||||
world.AddComponent(card, new InDeck { Source = card, Target = deckEntity });
|
||||
}
|
||||
}
|
||||
|
||||
state.Phase = GamePhase.Betting;
|
||||
state.Result = RoundResult.None;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Places a bet and advances the game to the dealing phase.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct PlaceBetCommand : ICommand
|
||||
{
|
||||
[Key(0)] public int Amount;
|
||||
|
||||
public void Execute(World world)
|
||||
{
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
if (state.Phase != GamePhase.Betting)
|
||||
return;
|
||||
|
||||
if (Amount < 1 || Amount > state.Chips)
|
||||
return;
|
||||
|
||||
state.CurrentBet = Amount;
|
||||
state.Chips -= Amount;
|
||||
state.Phase = GamePhase.Dealing;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Player stands: advance to the dealer's turn.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct StandCommand : ICommand
|
||||
{
|
||||
public void Execute(World world)
|
||||
{
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
if (state.Phase != GamePhase.PlayerTurn)
|
||||
return;
|
||||
|
||||
state.Phase = GamePhase.DealerTurn;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// A playing card with a suit and rank.
|
||||
/// One entity per card.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Card
|
||||
{
|
||||
[Key(0)] public Suit Suit;
|
||||
[Key(1)] public Rank Rank;
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component marking the dealer's hand entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct DealerHand { }
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component marking the deck entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Deck { }
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Relationship from a hand entity to a card entity,
|
||||
/// representing that the hand holds this card.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Holds : IRelationship
|
||||
{
|
||||
[Key(0)] public Entity Source { get; set; }
|
||||
[Key(1)] public Entity Target { get; set; }
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Relationship from a card entity to the deck entity,
|
||||
/// representing that the card is in the deck.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct InDeck : IRelationship
|
||||
{
|
||||
[Key(0)] public Entity Source { get; set; }
|
||||
[Key(1)] public Entity Target { get; set; }
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component marking the player's hand entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct PlayerHand { }
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
namespace Blackjack;
|
||||
|
||||
public enum Rank : byte
|
||||
{
|
||||
Ace = 1,
|
||||
Two = 2,
|
||||
Three = 3,
|
||||
Four = 4,
|
||||
Five = 5,
|
||||
Six = 6,
|
||||
Seven = 7,
|
||||
Eight = 8,
|
||||
Nine = 9,
|
||||
Ten = 10,
|
||||
Jack = 11,
|
||||
Queen = 12,
|
||||
King = 13
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
namespace Blackjack;
|
||||
|
||||
public enum Suit : byte
|
||||
{
|
||||
Clubs = 0,
|
||||
Diamonds = 1,
|
||||
Hearts = 2,
|
||||
Spades = 3
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Mulberry32 PRNG — a fast, high-quality 32-bit random number generator.
|
||||
/// Seed is stored in the ECS GameState singleton.
|
||||
/// </summary>
|
||||
public static class Mulberry32
|
||||
{
|
||||
/// <summary>
|
||||
/// Advances the state and returns a random float in [0, 1).
|
||||
/// </summary>
|
||||
public static float NextFloat(ref uint state)
|
||||
{
|
||||
state += 0x6D2B79F5u;
|
||||
uint z = state;
|
||||
z = (z ^ (z >> 15)) * (z | 1u);
|
||||
z ^= z + (z ^ (z >> 7)) * (z | 61u);
|
||||
return (z ^ (z >> 14)) / (float)uint.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the state and returns a random integer in [min, max].
|
||||
/// </summary>
|
||||
public static int NextInt(ref uint state, int min, int max)
|
||||
{
|
||||
return (int)(NextFloat(ref state) * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static void Main()
|
||||
{
|
||||
var world = new World();
|
||||
|
||||
// ── Initialize game state ─────────────────────────────────────
|
||||
|
||||
// Use a seed based on current time, or a fixed seed for reproducibility.
|
||||
uint seed = (uint)Environment.TickCount;
|
||||
// Uncomment the next line for a deterministic game:
|
||||
// seed = 12345u;
|
||||
|
||||
world.SetSingleton(new GameState
|
||||
{
|
||||
Phase = GamePhase.Betting,
|
||||
Result = RoundResult.None,
|
||||
Chips = 100,
|
||||
CurrentBet = 0,
|
||||
RoundNumber = 1,
|
||||
Seed = seed
|
||||
});
|
||||
world.PostChanges();
|
||||
|
||||
// ── Wire up systems ───────────────────────────────────────────
|
||||
|
||||
var group = new SystemGroup(world);
|
||||
group.Add(new DeckSetupSystem());
|
||||
group.Add(new DealSystem());
|
||||
group.Add(new PlayerBustCheckSystem());
|
||||
group.Add(new DealerSystem());
|
||||
group.Add(new RenderSystem());
|
||||
|
||||
// ── Game loop ─────────────────────────────────────────────────
|
||||
|
||||
group.RunLogical();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
|
||||
// Check for game over (out of chips).
|
||||
if (state.Chips <= 0 && state.Phase == GamePhase.Betting)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" You're out of chips! Game over.");
|
||||
break;
|
||||
}
|
||||
|
||||
switch (state.Phase)
|
||||
{
|
||||
case GamePhase.Betting:
|
||||
var input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
continue;
|
||||
if (input.Trim().ToLower() == "quit")
|
||||
return;
|
||||
|
||||
if (int.TryParse(input.Trim(), out var bet))
|
||||
{
|
||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = bet });
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(" Invalid bet. Enter a number.");
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case GamePhase.PlayerTurn:
|
||||
var choice = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(choice))
|
||||
continue;
|
||||
if (choice.Trim().ToLower() == "quit")
|
||||
return;
|
||||
|
||||
switch (choice.Trim().ToLower())
|
||||
{
|
||||
case "h":
|
||||
case "hit":
|
||||
world.Commands.Enqueue(new HitCommand());
|
||||
break;
|
||||
case "s":
|
||||
case "stand":
|
||||
world.Commands.Enqueue(new StandCommand());
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine(" Invalid choice. Enter H or S.");
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
case GamePhase.RoundOver:
|
||||
Console.ReadLine();
|
||||
ref var roundState = ref world.GetSingleton<GameState>();
|
||||
roundState.RoundNumber++;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
world.PostChanges();
|
||||
world.Commands.Enqueue(new NewRoundCommand());
|
||||
break;
|
||||
|
||||
default:
|
||||
// Dealing, DealerTurn — systems handle these automatically.
|
||||
break;
|
||||
}
|
||||
|
||||
group.RunLogical();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace Blackjack;
|
||||
|
||||
public enum GamePhase : byte
|
||||
{
|
||||
Betting = 0,
|
||||
Dealing = 1,
|
||||
PlayerTurn = 2,
|
||||
DealerTurn = 3,
|
||||
RoundOver = 4
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Global game state stored on the singleton entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct GameState
|
||||
{
|
||||
[Key(0)] public GamePhase Phase;
|
||||
[Key(1)] public RoundResult Result;
|
||||
[Key(2)] public int Chips;
|
||||
[Key(3)] public int CurrentBet;
|
||||
[Key(4)] public int RoundNumber;
|
||||
[Key(5)] public uint Seed;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
namespace Blackjack;
|
||||
|
||||
public enum RoundResult : byte
|
||||
{
|
||||
None = 0,
|
||||
PlayerBust = 1,
|
||||
DealerBust = 2,
|
||||
PlayerWin = 3,
|
||||
DealerWin = 4,
|
||||
Push = 5,
|
||||
Blackjack = 6
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Deals initial two cards to player and dealer when entering the dealing phase.
|
||||
/// Advances to player turn after dealing.
|
||||
/// </summary>
|
||||
public class DealSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.Dealing)
|
||||
return;
|
||||
|
||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
||||
|
||||
// Deal two cards to player, then two to dealer.
|
||||
HitCommand.DrawCard<PlayerHand>(world);
|
||||
HitCommand.DrawCard<PlayerHand>(world);
|
||||
HitCommand.DrawCard<DealerHand>(world);
|
||||
HitCommand.DrawCard<DealerHand>(world);
|
||||
|
||||
mutableState.Phase = GamePhase.PlayerTurn;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class PlayerHandTag { public static readonly PlayerHand Instance = new(); }
|
||||
internal static class DealerHandTag { public static readonly DealerHand Instance = new(); }
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Dealer draws cards until reaching 17 or higher,
|
||||
/// then resolves the round result.
|
||||
/// </summary>
|
||||
public class DealerSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.DealerTurn)
|
||||
return;
|
||||
|
||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
||||
|
||||
// Dealer must hit on 16 and below, stand on 17+.
|
||||
int dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
|
||||
while (dealerTotal < 17)
|
||||
{
|
||||
HitCommand.DrawCard<DealerHand>(world);
|
||||
dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
|
||||
}
|
||||
|
||||
int playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
|
||||
|
||||
mutableState.Phase = GamePhase.RoundOver;
|
||||
|
||||
if (dealerTotal > 21)
|
||||
{
|
||||
mutableState.Result = RoundResult.DealerBust;
|
||||
mutableState.Chips += mutableState.CurrentBet * 2;
|
||||
}
|
||||
else if (playerTotal > dealerTotal)
|
||||
{
|
||||
mutableState.Result = RoundResult.PlayerWin;
|
||||
mutableState.Chips += mutableState.CurrentBet * 2;
|
||||
}
|
||||
else if (dealerTotal > playerTotal)
|
||||
{
|
||||
mutableState.Result = RoundResult.DealerWin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Push: return the bet.
|
||||
mutableState.Result = RoundResult.Push;
|
||||
mutableState.Chips += mutableState.CurrentBet;
|
||||
}
|
||||
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the deck (52 cards + deck/hand entities) and shuffles
|
||||
/// using mulberry32 with the seed from the GameState singleton.
|
||||
/// Runs once at the start of each round (during Dealing phase).
|
||||
/// </summary>
|
||||
public class DeckSetupSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.Dealing)
|
||||
return;
|
||||
|
||||
// Only create deck entities if they don't exist yet.
|
||||
var singletonEntity = World.SingletonEntity;
|
||||
bool hasDeck = false;
|
||||
using (var iter = world.Select<Deck>())
|
||||
{
|
||||
hasDeck = iter.MoveNext() && iter.CurrentEntity != singletonEntity;
|
||||
}
|
||||
|
||||
if (!hasDeck)
|
||||
{
|
||||
// Create deck entity.
|
||||
var deckEntity = world.CreateEntity();
|
||||
world.AddComponent(deckEntity, new Deck());
|
||||
|
||||
// Create all 52 cards.
|
||||
foreach (Suit suit in Enum.GetValues<Suit>())
|
||||
{
|
||||
foreach (Rank rank in Enum.GetValues<Rank>())
|
||||
{
|
||||
var cardEntity = world.CreateEntity();
|
||||
world.AddComponent(cardEntity, new Card { Suit = suit, Rank = rank });
|
||||
world.AddComponent(cardEntity, new InDeck { Source = cardEntity, Target = deckEntity });
|
||||
}
|
||||
}
|
||||
|
||||
// Create hand entities.
|
||||
var playerHand = world.CreateEntity();
|
||||
world.AddComponent(playerHand, new PlayerHand());
|
||||
|
||||
var dealerHand = world.CreateEntity();
|
||||
world.AddComponent(dealerHand, new DealerHand());
|
||||
}
|
||||
|
||||
// Shuffle the deck using mulberry32 with the current seed.
|
||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
||||
var deckEntity2 = HitCommand.FindEntity<Deck>(world, singletonEntity);
|
||||
var cards = world.GetSources<InDeck>(deckEntity2).ToArray();
|
||||
Shuffle(world, deckEntity2, cards, ref mutableState.Seed);
|
||||
}
|
||||
|
||||
private static void Shuffle(World world, Entity deckEntity, Entity[] cardEntities, ref uint seed)
|
||||
{
|
||||
// Fisher-Yates shuffle using mulberry32.
|
||||
for (int i = cardEntities.Length - 1; i > 0; i--)
|
||||
{
|
||||
int j = Mulberry32.NextInt(ref seed, 0, i);
|
||||
|
||||
// Swap the InDeck relationship targets (cards are always in the deck,
|
||||
// so there's nothing to swap except the cards themselves — but we
|
||||
// shuffle the card order conceptually by removing and re-adding
|
||||
// InDeck components in shuffled order). Actually, since InDeck
|
||||
// is just a tag, we just re-shuffle the order in the source collection.
|
||||
// The simplest approach: no need to swap component data; we just
|
||||
// need to ensure the cards are iterated in shuffled order.
|
||||
// We'll swap the card entities in the array.
|
||||
(cardEntities[i], cardEntities[j]) = (cardEntities[j], cardEntities[i]);
|
||||
}
|
||||
|
||||
// Now remove all InDeck and re-add in shuffled order so GetSources
|
||||
// returns them in shuffled order.
|
||||
for (int i = cardEntities.Length - 1; i >= 0; i--)
|
||||
{
|
||||
world.RemoveComponent<InDeck>(cardEntities[i]);
|
||||
}
|
||||
for (int i = 0; i < cardEntities.Length; i++)
|
||||
{
|
||||
world.AddComponent(cardEntities[i], new InDeck { Source = cardEntities[i], Target = deckEntity });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Helper to calculate the blackjack value of a hand.
|
||||
/// Aces count as 11 unless that would bust, then they count as 1.
|
||||
/// </summary>
|
||||
public static class HandUtil
|
||||
{
|
||||
public static int CalculateHand(World world, DealerHand hand)
|
||||
{
|
||||
return CalculateHandImpl(world, hand);
|
||||
}
|
||||
|
||||
public static int CalculateHand(World world, PlayerHand hand)
|
||||
{
|
||||
return CalculateHandImpl(world, hand);
|
||||
}
|
||||
|
||||
private static int CalculateHandImpl<T>(World world, T handTag)
|
||||
where T : struct
|
||||
{
|
||||
// Find the hand entity.
|
||||
Entity handEntity = Entity.Null;
|
||||
using (var iter = world.Select<T>())
|
||||
{
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != World.SingletonEntity)
|
||||
{
|
||||
handEntity = iter.CurrentEntity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (handEntity == Entity.Null)
|
||||
return 0;
|
||||
|
||||
var cards = world.GetSources<Holds>(handEntity);
|
||||
int total = 0;
|
||||
int aceCount = 0;
|
||||
|
||||
foreach (var cardEntity in cards)
|
||||
{
|
||||
var card = world.ReadComponent<Card>(cardEntity);
|
||||
int value = card.Rank switch
|
||||
{
|
||||
Rank.Ace => 11,
|
||||
Rank.Jack => 10,
|
||||
Rank.Queen => 10,
|
||||
Rank.King => 10,
|
||||
_ => (int)card.Rank
|
||||
};
|
||||
|
||||
if (card.Rank == Rank.Ace)
|
||||
aceCount++;
|
||||
|
||||
total += value;
|
||||
}
|
||||
|
||||
// Downgrade aces from 11 to 1 as needed.
|
||||
while (total > 21 && aceCount > 0)
|
||||
{
|
||||
total -= 10;
|
||||
aceCount--;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the player's hand total after each hit.
|
||||
/// Busts the player if they exceed 21.
|
||||
/// </summary>
|
||||
public class PlayerBustCheckSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.PlayerTurn)
|
||||
return;
|
||||
|
||||
var total = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
|
||||
if (total <= 21)
|
||||
return;
|
||||
|
||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
||||
mutableState.Phase = GamePhase.RoundOver;
|
||||
mutableState.Result = RoundResult.PlayerBust;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
using OECS;
|
||||
|
||||
namespace Blackjack;
|
||||
|
||||
/// <summary>
|
||||
/// Renders the current game state to the console.
|
||||
/// </summary>
|
||||
public class RenderSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" Blackjack");
|
||||
Console.WriteLine(" ═════════");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($" Chips: {state.Chips} | Round: {state.RoundNumber}");
|
||||
Console.WriteLine();
|
||||
|
||||
// Show dealer's hand.
|
||||
var dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
|
||||
Console.Write(" Dealer: ");
|
||||
if (state.Phase == GamePhase.PlayerTurn)
|
||||
{
|
||||
// Hide first card (hole card) during player's turn.
|
||||
Console.Write("?? ");
|
||||
PrintHand(world, DealerHandTag.Instance, skipFirst: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
PrintHand(world, DealerHandTag.Instance);
|
||||
Console.Write($" ({dealerTotal})");
|
||||
}
|
||||
Console.WriteLine();
|
||||
|
||||
// Show player's hand.
|
||||
var playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
|
||||
Console.Write(" Player: ");
|
||||
PrintHand(world, PlayerHandTag.Instance);
|
||||
Console.Write($" ({playerTotal})");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
|
||||
// Status line.
|
||||
switch (state.Phase)
|
||||
{
|
||||
case GamePhase.Betting:
|
||||
Console.Write($" Enter bet (1–{state.Chips}): ");
|
||||
break;
|
||||
case GamePhase.PlayerTurn:
|
||||
Console.Write(" [H]it or [S]tand: ");
|
||||
break;
|
||||
case GamePhase.RoundOver:
|
||||
Console.WriteLine($" {ResultText(state.Result)}");
|
||||
Console.WriteLine();
|
||||
Console.Write(" Press Enter for next round...");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintHand(World world, DealerHand hand, bool skipFirst = false)
|
||||
{
|
||||
PrintHandImpl(world, hand, skipFirst);
|
||||
}
|
||||
|
||||
private static void PrintHand(World world, PlayerHand hand)
|
||||
{
|
||||
PrintHandImpl(world, hand, skipFirst: false);
|
||||
}
|
||||
|
||||
private static void PrintHandImpl<T>(World world, T handTag, bool skipFirst)
|
||||
where T : struct
|
||||
{
|
||||
Entity handEntity = Entity.Null;
|
||||
using (var iter = world.Select<T>())
|
||||
{
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != World.SingletonEntity)
|
||||
{
|
||||
handEntity = iter.CurrentEntity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (handEntity == Entity.Null)
|
||||
return;
|
||||
|
||||
var cards = world.GetSources<Holds>(handEntity);
|
||||
var ordered = cards.OrderByDescending(c =>
|
||||
{
|
||||
var card = world.ReadComponent<Card>(c);
|
||||
return (int)card.Rank;
|
||||
}).ToList();
|
||||
|
||||
bool first = true;
|
||||
foreach (var cardEntity in ordered)
|
||||
{
|
||||
if (skipFirst && first)
|
||||
{
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
first = false;
|
||||
var card = world.ReadComponent<Card>(cardEntity);
|
||||
Console.Write(CardToString(card));
|
||||
Console.Write(" ");
|
||||
}
|
||||
}
|
||||
|
||||
private static string CardToString(Card card)
|
||||
{
|
||||
var suit = card.Suit switch
|
||||
{
|
||||
Suit.Hearts => "♥",
|
||||
Suit.Diamonds => "♦",
|
||||
Suit.Clubs => "♣",
|
||||
Suit.Spades => "♠",
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
var rank = card.Rank switch
|
||||
{
|
||||
Rank.Ace => "A",
|
||||
Rank.Jack => "J",
|
||||
Rank.Queen => "Q",
|
||||
Rank.King => "K",
|
||||
_ => ((int)card.Rank).ToString()
|
||||
};
|
||||
|
||||
return $"{rank}{suit}";
|
||||
}
|
||||
|
||||
private static string ResultText(RoundResult result) => result switch
|
||||
{
|
||||
RoundResult.PlayerBust => "You bust! Dealer wins.",
|
||||
RoundResult.DealerBust => "Dealer busts! You win!",
|
||||
RoundResult.PlayerWin => "You win!",
|
||||
RoundResult.DealerWin => "Dealer wins.",
|
||||
RoundResult.Push => "Push — it's a tie!",
|
||||
RoundResult.Blackjack => "Blackjack!",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
|
@ -50,8 +50,8 @@ public static class Program
|
|||
// ── Wire up systems ───────────────────────────────────────────
|
||||
|
||||
var group = new SystemGroup(world);
|
||||
group.Add(new WinCheckSystem());
|
||||
group.Add(new RenderSystem());
|
||||
group.Add(new WinCheckSystem(world));
|
||||
group.Add(new RenderSystem(world));
|
||||
|
||||
// ── Game loop ─────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ namespace TicTacToe;
|
|||
/// </summary>
|
||||
public class RenderSystem : ISystem
|
||||
{
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public RenderSystem(World world)
|
||||
{
|
||||
Query = world.Query().With<Cell>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
|
|
@ -18,7 +25,8 @@ public class RenderSystem : ISystem
|
|||
grid[r, c] = '.';
|
||||
|
||||
// Fill in placed marks using the iterator API.
|
||||
using var markIter = world.Select<Cell, Mark>();
|
||||
var markQuery = world.Query().With<Cell>().With<Mark>().Build();
|
||||
using var markIter = world.Select<Cell, Mark>(markQuery);
|
||||
while (markIter.MoveNext())
|
||||
{
|
||||
grid[markIter.Current1.Row, markIter.Current1.Col] =
|
||||
|
|
|
|||
|
|
@ -8,19 +8,24 @@ namespace TicTacToe;
|
|||
/// </summary>
|
||||
public class WinCheckSystem : ISystem
|
||||
{
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public WinCheckSystem(World world)
|
||||
{
|
||||
Query = world.Query().With<Cell>().With<Mark>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
{
|
||||
// Check game status before doing any work. Use ReadSingleton
|
||||
// to avoid auto-marking GameState as modified when we bail early.
|
||||
if (world.ReadSingleton<GameState>().Status != GameStatus.Playing)
|
||||
return;
|
||||
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
|
||||
if (state.Status != GameStatus.Playing)
|
||||
return;
|
||||
|
||||
// Build a 3×3 grid of marks using the iterator API.
|
||||
var grid = new Player[3, 3];
|
||||
|
||||
using var iter = world.Select<Cell, Mark>();
|
||||
using var iter = world.Select<Cell, Mark>(Query);
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
grid[iter.Current1.Row, iter.Current1.Col] = iter.Current2.Player;
|
||||
|
|
|
|||
|
|
@ -12,11 +12,6 @@
|
|||
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OECS.SourceGen\OECS.SourceGen.csproj"
|
||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Data\board.csv">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<!-- Local feed for OECS development builds.
|
||||
Run `dotnet pack src/OECS/OECS.csproj -c Release -o ./nupkgs`
|
||||
to produce the package. -->
|
||||
<add key="local-oecs" value="./nupkgs" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
|
||||
namespace OECS.SourceGen;
|
||||
|
||||
/// <summary>
|
||||
/// Incremental source generator that discovers all component types used with
|
||||
/// <see cref="OECS.World"/> and generates a strongly-typed registry so that
|
||||
/// <see cref="OECS.WorldSerializer"/> can save/load without reflection.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly HashSet<string> _componentApiMethods = new()
|
||||
{
|
||||
// Component management
|
||||
"AddComponent",
|
||||
"RemoveComponent",
|
||||
"HasComponent",
|
||||
"GetComponent",
|
||||
"TryGetComponent",
|
||||
"ReadComponent",
|
||||
"MarkModified",
|
||||
|
||||
// Singletons
|
||||
"SetSingleton",
|
||||
"GetSingleton",
|
||||
"ReadSingleton",
|
||||
"HasSingleton",
|
||||
"RemoveSingleton",
|
||||
|
||||
// Observability
|
||||
"ObserveComponentChanges",
|
||||
|
||||
// Query building
|
||||
"With",
|
||||
"Without",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _forEachNames = new()
|
||||
{
|
||||
"ForEach",
|
||||
};
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Collect all generic method invocations that reference component types.
|
||||
var invocations = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: IsCandidateInvocation,
|
||||
transform: ExtractComponentType)
|
||||
.Where(t => t.FullyQualifiedName != null)
|
||||
.Select((t, _) => (t.FullyQualifiedName!, t.AssemblyQualifiedName!));
|
||||
|
||||
// Also collect ForEach type arguments from the World class.
|
||||
var forEachCalls = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: IsForEachCandidate,
|
||||
transform: ExtractForEachTypes)
|
||||
.Where(list => list.Length > 0)
|
||||
.SelectMany((list, _) => list);
|
||||
|
||||
var allTypes = invocations.Collect().Combine(forEachCalls.Collect());
|
||||
|
||||
context.RegisterSourceOutput(allTypes, GenerateRegistry);
|
||||
}
|
||||
|
||||
private static bool IsCandidateInvocation(SyntaxNode node, CancellationToken _)
|
||||
{
|
||||
if (node is not InvocationExpressionSyntax invocation)
|
||||
return false;
|
||||
|
||||
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
if (memberAccess.Name is GenericNameSyntax genericName)
|
||||
{
|
||||
return _componentApiMethods.Contains(genericName.Identifier.Text);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsForEachCandidate(SyntaxNode node, CancellationToken _)
|
||||
{
|
||||
if (node is not InvocationExpressionSyntax invocation)
|
||||
return false;
|
||||
|
||||
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
var name = memberAccess.Name is GenericNameSyntax gn
|
||||
? gn.Identifier.Text
|
||||
: memberAccess.Name.Identifier.Text;
|
||||
return _forEachNames.Contains(name);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static (string? FullyQualifiedName, string? AssemblyQualifiedName) ExtractComponentType(
|
||||
GeneratorSyntaxContext ctx, CancellationToken _)
|
||||
{
|
||||
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
||||
return default;
|
||||
|
||||
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
|
||||
return default;
|
||||
|
||||
if (memberAccess.Name is not GenericNameSyntax genericName)
|
||||
return default;
|
||||
|
||||
var typeArg = genericName.TypeArgumentList.Arguments.FirstOrDefault();
|
||||
if (typeArg == null)
|
||||
return default;
|
||||
|
||||
var symbolInfo = ctx.SemanticModel.GetSymbolInfo(typeArg);
|
||||
if (symbolInfo.Symbol is not INamedTypeSymbol typeSymbol)
|
||||
return default;
|
||||
|
||||
// Only public struct types are valid components.
|
||||
if (!typeSymbol.IsValueType || typeSymbol.IsAbstract)
|
||||
return default;
|
||||
if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
|
||||
return default;
|
||||
|
||||
var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
// Assembly-qualified name for stable serialization (without assembly version).
|
||||
var aqn = typeSymbol.ContainingAssembly != null
|
||||
? $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}, {typeSymbol.ContainingAssembly.Name}"
|
||||
: fqn;
|
||||
|
||||
return (fqn, aqn);
|
||||
}
|
||||
|
||||
private static ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> ExtractForEachTypes(
|
||||
GeneratorSyntaxContext ctx, CancellationToken _)
|
||||
{
|
||||
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
||||
return ImmutableArray<(string, string)>.Empty;
|
||||
|
||||
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
|
||||
return ImmutableArray<(string, string)>.Empty;
|
||||
|
||||
if (memberAccess.Name is not GenericNameSyntax genericName)
|
||||
return ImmutableArray<(string, string)>.Empty;
|
||||
|
||||
var types = ImmutableArray.CreateBuilder<(string, string)>();
|
||||
foreach (var typeArg in genericName.TypeArgumentList.Arguments)
|
||||
{
|
||||
var symbolInfo = ctx.SemanticModel.GetSymbolInfo(typeArg);
|
||||
if (symbolInfo.Symbol is INamedTypeSymbol typeSymbol &&
|
||||
typeSymbol.IsValueType && !typeSymbol.IsAbstract &&
|
||||
typeSymbol.DeclaredAccessibility == Accessibility.Public)
|
||||
{
|
||||
var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var aqn = typeSymbol.ContainingAssembly != null
|
||||
? $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}, {typeSymbol.ContainingAssembly.Name}"
|
||||
: fqn;
|
||||
types.Add((fqn, aqn));
|
||||
}
|
||||
}
|
||||
|
||||
return types.ToImmutable();
|
||||
}
|
||||
|
||||
private static void GenerateRegistry(SourceProductionContext context,
|
||||
(ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> Left,
|
||||
ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> Right) data)
|
||||
{
|
||||
var (invocations, forEachTypes) = data;
|
||||
|
||||
// Collect unique types by fully-qualified name, deduplicating.
|
||||
var typeMap = new Dictionary<string, (string Fqn, string Aqn)>();
|
||||
foreach (var t in invocations)
|
||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName);
|
||||
foreach (var t in forEachTypes)
|
||||
{
|
||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName);
|
||||
}
|
||||
|
||||
if (typeMap.Count == 0)
|
||||
return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated/>");
|
||||
sb.AppendLine("using System.Runtime.CompilerServices;");
|
||||
sb.AppendLine("using MessagePack;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace OECS");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" file static class ComponentRegistryInitializer");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" [ModuleInitializer]");
|
||||
sb.AppendLine(" public static void Initialize()");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" ComponentRegistry.Initialize(new ComponentDescriptor[]");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
bool first = true;
|
||||
foreach (var (fqn, aqn) in typeMap.Values.OrderBy(t => t.Fqn))
|
||||
{
|
||||
if (!first)
|
||||
sb.AppendLine(",");
|
||||
first = false;
|
||||
|
||||
sb.AppendLine($" new ComponentDescriptor(");
|
||||
sb.AppendLine($" typeName: \"{aqn}\",");
|
||||
sb.AppendLine($" type: typeof({fqn}),");
|
||||
sb.AppendLine($" serialize: obj => MessagePackSerializer.Serialize(({fqn})obj),");
|
||||
sb.AppendLine($" deserializeAndAdd: (world, entity, data) =>");
|
||||
sb.AppendLine($" world.AddComponent(entity, MessagePackSerializer.Deserialize<{fqn}>(data))");
|
||||
sb.Append(" )");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" });");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
context.AddSource("ComponentRegistry.g.cs", sb.ToString());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12</LangVersion>
|
||||
<RootNamespace>OECS.SourceGen</RootNamespace>
|
||||
<AssemblyName>OECS.SourceGen</AssemblyName>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<DevelopmentDependency>true</DevelopmentDependency>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -14,18 +14,8 @@ internal class ChangeBuffer
|
|||
private readonly ChangeSet _pending = new();
|
||||
|
||||
private readonly Subject<EntityChange> _entitySubject = new();
|
||||
private readonly Dictionary<Type, TrackedSubject> _componentSubjects = new();
|
||||
private readonly Dictionary<QueryDescriptor, TrackedSubject> _querySubjects = new();
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="Subject{T}"/> with a subscriber count so that
|
||||
/// subjects with no remaining subscribers can be cleaned up.
|
||||
/// </summary>
|
||||
private sealed class TrackedSubject
|
||||
{
|
||||
public readonly Subject<EntityChange> Subject = new();
|
||||
public int SubscriberCount;
|
||||
}
|
||||
private readonly Dictionary<Type, Subject<EntityChange>> _componentSubjects = new();
|
||||
private readonly Dictionary<QueryDescriptor, Subject<EntityChange>> _querySubjects = new();
|
||||
|
||||
/// <summary>
|
||||
/// The change set currently accumulating. Cleared after each <see cref="Post"/>.
|
||||
|
|
@ -50,18 +40,18 @@ internal class ChangeBuffer
|
|||
// Push to component-specific subjects.
|
||||
if (change.ComponentType != null)
|
||||
{
|
||||
if (_componentSubjects.TryGetValue(change.ComponentType, out var compTracked))
|
||||
if (_componentSubjects.TryGetValue(change.ComponentType, out var compSubject))
|
||||
{
|
||||
compTracked.Subject.OnNext(change);
|
||||
compSubject.OnNext(change);
|
||||
}
|
||||
}
|
||||
|
||||
// Push to matching query subjects.
|
||||
foreach (var (query, tracked) in _querySubjects)
|
||||
foreach (var (query, subject) in _querySubjects)
|
||||
{
|
||||
if (ChangeMatchesQuery(change, query))
|
||||
{
|
||||
tracked.Subject.OnNext(change);
|
||||
subject.OnNext(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,22 +72,12 @@ internal class ChangeBuffer
|
|||
/// </summary>
|
||||
public Observable<EntityChange> ObserveComponentChanges(Type componentType)
|
||||
{
|
||||
if (!_componentSubjects.TryGetValue(componentType, out var tracked))
|
||||
if (!_componentSubjects.TryGetValue(componentType, out var subject))
|
||||
{
|
||||
tracked = new TrackedSubject();
|
||||
_componentSubjects[componentType] = tracked;
|
||||
subject = new Subject<EntityChange>();
|
||||
_componentSubjects[componentType] = subject;
|
||||
}
|
||||
|
||||
tracked.SubscriberCount++;
|
||||
return WrapWithCleanup(tracked.Subject, () =>
|
||||
{
|
||||
tracked.SubscriberCount--;
|
||||
if (tracked.SubscriberCount <= 0)
|
||||
{
|
||||
tracked.Subject.Dispose();
|
||||
_componentSubjects.Remove(componentType);
|
||||
}
|
||||
});
|
||||
return subject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -105,40 +85,12 @@ internal class ChangeBuffer
|
|||
/// </summary>
|
||||
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
|
||||
{
|
||||
if (!_querySubjects.TryGetValue(query, out var tracked))
|
||||
if (!_querySubjects.TryGetValue(query, out var subject))
|
||||
{
|
||||
tracked = new TrackedSubject();
|
||||
_querySubjects[query] = tracked;
|
||||
subject = new Subject<EntityChange>();
|
||||
_querySubjects[query] = subject;
|
||||
}
|
||||
|
||||
tracked.SubscriberCount++;
|
||||
return WrapWithCleanup(tracked.Subject, () =>
|
||||
{
|
||||
tracked.SubscriberCount--;
|
||||
if (tracked.SubscriberCount <= 0)
|
||||
{
|
||||
tracked.Subject.Dispose();
|
||||
_querySubjects.Remove(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an observable so that <paramref name="onLastDispose"/> is called
|
||||
/// when the last subscriber disposes.
|
||||
/// </summary>
|
||||
private static Observable<EntityChange> WrapWithCleanup(
|
||||
Observable<EntityChange> source, Action onLastDispose)
|
||||
{
|
||||
return Observable.Create<EntityChange>(observer =>
|
||||
{
|
||||
var subscription = source.Subscribe(observer);
|
||||
return Disposable.Create(() =>
|
||||
{
|
||||
subscription.Dispose();
|
||||
onLastDispose();
|
||||
});
|
||||
});
|
||||
return subject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -147,14 +99,14 @@ internal class ChangeBuffer
|
|||
public void Dispose()
|
||||
{
|
||||
_entitySubject.Dispose();
|
||||
foreach (var tracked in _componentSubjects.Values)
|
||||
foreach (var subject in _componentSubjects.Values)
|
||||
{
|
||||
tracked.Subject.Dispose();
|
||||
subject.Dispose();
|
||||
}
|
||||
_componentSubjects.Clear();
|
||||
foreach (var tracked in _querySubjects.Values)
|
||||
foreach (var subject in _querySubjects.Values)
|
||||
{
|
||||
tracked.Subject.Dispose();
|
||||
subject.Dispose();
|
||||
}
|
||||
_querySubjects.Clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ namespace OECS;
|
|||
/// Accumulates <see cref="EntityChange"/> entries during a system run.
|
||||
///
|
||||
/// Deduplication: if the same (entity, kind, componentType) change is marked
|
||||
/// multiple times, only one entry is kept. However, if a structurally opposed
|
||||
/// change arrives (Added vs Removed) for the same (entity, componentType),
|
||||
/// the old entry is removed so observers see the full state transition.
|
||||
/// multiple times, only one entry is kept. Entity-level changes (Added/Removed)
|
||||
/// take precedence over component-level changes for the same entity.
|
||||
/// </summary>
|
||||
internal class ChangeSet
|
||||
{
|
||||
private readonly List<EntityChange> _changes = new();
|
||||
private readonly Dictionary<(Entity Entity, ChangeKind Kind, Type? ComponentType), int> _dedup = new();
|
||||
private readonly HashSet<(Entity Entity, ChangeKind Kind, Type? ComponentType)> _dedup = new();
|
||||
|
||||
/// <summary>
|
||||
/// All accumulated changes in insertion order.
|
||||
|
|
@ -76,31 +75,9 @@ internal class ChangeSet
|
|||
private void TryAdd(EntityChange change)
|
||||
{
|
||||
var key = (change.Entity, change.Kind, change.ComponentType);
|
||||
if (_dedup.ContainsKey(key))
|
||||
if (_dedup.Add(key))
|
||||
{
|
||||
// Same (entity, kind, componentType) already recorded — deduplicate.
|
||||
return;
|
||||
}
|
||||
|
||||
// When a component is re-added after being removed within the same
|
||||
// batch, remove the old ComponentRemoved entry so observers see the
|
||||
// full Add → Remove → Add sequence.
|
||||
if (change.Kind == ChangeKind.ComponentAdded && change.ComponentType != null)
|
||||
{
|
||||
var removedKey = (change.Entity, ChangeKind.ComponentRemoved, change.ComponentType);
|
||||
if (_dedup.Remove(removedKey, out int oldIndex))
|
||||
{
|
||||
_changes.RemoveAt(oldIndex);
|
||||
// Adjust indices for all entries that shifted down.
|
||||
foreach (var k in _dedup.Keys.ToList())
|
||||
{
|
||||
if (_dedup[k] > oldIndex)
|
||||
_dedup[k]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_dedup[key] = _changes.Count;
|
||||
_changes.Add(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a component type discovered at compile time by the source generator.
|
||||
/// Used by <see cref="WorldSerializer"/> to save/load components without reflection.
|
||||
/// </summary>
|
||||
public sealed class ComponentDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// The assembly-qualified name of the component type, for stable serialization
|
||||
/// across assemblies.
|
||||
/// </summary>
|
||||
public string TypeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="System.Type"/> of the component.
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a boxed component instance to a MessagePack byte array.
|
||||
/// </summary>
|
||||
public Func<object, byte[]> Serialize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a MessagePack byte array and adds the component to the entity
|
||||
/// via the typed <see cref="World.AddComponent{T}"/> method. No reflection.
|
||||
/// </summary>
|
||||
public Action<World, Entity, byte[]> DeserializeAndAdd { get; }
|
||||
|
||||
public ComponentDescriptor(
|
||||
string typeName,
|
||||
Type type,
|
||||
Func<object, byte[]> serialize,
|
||||
Action<World, Entity, byte[]> deserializeAndAdd)
|
||||
{
|
||||
TypeName = typeName;
|
||||
Type = type;
|
||||
Serialize = serialize;
|
||||
DeserializeAndAdd = deserializeAndAdd;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of component types discovered at compile time by the
|
||||
/// OECS.SourceGen incremental generator. The consuming project's
|
||||
/// generated code populates this via a module initializer, so no
|
||||
/// manual registration is needed.
|
||||
/// </summary>
|
||||
public static class ComponentRegistry
|
||||
{
|
||||
private static ComponentDescriptor[]? _descriptors;
|
||||
private static Dictionary<string, ComponentDescriptor>? _byTypeName;
|
||||
|
||||
/// <summary>
|
||||
/// All discovered component descriptors. Populated automatically
|
||||
/// by the source generator at module initialization.
|
||||
/// </summary>
|
||||
public static ComponentDescriptor[] Descriptors =>
|
||||
_descriptors ?? Array.Empty<ComponentDescriptor>();
|
||||
|
||||
/// <summary>
|
||||
/// Lookup by assembly-qualified type name. Populated automatically
|
||||
/// by the source generator at module initialization.
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, ComponentDescriptor> ByTypeName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_byTypeName == null)
|
||||
{
|
||||
var dict = new Dictionary<string, ComponentDescriptor>();
|
||||
foreach (var desc in Descriptors)
|
||||
dict[desc.TypeName] = desc;
|
||||
_byTypeName = dict;
|
||||
}
|
||||
return _byTypeName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by generated code to register discovered component types.
|
||||
/// Must be called before any serialization occurs.
|
||||
/// </summary>
|
||||
public static void Initialize(ComponentDescriptor[] descriptors)
|
||||
{
|
||||
_descriptors = descriptors;
|
||||
_byTypeName = null; // Rebuild on next access.
|
||||
}
|
||||
}
|
||||
|
|
@ -20,17 +20,6 @@ public static class EntityIterator
|
|||
return new Select1<T1>(world, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator over entities that have a component of type
|
||||
/// <typeparamref name="T1"/>. No <c>Without</c> filter is applied.
|
||||
/// </summary>
|
||||
public static Select1<T1> Select<T1>(
|
||||
this World world)
|
||||
where T1 : struct
|
||||
{
|
||||
return new Select1<T1>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator over entities matching the query, with ref access
|
||||
/// to two component types.
|
||||
|
|
@ -42,18 +31,6 @@ public static class EntityIterator
|
|||
return new Select2<T1, T2>(world, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator over entities that have both components of type
|
||||
/// <typeparamref name="T1"/> and <typeparamref name="T2"/>.
|
||||
/// No <c>Without</c> filter is applied.
|
||||
/// </summary>
|
||||
public static Select2<T1, T2> Select<T1, T2>(
|
||||
this World world)
|
||||
where T1 : struct where T2 : struct
|
||||
{
|
||||
return new Select2<T1, T2>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator over entities matching the query, with ref access
|
||||
/// to three component types.
|
||||
|
|
@ -64,18 +41,6 @@ public static class EntityIterator
|
|||
{
|
||||
return new Select3<T1, T2, T3>(world, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an iterator over entities that have all three components of type
|
||||
/// <typeparamref name="T1"/>, <typeparamref name="T2"/>, and
|
||||
/// <typeparamref name="T3"/>. No <c>Without</c> filter is applied.
|
||||
/// </summary>
|
||||
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
|
||||
this World world)
|
||||
where T1 : struct where T2 : struct where T3 : struct
|
||||
{
|
||||
return new Select3<T1, T2, T3>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@ namespace OECS;
|
|||
|
||||
/// <summary>
|
||||
/// A system that runs on every tick.
|
||||
/// Systems receive the world in their <see cref="Run"/> method.
|
||||
/// Systems declare a query and receive the world in their <see cref="Run"/> method.
|
||||
/// </summary>
|
||||
public interface ISystem
|
||||
{
|
||||
/// <summary>
|
||||
/// The query that defines which entities this system operates on.
|
||||
/// </summary>
|
||||
QueryDescriptor Query { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the system logic for this tick.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -23,20 +23,4 @@
|
|||
<PackageReference Include="R3" Version="1.2.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Bundle the source generator into the NuGet package as an analyzer.
|
||||
Consumers get compile-time component discovery automatically. -->
|
||||
<ItemGroup>
|
||||
<None Include="..\OECS.SourceGen\bin\$(Configuration)\netstandard2.0\OECS.SourceGen.dll"
|
||||
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Ensure the source generator is built before packing.
|
||||
Skipped when NoBuild property is set to true. -->
|
||||
<Target Name="BuildSourceGenerator" BeforeTargets="GenerateNuspec"
|
||||
Condition="'$(NoBuild)' != 'true'">
|
||||
<MSBuild Projects="..\OECS.SourceGen\OECS.SourceGen.csproj"
|
||||
Targets="Build"
|
||||
Properties="Configuration=$(Configuration)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -129,55 +129,21 @@ internal static class QueryExecutor
|
|||
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||
if (set3 == null) return;
|
||||
|
||||
// Pick the smallest set as the driver.
|
||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count;
|
||||
|
||||
if (c2 <= c1 && c2 <= c3)
|
||||
{
|
||||
var dense = set2.Dense;
|
||||
var denseEntities = set2.DenseEntities;
|
||||
var count = set2.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (c3 <= c1 && c3 <= c2)
|
||||
{
|
||||
var dense = set3.Dense;
|
||||
var denseEntities = set3.DenseEntities;
|
||||
var count = set3.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2, T3, T4>(
|
||||
ComponentStore store,
|
||||
|
|
@ -194,63 +160,10 @@ internal static class QueryExecutor
|
|||
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
||||
if (set4 == null) return;
|
||||
|
||||
// Pick the smallest set as the driver.
|
||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count;
|
||||
int min = Math.Min(Math.Min(c1, c2), Math.Min(c3, c4));
|
||||
|
||||
if (min == c2)
|
||||
{
|
||||
var dense = set2.Dense;
|
||||
var denseEntities = set2.DenseEntities;
|
||||
var count = set2.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c3)
|
||||
{
|
||||
var dense = set3.Dense;
|
||||
var denseEntities = set3.DenseEntities;
|
||||
var count = set3.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c4)
|
||||
{
|
||||
var dense = set4.Dense;
|
||||
var denseEntities = set4.DenseEntities;
|
||||
var count = set4.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
|
|
@ -258,11 +171,11 @@ internal static class QueryExecutor
|
|||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2, T3, T4, T5>(
|
||||
ComponentStore store,
|
||||
|
|
@ -281,83 +194,10 @@ internal static class QueryExecutor
|
|||
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
|
||||
if (set5 == null) return;
|
||||
|
||||
// Pick the smallest set as the driver.
|
||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count, c5 = set5.Count;
|
||||
int min = Math.Min(Math.Min(Math.Min(c1, c2), Math.Min(c3, c4)), c5);
|
||||
|
||||
if (min == c2)
|
||||
{
|
||||
var dense = set2.Dense;
|
||||
var denseEntities = set2.DenseEntities;
|
||||
var count = set2.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c3)
|
||||
{
|
||||
var dense = set3.Dense;
|
||||
var denseEntities = set3.DenseEntities;
|
||||
var count = set3.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity), ref set5.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c4)
|
||||
{
|
||||
var dense = set4.Dense;
|
||||
var denseEntities = set4.DenseEntities;
|
||||
var count = set4.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i], ref set5.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c5)
|
||||
{
|
||||
var dense = set5.Dense;
|
||||
var denseEntities = set5.DenseEntities;
|
||||
var count = set5.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref dense[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
|
|
@ -366,11 +206,11 @@ internal static class QueryExecutor
|
|||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ForEach<T1, T2, T3, T4, T5, T6>(
|
||||
ComponentStore store,
|
||||
|
|
@ -391,105 +231,10 @@ internal static class QueryExecutor
|
|||
var set6 = store.GetSet(typeof(T6)) as SparseSet<T6>;
|
||||
if (set6 == null) return;
|
||||
|
||||
// Pick the smallest set as the driver.
|
||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count, c5 = set5.Count, c6 = set6.Count;
|
||||
int min = Math.Min(Math.Min(Math.Min(c1, c2), Math.Min(c3, c4)), Math.Min(c5, c6));
|
||||
|
||||
if (min == c2)
|
||||
{
|
||||
var dense = set2.Dense;
|
||||
var denseEntities = set2.DenseEntities;
|
||||
var count = set2.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!set6.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c3)
|
||||
{
|
||||
var dense = set3.Dense;
|
||||
var denseEntities = set3.DenseEntities;
|
||||
var count = set3.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!set6.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c4)
|
||||
{
|
||||
var dense = set4.Dense;
|
||||
var denseEntities = set4.DenseEntities;
|
||||
var count = set4.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!set6.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i], ref set5.Get(entity), ref set6.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c5)
|
||||
{
|
||||
var dense = set5.Dense;
|
||||
var denseEntities = set5.DenseEntities;
|
||||
var count = set5.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set6.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref dense[i], ref set6.Get(entity));
|
||||
}
|
||||
}
|
||||
else if (min == c6)
|
||||
{
|
||||
var dense = set6.Dense;
|
||||
var denseEntities = set6.DenseEntities;
|
||||
var count = set6.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
if (entity.Id == SingletonId) continue;
|
||||
if (!set1.Contains(entity)) continue;
|
||||
if (!set2.Contains(entity)) continue;
|
||||
if (!set3.Contains(entity)) continue;
|
||||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref dense[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var dense = set1.Dense;
|
||||
var denseEntities = set1.DenseEntities;
|
||||
var count = set1.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var entity = denseEntities[i];
|
||||
|
|
@ -499,9 +244,9 @@ internal static class QueryExecutor
|
|||
if (!set4.Contains(entity)) continue;
|
||||
if (!set5.Contains(entity)) continue;
|
||||
if (!set6.Contains(entity)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
||||
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||
continue;
|
||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ namespace OECS;
|
|||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Relationship<TSelf, TTarget> : IRelationship
|
||||
where TSelf : struct where TTarget : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity that owns this relationship component.
|
||||
|
|
|
|||
|
|
@ -180,7 +180,6 @@ internal class SparseSet<T> : ISparseSet where T : struct
|
|||
{
|
||||
_sparse[_denseEntities[i].Id] = -1;
|
||||
}
|
||||
Array.Clear(_denseEntities, 0, _count);
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -188,17 +188,6 @@ public class World : IDisposable
|
|||
// If replacing an existing relationship, remove the old index entry first.
|
||||
if (component is IRelationship newRel)
|
||||
{
|
||||
// Guard against mismatched Source: the relationship must be stored
|
||||
// on the entity it claims as its Source, otherwise the reverse
|
||||
// index becomes corrupted.
|
||||
if (newRel.Source != entity)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Relationship of type {typeof(T).Name} has Source={newRel.Source} " +
|
||||
$"but is being added to entity {entity}. " +
|
||||
$"The Source must match the entity the component is added to.");
|
||||
}
|
||||
|
||||
if (_components.TryGet<T>(entity, out var old))
|
||||
{
|
||||
var oldRel = (IRelationship)(object)old;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ namespace OECS;
|
|||
/// <summary>
|
||||
/// Saves and loads a <see cref="World"/> to/from a MessagePack stream.
|
||||
///
|
||||
/// Uses the compile-time generated <see cref="ComponentRegistry"/> to
|
||||
/// serialize and deserialize components without reflection. All component
|
||||
/// types used with the World API are discovered by the OECS.SourceGen
|
||||
/// incremental generator at build time.
|
||||
/// Components are serialized by their runtime type. The caller must
|
||||
/// ensure all component types used in the world are resolvable via
|
||||
/// <see cref="Type.GetType(string)"/> at load time.
|
||||
/// </summary>
|
||||
public static class WorldSerializer
|
||||
{
|
||||
|
|
@ -19,9 +18,9 @@ public static class WorldSerializer
|
|||
{
|
||||
var entityComponents = new Dictionary<Entity, List<ComponentEntry>>();
|
||||
|
||||
foreach (var desc in ComponentRegistry.Descriptors)
|
||||
foreach (var ct in world.Components.ComponentTypes)
|
||||
{
|
||||
var set = world.Components.GetSet(desc.Type);
|
||||
var set = world.Components.GetSet(ct);
|
||||
if (set == null || set.Count == 0) continue;
|
||||
|
||||
var entities = set.GetDenseEntities();
|
||||
|
|
@ -38,8 +37,8 @@ public static class WorldSerializer
|
|||
|
||||
list.Add(new ComponentEntry
|
||||
{
|
||||
TypeName = desc.TypeName,
|
||||
Data = desc.Serialize(component)
|
||||
TypeName = ct.AssemblyQualifiedName!,
|
||||
Data = MessagePackSerializer.Serialize(ct, component)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -68,11 +67,6 @@ public static class WorldSerializer
|
|||
{
|
||||
var snapshot = MessagePackSerializer.Deserialize<WorldSnapshot>(stream);
|
||||
|
||||
// Build a lookup by type name for O(1) descriptor resolution.
|
||||
var lookup = new Dictionary<string, ComponentDescriptor>();
|
||||
foreach (var desc in ComponentRegistry.Descriptors)
|
||||
lookup[desc.TypeName] = desc;
|
||||
|
||||
foreach (var es in snapshot.Entities)
|
||||
{
|
||||
var entity = world.CreateEntity(es.Entity);
|
||||
|
|
@ -87,14 +81,18 @@ public static class WorldSerializer
|
|||
|
||||
foreach (var ce in es.Components)
|
||||
{
|
||||
if (!lookup.TryGetValue(ce.TypeName, out var desc))
|
||||
throw new InvalidOperationException(
|
||||
var type = Type.GetType(ce.TypeName)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Unknown component type '{ce.TypeName}'. " +
|
||||
$"Ensure the component type is used with the World " +
|
||||
$"API so the source generator can discover it.");
|
||||
$"Ensure the assembly is loaded.");
|
||||
|
||||
// Deserialize and add in one typed operation — no reflection.
|
||||
desc.DeserializeAndAdd(world, entity, ce.Data);
|
||||
var component = MessagePackSerializer.Deserialize(type, ce.Data)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Failed to deserialize component of type '{ce.TypeName}'.");
|
||||
|
||||
var method = typeof(World).GetMethod(nameof(World.AddComponent))!
|
||||
.MakeGenericMethod(type);
|
||||
method.Invoke(world, [entity, component]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,10 +249,13 @@ public class CommandTests
|
|||
private readonly World _world;
|
||||
private readonly SpawnCommand _command;
|
||||
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public CommandEnqueueSystem(World world, SpawnCommand command)
|
||||
{
|
||||
_world = world;
|
||||
_command = command;
|
||||
Query = world.Query().With<TestPosition>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
|
|
@ -263,17 +266,18 @@ public class CommandTests
|
|||
|
||||
private class EntityCountObserver : ISystem
|
||||
{
|
||||
private readonly QueryDescriptor _query;
|
||||
public int SeenCount { get; private set; }
|
||||
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public EntityCountObserver(World world)
|
||||
{
|
||||
_query = world.Query().With<TestPosition>().Build();
|
||||
Query = world.Query().With<TestPosition>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
{
|
||||
world.ForEach(_query, (Entity e, ref TestPosition pos) =>
|
||||
world.ForEach(Query, (Entity e, ref TestPosition pos) =>
|
||||
{
|
||||
SeenCount++;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,9 +23,4 @@
|
|||
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OECS.SourceGen\OECS.SourceGen.csproj"
|
||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -175,11 +175,11 @@ public class SystemTests
|
|||
|
||||
private class MovementSystem : ITickedSystem
|
||||
{
|
||||
private readonly QueryDescriptor _query;
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public MovementSystem(World world)
|
||||
{
|
||||
_query = world.Query().With<Position>().With<Velocity>().Build();
|
||||
Query = world.Query().With<Position>().With<Velocity>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world) => Run(world, Tick.Logical());
|
||||
|
|
@ -187,7 +187,7 @@ public class SystemTests
|
|||
public void Run(World world, Tick tick)
|
||||
{
|
||||
float dt = tick.DeltaTime;
|
||||
world.ForEach(_query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
||||
world.ForEach(Query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
||||
{
|
||||
pos.X += vel.X * dt;
|
||||
pos.Y += vel.Y * dt;
|
||||
|
|
@ -232,11 +232,13 @@ public class SystemTests
|
|||
|
||||
private class TestLogicalSystem : ITickedSystem
|
||||
{
|
||||
public QueryDescriptor Query { get; }
|
||||
public bool WasCalled { get; private set; }
|
||||
public Tick ReceivedTick { get; private set; }
|
||||
|
||||
public TestLogicalSystem(World world)
|
||||
{
|
||||
Query = world.Query().With<Position>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world) => Run(world, Tick.Logical());
|
||||
|
|
@ -270,10 +272,13 @@ public class SystemTests
|
|||
private readonly int _id;
|
||||
private readonly List<int> _order;
|
||||
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public OrderTrackingSystem(World world, int id, List<int> order)
|
||||
{
|
||||
_id = id;
|
||||
_order = order;
|
||||
Query = world.Query().With<Position>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
|
|
|
|||
|
|
@ -268,9 +268,12 @@ public class ReactivityTests
|
|||
{
|
||||
private readonly World _world;
|
||||
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public EntityCreatorSystem(World world)
|
||||
{
|
||||
_world = world;
|
||||
Query = world.Query().With<Position>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ public struct ChildOf { }
|
|||
public struct ParentOf { }
|
||||
public struct OwnedBy { }
|
||||
public struct MemberOf { }
|
||||
public struct Thing { }
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -154,20 +153,20 @@ public class RelationshipTests
|
|||
var owner = world.CreateEntity();
|
||||
var group = world.CreateEntity();
|
||||
|
||||
world.AddComponent(entity, new Relationship<OwnedBy, Thing>
|
||||
world.AddComponent(entity, new Relationship<OwnedBy, object>
|
||||
{
|
||||
Source = entity, Target = owner
|
||||
});
|
||||
world.AddComponent(entity, new Relationship<MemberOf, Thing>
|
||||
world.AddComponent(entity, new Relationship<MemberOf, object>
|
||||
{
|
||||
Source = entity, Target = group
|
||||
});
|
||||
|
||||
world.GetSources<Relationship<OwnedBy, Thing>>(owner).Should().BeEquivalentTo([entity]);
|
||||
world.GetSources<Relationship<MemberOf, Thing>>(group).Should().BeEquivalentTo([entity]);
|
||||
world.GetSources<Relationship<OwnedBy, object>>(owner).Should().BeEquivalentTo([entity]);
|
||||
world.GetSources<Relationship<MemberOf, object>>(group).Should().BeEquivalentTo([entity]);
|
||||
|
||||
// Cross-check: OwnedBy sources should not appear in MemberOf lookups.
|
||||
world.GetSources<Relationship<MemberOf, Thing>>(owner).Should().BeEmpty();
|
||||
world.GetSources<Relationship<MemberOf, object>>(owner).Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -148,9 +148,12 @@ public class SingletonTests
|
|||
{
|
||||
private readonly World _world;
|
||||
|
||||
public QueryDescriptor Query { get; }
|
||||
|
||||
public TimeSystem(World world)
|
||||
{
|
||||
_world = world;
|
||||
Query = world.Query().With<GameConfig>().Build();
|
||||
}
|
||||
|
||||
public void Run(World world) => Run(world, Tick.Logical());
|
||||
|
|
|
|||
Loading…
Reference in New Issue