feat(docs): Add interrupt system documentation
This commit is contained in:
@@ -67,6 +67,10 @@ public class World : IDisposable
|
||||
public CommandQueue Commands { get; }
|
||||
public void ExecuteCommands();
|
||||
|
||||
// --- Interrupts ---
|
||||
public void Interrupt<T>(T interrupt) where T : struct, IInterrupt;
|
||||
public bool HasInterrupt<T>() where T : struct, IInterrupt;
|
||||
|
||||
// --- Reactivity ---
|
||||
public void MarkModified<T>(Entity entity) where T : struct;
|
||||
public void PostChanges();
|
||||
@@ -322,6 +326,52 @@ 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\<TInterrupt\>
|
||||
|
||||
```csharp
|
||||
namespace OECS;
|
||||
|
||||
public interface IInterruptHandlerCommand<TInterrupt> : 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<ConfirmInterrupt>
|
||||
{
|
||||
[Key(0)] public bool Confirmed;
|
||||
|
||||
public bool TryResolve(ConfirmInterrupt interrupt)
|
||||
{
|
||||
// Do work here. Return false to reject.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IRelationship
|
||||
|
||||
```csharp
|
||||
|
||||
@@ -385,3 +385,53 @@ package is included for compile-time validation.
|
||||
- The `MessagePackAnalyzer` package is a compile-time dependency.
|
||||
- `[Key]` indices should be sequential starting from 0 to avoid null
|
||||
placeholders in the binary output.
|
||||
|
||||
---
|
||||
|
||||
## ADR-013: Interrupts for System-Blocking Prompts
|
||||
|
||||
**Status:** Accepted
|
||||
|
||||
**Context:** Systems sometimes need to pause execution until an external
|
||||
response arrives (e.g., a UI confirmation dialog). The pause must be
|
||||
serialization-safe (the world is fully consistent when blocked) and
|
||||
must not interrupt mid-tick (which would leave partial state).
|
||||
|
||||
**Decision:** Add an interrupt system. Interrupts are issued via
|
||||
`world.Interrupt<T>(interrupt)` and block the **next** tick, not the
|
||||
current one. The interrupt is stored in an `InterruptStore` owned by
|
||||
`SystemGroup`. A matching `IInterruptHandlerCommand<T>` (a regular
|
||||
`ICommand`) resolves the interrupt when executed.
|
||||
|
||||
**Rationale:**
|
||||
|
||||
- Deferring the block to the next tick avoids mid-tick state
|
||||
inconsistency. The current tick completes normally, all mutations
|
||||
are flushed, and the world is serializable before the block takes
|
||||
effect.
|
||||
- The interrupt store lives in `SystemGroup`, not `World`. This keeps
|
||||
`World` lean and means interrupts are a no-op when no `SystemGroup`
|
||||
is managing the world.
|
||||
- Resolution is done via a regular `ICommand` with a default
|
||||
`Execute` implementation. The handler's `TryResolve` method receives
|
||||
the interrupt data and can inspect it before accepting or rejecting
|
||||
the resolution.
|
||||
- One interrupt per type ensures no ambiguity about which handler
|
||||
resolves which interrupt.
|
||||
|
||||
**Alternatives considered:**
|
||||
|
||||
- **Mid-tick blocking:** Would leave partial system state, making
|
||||
serialization and recovery unsafe.
|
||||
- **Storing interrupts in World:** Adds complexity to `World` without
|
||||
benefit — interrupts are a tick-level concern, not a component-level
|
||||
one.
|
||||
|
||||
**Consequences:**
|
||||
|
||||
- Systems that issue interrupts must be prepared for the next tick to
|
||||
be skipped entirely.
|
||||
- Interrupts are transient — they are not serialized and do not survive
|
||||
save/load.
|
||||
- `IInterruptHandlerCommand<T>` types must be `[MessagePackObject]`
|
||||
structs (like all commands) to enable serialization for replay.
|
||||
|
||||
@@ -563,12 +563,70 @@ and design validation. See `docs/testing-games.md` for the testing strategy.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — Interrupts (Week 9)
|
||||
|
||||
**Goal:** System-issued blocking signals resolved by handler commands.
|
||||
|
||||
### 9.1 IInterrupt
|
||||
|
||||
Marker interface for interrupt struct types. Interrupts are not components —
|
||||
they're transient signals stored in the `InterruptStore`.
|
||||
|
||||
### 9.2 IInterruptHandlerCommand\<T\>
|
||||
|
||||
```csharp
|
||||
interface IInterruptHandlerCommand<TInterrupt> : ICommand
|
||||
where TInterrupt : struct, IInterrupt
|
||||
{
|
||||
bool TryResolve(TInterrupt interrupt);
|
||||
}
|
||||
```
|
||||
|
||||
Extends `ICommand` with a default `Execute` that looks up the pending
|
||||
interrupt of type `TInterrupt`, calls `TryResolve`, and resolves the
|
||||
interrupt if the handler returns `true`.
|
||||
|
||||
### 9.3 InterruptStore
|
||||
|
||||
Internal class owned by `SystemGroup`. Maps `Type` → boxed `IInterrupt`.
|
||||
Supports `Add`, `TryGet`, `Remove`, `HasAny`, `Has<T>`, `Clear`.
|
||||
|
||||
### 9.4 World API
|
||||
|
||||
```csharp
|
||||
void Interrupt<T>(T interrupt) where T : struct, IInterrupt;
|
||||
bool HasInterrupt<T>() where T : struct, IInterrupt;
|
||||
```
|
||||
|
||||
Lightweight pass-through to the `InterruptStore` injected by `SystemGroup`.
|
||||
When no `SystemGroup` manages the world, interrupts are no-ops.
|
||||
|
||||
### 9.5 SystemGroup Integration
|
||||
|
||||
At the start of each tick, `SystemGroup.RunAll` drains commands and checks
|
||||
`_interrupts.HasAny`. If pending, the tick skips all systems but still posts
|
||||
changes. Interrupts block the **next** tick, not the current one.
|
||||
|
||||
### 9.6 Tests
|
||||
|
||||
- Interrupt blocks the next tick.
|
||||
- Handler resolves interrupt and unblocks.
|
||||
- Handler rejection leaves interrupt pending.
|
||||
- `HasInterrupt` query works.
|
||||
- Duplicate interrupt type throws.
|
||||
- Multiple interrupt types are independent.
|
||||
- Interrupt is a no-op without SystemGroup.
|
||||
- Commands still execute when blocked.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 1 (Core)
|
||||
└─→ Phase 2 (Queries & Systems)
|
||||
└─→ Phase 3 (Commands)
|
||||
└─→ Phase 9 (Interrupts)
|
||||
└─→ Phase 4 (Relationships)
|
||||
└─→ Phase 5 (Reactivity)
|
||||
└─→ Phase 6 (Singletons)
|
||||
@@ -576,7 +634,8 @@ Phase 1 (Core)
|
||||
└─→ Phase 8 (Polish)
|
||||
```
|
||||
|
||||
Phases 3 and 4 can be done in parallel; Phase 5 depends on both.
|
||||
Interrupts depend on Phase 3 (Commands) because they use `ICommand` for
|
||||
handler commands. They're independent of Phases 4–8.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user