# API Surface: OECS This document describes the public API surface of the OECS library. It serves as a contract for implementation and a reference for consumers. All types reside in the `OECS` namespace unless otherwise noted. --- ## Entity ```csharp namespace OECS; public readonly struct Entity : IEquatable { 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 bool IsNull { get; } // true for Entity.Null public bool Equals(Entity other); public override bool Equals(object? obj); public override int GetHashCode(); public override string ToString(); // "Entity(42:v3)" public static bool operator ==(Entity left, Entity right); public static bool operator !=(Entity left, Entity right); } ``` --- ## World ```csharp namespace OECS; public class World : IDisposable { // --- Lifecycle --- public World(); // --- Entity Management --- public Entity CreateEntity(); public void DestroyEntity(Entity entity); public bool IsAlive(Entity entity); // --- Component Management --- public void AddComponent(Entity entity, T component) where T : struct; public void RemoveComponent(Entity entity) where T : struct; public ref T GetComponent(Entity entity) where T : struct; public T ReadComponent(Entity entity) where T : struct; public bool TryGetComponent(Entity entity, out T value) where T : struct; public bool HasComponent(Entity entity) where T : struct; // --- Singleton --- public void SetSingleton(T component) where T : struct; public ref T GetSingleton() where T : struct; public T ReadSingleton() where T : struct; public bool HasSingleton() where T : struct; public void RemoveSingleton() where T : struct; // --- Queries --- // See WorldQueryExtensions for Select / FindEntity. // --- Commands --- public CommandQueue Commands { get; } public void ExecuteCommands(); // --- Interrupts --- public void Interrupt(T interrupt) where T : struct, IInterrupt; public bool HasInterrupt() where T : struct, IInterrupt; // --- Reactivity --- public void MarkModified(Entity entity) where T : struct; public void PostChanges(); public Observable ObserveEntityChanges(); public Observable ObserveComponentChanges() where T : struct; public Observable ObserveQuery(Query query = default) where T1 : struct; public Observable ObserveQuery(Query query = default) where T1 : struct where T2 : struct; // --- Relationships --- public IReadOnlyCollection GetSources(Entity target) where T : struct, IRelationship; public void ReorderSources(Entity target, IReadOnlyList ordered) where T : struct, IRelationship; // --- Cleanup --- public void Dispose(); } ``` --- ## WorldQueryExtensions Extension methods on `World` providing `foreach`-compatible iteration and entity lookup. `Select` returns `ref struct` enumerators for zero-allocation iteration with `ref` access to components. ```csharp namespace OECS; public static class WorldQueryExtensions { // Select — returns a ref struct enumerator for foreach. public static Select1 Select( this World world, Query query = default) where T1 : struct; // Overloads for 2–6 component types: public static Select2 Select(...) where T1 : struct where T2 : struct; // ... up to Select6 // FindEntity — returns the first matching entity or Entity.Null. public static Entity FindEntity( this World world, Query query = default) where T1 : struct; public static Entity FindEntity( this World world, Query query = default) where T1 : struct where T2 : struct; public static Entity FindEntity( this World world, Query query = default) where T1 : struct where T2 : struct where T3 : struct; } ``` Each ref struct (Select1 through Select6) exposes: - `Entity Entity` — the current entity handle. - `ref T1 Ref1`, `ref T2 Ref2`, ... — mutable references to components. Access is tracked for auto-dirty-marking during a batching scope. - `ref readonly T1 Val1`, `ref readonly T2 Val2`, ... — read-only references. Access is NOT tracked — use these when you only read the component. - `bool MoveNext()` — advances and returns true while items remain. - `GetEnumerator()` — returns `this` for `foreach` compatibility. - `Dispose()` — ends the batching scope (flushes deferred mutations). Usage: ```csharp // Mutating: use RefN to auto-mark as modified. foreach (var it in world.Select()) { it.Ref1.X += it.Val2.X * dt; // Position tracked, Velocity not } // Read-only: use ValN everywhere. foreach (var it in world.Select()) { grid[it.Val1.Row, it.Val1.Col] = it.Val2.Player; } // With Without filter: var query = new Query().Without(); foreach (var it in world.Select(query)) { // it.Ref1, it.Val1 } // Find first entity: var hand = world.FindEntity(); ``` --- ## Query Describes a query over the ECS world. The `With` types are encoded as generic parameters. Optional `Without` filters are added via the fluent API. ```csharp namespace OECS; public struct Query where T1 : struct { public Query Without() where W : struct; } // Overloads for 2–6 With types: public struct Query where T1 : struct where T2 : struct { ... } // ... up to Query ``` Usage: ```csharp // Two required types, one excluded: var query = new Query().Without(); foreach (var it in world.Select(query)) { ... } // Simple scan — no Without filter needed: foreach (var it in world.Select()) { ... } ``` --- ## ISystem A system is a unit of logic that runs during a tick. Implement `RunImpl` with the system logic. Use the `Run` extension method to execute with automatic batching — or register with `SystemGroup` which handles this automatically. ```csharp namespace OECS; public interface ISystem { void RunImpl(World world); } ``` Systems do **not** declare a query on the interface. Instead, they either read singletons directly (`world.ReadSingleton()`) or use the `WorldQueryExtensions` (`Select`, `FindEntity`) with a `Query` they build inline. This keeps the interface minimal and gives systems full flexibility over what they inspect at runtime. The `Run` extension method wraps `RunImpl` in a batching scope so component accesses via `GetComponent` and `RefN` are auto-tracked for modification: ```csharp public static class SystemExtensions { public static void Run(this ISystem system, World world); public static void Run(this ITickedSystem system, World world, Tick tick); } ``` --- ## ITickedSystem An optional extension for systems that need tick metadata (delta time or logical tick marker). ```csharp namespace OECS; public interface ITickedSystem : ISystem { void RunImpl(World world, Tick tick); } ``` `ITickedSystem` must also implement `ISystem.RunImpl(World)` (typically delegating to the ticked overload with a default tick). `SystemGroup` checks each system at runtime and calls the appropriate `Run` extension. --- ## Tick ```csharp namespace OECS; public readonly struct Tick { public TickType Type { get; } public float DeltaTime { get; } public static Tick Timed(float deltaTime); public static Tick Logical(); } public enum TickType { Timed, Logical } ``` --- ## SystemGroup ```csharp namespace OECS; public class SystemGroup { public SystemGroup(World world); public void Add(ISystem system); public void Remove(ISystem system); public void RunTimed(float deltaTime); public void RunLogical(); public int Count { get; } } ``` Systems execute in registration order. `SystemGroup` wraps the entire tick in a batching scope, and each system's `Run` extension adds a nested scope. Commands are drained, pending mutations are flushed, and changes are posted after each system and after the full tick. --- ## ICommand ```csharp namespace OECS; public interface ICommand { void Execute(World world); } ``` Implementations should be `[MessagePackObject]` structs so they can be serialized and replayed. --- ## CommandQueue ```csharp namespace OECS; public class CommandQueue { public void Enqueue(T command) where T : struct, ICommand; public void ExecuteAll(World world); public void Clear(); public void ClearErrors(); public int Count { get; } public IReadOnlyList Errors { get; } } ``` `Enqueue` uses a constrained generic to avoid boxing at the call site. `ExecuteAll` wraps the drain in a batching scope — mutations are deferred and component accesses are auto-tracked. Pending mutations are flushed after each command so chained commands see each other's changes within the same drain cycle. --- ## IInterrupt ```csharp namespace OECS; public interface IInterrupt { } ``` Marker interface for interrupt types. Interrupts are issued by systems to block the next tick until a matching handler command resolves them. --- ## IInterruptHandlerCommand\ ```csharp namespace OECS; public interface IInterruptHandlerCommand : ICommand where TInterrupt : struct, IInterrupt { bool TryResolve(TInterrupt interrupt); } ``` A command that resolves a pending interrupt. The default `ICommand.Execute` looks up the pending interrupt, calls `TryResolve`, and resolves the interrupt if `TryResolve` returns `true`. Usage: ```csharp [MessagePackObject] public struct ConfirmHandler : IInterruptHandlerCommand { [Key(0)] public bool Confirmed; public bool TryResolve(ConfirmInterrupt interrupt) { // Do work here. Return false to reject. return true; } } ``` --- ## IRelationship ```csharp namespace OECS; public interface IRelationship { Entity Target { get; } } ``` --- ## Relationship\ 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 : IRelationship where TSelf : struct where TTarget : struct { [Key(0)] public Entity Target { get; set; } } ``` --- ## EntityChange ```csharp namespace OECS; public readonly struct EntityChange : IEquatable { public Entity Entity { get; } public ChangeKind Kind { get; } public Type? ComponentType { get; } // null for entity-level changes } public enum ChangeKind { EntityAdded, EntityRemoved, ComponentAdded, ComponentRemoved, ComponentModified, RelationshipReordered } ``` --- ## WorldSerializer Saves and loads a `World` to/from a MessagePack stream. Components are serialized by their runtime type using the source-generated `ComponentRegistry`. ```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; } [IgnoreMember] public Entity Entity { get; } } [MessagePackObject] public class ComponentEntry { [Key(0)] public string TypeName { get; set; } [Key(1)] public byte[] Data { get; set; } } ``` --- ## Usage Example Components and commands should be `public record struct` types with explicit fields/properties (not positional syntax). See the `writing-games` skill for the rationale. ```csharp using OECS; // Define components [MessagePackObject] public record struct Position { [Key(0)] public float X; [Key(1)] public float Y; } [MessagePackObject] public record struct Velocity { [Key(0)] public float X; [Key(1)] public float Y; } // Define a system public class MovementSystem : ITickedSystem { public void RunImpl(World world) => RunImpl(world, Tick.Logical()); public void RunImpl(World world, Tick tick) { float dt = tick.DeltaTime; foreach (var it in world.Select()) { it.Ref1.X += it.Val2.X * dt; it.Ref1.Y += it.Val2.Y * dt; } } } // Wire it up var world = new World(); var group = new SystemGroup(world); group.Add(new MovementSystem()); // Create entities var player = world.CreateEntity(); world.AddComponent(player, new Position { X = 0, Y = 0 }); world.AddComponent(player, new Velocity { X = 1, Y = 0 }); // Observe changes via R3 world.ObserveComponentChanges() .Subscribe(change => Console.WriteLine($"{change.Entity} moved")); // Run a tick group.RunTimed(0.016f); // ~60 FPS ``` --- ## Internal Types (not part of public API) These types are implementation details and may change without notice: | Type | Purpose | |---|---| | `SparseSet` | Dense/sparse array pair for component storage. | | `ComponentStore` | Registry of `SparseSet` 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. | | `WorldQueryExtensions` | Extension methods with ref struct iterators and entity lookup. | | `ComponentRegistry` | Source-generated registry of all component types for serialization. | | `ComponentDescriptor` | Source-generated per-type descriptor with serialize/deserialize callbacks. |