feat(oecs): Add interrupt mechanism to block system ticks
Introduce IInterrupt marker interface, IInterruptHandlerCommand for resolving interrupts, and InterruptStore. When a pending interrupt exists at the start of a tick, system execution is skipped until resolved. Only one interrupt of a given type may be pending at a time.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for interrupt types. An interrupt is a signal issued by a
|
||||
/// system that blocks the next tick until a matching <see cref="IInterruptHandlerCommand{TInterrupt}"/>
|
||||
/// resolves it.
|
||||
/// </summary>
|
||||
public interface IInterrupt { }
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// A command that resolves a pending interrupt of type <typeparamref name="TInterrupt"/>.
|
||||
///
|
||||
/// The default <see cref="ICommand.Execute"/> implementation looks up the pending
|
||||
/// interrupt, calls <see cref="TryResolve"/> to let the handler inspect it, and
|
||||
/// resolves the interrupt if the handler returns <c>true</c>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInterrupt">The interrupt type this handler resolves.</typeparam>
|
||||
public interface IInterruptHandlerCommand<TInterrupt> : ICommand
|
||||
where TInterrupt : struct, IInterrupt
|
||||
{
|
||||
void ICommand.Execute(World world)
|
||||
{
|
||||
if (world.TryGetPendingInterrupt<TInterrupt>(out var interrupt))
|
||||
{
|
||||
if (TryResolve(interrupt))
|
||||
{
|
||||
world.ResolveInterrupt<TInterrupt>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the default <see cref="ICommand.Execute"/> with the pending interrupt.
|
||||
/// Return <c>true</c> to resolve and remove the interrupt; <c>false</c> to leave it
|
||||
/// pending for another handler.
|
||||
/// </summary>
|
||||
bool TryResolve(TInterrupt interrupt);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace OECS;
|
||||
|
||||
/// <summary>
|
||||
/// Internal store for pending interrupts. Owned by <see cref="SystemGroup"/>
|
||||
/// and injected into <see cref="World"/> so the handler's default Execute
|
||||
/// can access it.
|
||||
/// </summary>
|
||||
internal class InterruptStore
|
||||
{
|
||||
private readonly Dictionary<Type, object> _interrupts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Stores an interrupt. Only one interrupt of a given type may be pending.
|
||||
/// </summary>
|
||||
public void Add<T>(T interrupt) where T : struct, IInterrupt
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (_interrupts.ContainsKey(type))
|
||||
throw new InvalidOperationException(
|
||||
$"An interrupt of type {type.Name} is already pending.");
|
||||
|
||||
_interrupts[type] = interrupt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve a pending interrupt of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
public bool TryGet<T>(out T interrupt) where T : struct, IInterrupt
|
||||
{
|
||||
if (_interrupts.TryGetValue(typeof(T), out var boxed))
|
||||
{
|
||||
interrupt = (T)boxed;
|
||||
return true;
|
||||
}
|
||||
|
||||
interrupt = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the pending interrupt of type <typeparamref name="T"/>.
|
||||
/// No-op if none is pending.
|
||||
/// </summary>
|
||||
public bool Remove<T>() where T : struct, IInterrupt
|
||||
{
|
||||
return _interrupts.Remove(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if any interrupt is currently pending.
|
||||
/// </summary>
|
||||
public bool HasAny => _interrupts.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// True if an interrupt of type <typeparamref name="T"/> is pending.
|
||||
/// </summary>
|
||||
public bool Has<T>() where T : struct, IInterrupt
|
||||
{
|
||||
return _interrupts.ContainsKey(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all pending interrupts. Used during world reset or serialization load.
|
||||
/// </summary>
|
||||
internal void Clear()
|
||||
{
|
||||
_interrupts.Clear();
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -3,18 +3,25 @@ namespace OECS;
|
||||
/// <summary>
|
||||
/// Manages a group of systems, running them in registration order
|
||||
/// against a specific <see cref="World"/>.
|
||||
///
|
||||
/// Owns the interrupt store — when a pending interrupt exists at the start
|
||||
/// of a tick, system execution is skipped until the interrupt is resolved.
|
||||
/// </summary>
|
||||
public class SystemGroup
|
||||
{
|
||||
private readonly World _world;
|
||||
private readonly List<ISystem> _systems = new();
|
||||
private readonly InterruptStore _interrupts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a system group bound to the given world.
|
||||
/// Injects the interrupt store into the world so <see cref="World.Interrupt{T}"/>
|
||||
/// and <see cref="IInterruptHandlerCommand{TInterrupt}"/> can route through it.
|
||||
/// </summary>
|
||||
public SystemGroup(World world)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_world._interruptStore = _interrupts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -59,9 +66,19 @@ public class SystemGroup
|
||||
private void RunAll(Tick tick)
|
||||
{
|
||||
// Drain commands enqueued before the tick so the first system
|
||||
// sees their effects.
|
||||
// sees their effects. This also processes interrupt handler
|
||||
// commands that may resolve pending interrupts.
|
||||
_world.ExecuteCommands();
|
||||
|
||||
// If an interrupt is still pending after draining commands,
|
||||
// skip all systems for this tick. The world is fully consistent
|
||||
// (the previous tick completed) and is safe to serialize.
|
||||
if (_interrupts.HasAny)
|
||||
{
|
||||
_world.PostChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
_world.BeginBatching();
|
||||
foreach (var system in _systems)
|
||||
{
|
||||
@@ -94,4 +111,4 @@ public class SystemGroup
|
||||
// Post any remaining changes (e.g., from command execution).
|
||||
_world.PostChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-1
@@ -43,6 +43,10 @@ public class World : IDisposable
|
||||
private readonly HashSet<(Entity Entity, Type ComponentType)> _accessedComponents = new();
|
||||
private readonly HashSet<(Entity Entity, Type ComponentType)> _markedModified = new();
|
||||
|
||||
// Interrupt store: injected by SystemGroup. Null when no SystemGroup is managing
|
||||
// this world, in which case Interrupt() is a no-op.
|
||||
internal InterruptStore? _interruptStore;
|
||||
|
||||
public World()
|
||||
{
|
||||
_allocator = new EntityAllocator();
|
||||
@@ -490,6 +494,52 @@ public class World : IDisposable
|
||||
_commands.ExecuteAll(this);
|
||||
}
|
||||
|
||||
// ── Interrupts ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Issues an interrupt of type <typeparamref name="T"/>, blocking the
|
||||
/// next tick from running systems until a matching
|
||||
/// <see cref="IInterruptHandlerCommand{TInterrupt}"/> resolves it.
|
||||
///
|
||||
/// Only one interrupt of a given type may be pending at a time.
|
||||
/// If no <see cref="SystemGroup"/> is managing this world, interrupts
|
||||
/// are silently ignored (calls are no-ops).
|
||||
/// </summary>
|
||||
public void Interrupt<T>(T interrupt) where T : struct, IInterrupt
|
||||
{
|
||||
_interruptStore?.Add(interrupt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an interrupt of type <typeparamref name="T"/> is
|
||||
/// currently pending.
|
||||
/// </summary>
|
||||
public bool HasInterrupt<T>() where T : struct, IInterrupt
|
||||
{
|
||||
return _interruptStore != null && _interruptStore.Has<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a pending interrupt for the handler's default Execute.
|
||||
/// </summary>
|
||||
internal bool TryGetPendingInterrupt<T>(out T interrupt) where T : struct, IInterrupt
|
||||
{
|
||||
if (_interruptStore != null)
|
||||
return _interruptStore.TryGet(out interrupt);
|
||||
|
||||
interrupt = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves and removes a pending interrupt. Called by the default
|
||||
/// <see cref="IInterruptHandlerCommand{TInterrupt}.TryResolve"/> implementation.
|
||||
/// </summary>
|
||||
internal void ResolveInterrupt<T>() where T : struct, IInterrupt
|
||||
{
|
||||
_interruptStore?.Remove<T>();
|
||||
}
|
||||
|
||||
// ── Iteration Lifecycle ──────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -616,4 +666,4 @@ public class World : IDisposable
|
||||
_disposed = true;
|
||||
_changes.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user