feat(docs): Add interrupt system documentation
This commit is contained in:
parent
6207872e45
commit
8a32588c54
|
|
@ -248,3 +248,55 @@ tracking. Outside a batching scope, call `MarkModified` after mutating.
|
||||||
`WorldSerializer.Save/Load` uses the source-generated `ComponentRegistry`.
|
`WorldSerializer.Save/Load` uses the source-generated `ComponentRegistry`.
|
||||||
All component types used with `World` generic methods are automatically
|
All component types used with `World` generic methods are automatically
|
||||||
discovered. Serialization round-trips must be tested — see `testing-games` skill.
|
discovered. Serialization round-trips must be tested — see `testing-games` skill.
|
||||||
|
|
||||||
|
## Interrupts
|
||||||
|
|
||||||
|
Interrupts pause system execution until an external response arrives. A system
|
||||||
|
issues an interrupt, the next tick is skipped, and a handler command resolves it.
|
||||||
|
|
||||||
|
### Issuing an Interrupt
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[MessagePackObject]
|
||||||
|
public record struct ConfirmInterrupt : IInterrupt
|
||||||
|
{
|
||||||
|
[Key(0)] public string Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a system:
|
||||||
|
world.Interrupt(new ConfirmInterrupt { Message = "Are you sure?" });
|
||||||
|
```
|
||||||
|
|
||||||
|
The current tick completes normally. The **next** tick is blocked — `SystemGroup`
|
||||||
|
skips all systems but still drains commands and posts changes.
|
||||||
|
|
||||||
|
### Handling an Interrupt
|
||||||
|
|
||||||
|
Handler commands implement `IInterruptHandlerCommand<T>`. The default `Execute`
|
||||||
|
calls `TryResolve` with the pending interrupt. Return `true` to resolve it,
|
||||||
|
`false` to leave it pending for another handler:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[MessagePackObject]
|
||||||
|
public struct ConfirmHandler : IInterruptHandlerCommand<ConfirmInterrupt>
|
||||||
|
{
|
||||||
|
[Key(0)] public bool Confirmed;
|
||||||
|
|
||||||
|
public bool TryResolve(ConfirmInterrupt interrupt)
|
||||||
|
{
|
||||||
|
// Do work here. The interrupt is just a signal.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// External code (e.g., UI button) enqueues the handler:
|
||||||
|
world.Commands.Enqueue(new ConfirmHandler { Confirmed = true });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Rules
|
||||||
|
|
||||||
|
- Only one interrupt of a given type may be pending at a time. A second call
|
||||||
|
to `Interrupt<T>` with the same type throws.
|
||||||
|
- Interrupts are transient — they are not serialized and do not survive save/load.
|
||||||
|
- If no `SystemGroup` manages the world, `Interrupt<T>()` is a no-op.
|
||||||
|
- Check pending interrupts with `world.HasInterrupt<T>()`.
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,10 @@ public class World : IDisposable
|
||||||
public CommandQueue Commands { get; }
|
public CommandQueue Commands { get; }
|
||||||
public void ExecuteCommands();
|
public void ExecuteCommands();
|
||||||
|
|
||||||
|
// --- Interrupts ---
|
||||||
|
public void Interrupt<T>(T interrupt) where T : struct, IInterrupt;
|
||||||
|
public bool HasInterrupt<T>() where T : struct, IInterrupt;
|
||||||
|
|
||||||
// --- Reactivity ---
|
// --- Reactivity ---
|
||||||
public void MarkModified<T>(Entity entity) where T : struct;
|
public void MarkModified<T>(Entity entity) where T : struct;
|
||||||
public void PostChanges();
|
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
|
## IRelationship
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
|
||||||
|
|
@ -385,3 +385,53 @@ package is included for compile-time validation.
|
||||||
- The `MessagePackAnalyzer` package is a compile-time dependency.
|
- The `MessagePackAnalyzer` package is a compile-time dependency.
|
||||||
- `[Key]` indices should be sequential starting from 0 to avoid null
|
- `[Key]` indices should be sequential starting from 0 to avoid null
|
||||||
placeholders in the binary output.
|
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
|
## Dependency Graph
|
||||||
|
|
||||||
```
|
```
|
||||||
Phase 1 (Core)
|
Phase 1 (Core)
|
||||||
└─→ Phase 2 (Queries & Systems)
|
└─→ Phase 2 (Queries & Systems)
|
||||||
└─→ Phase 3 (Commands)
|
└─→ Phase 3 (Commands)
|
||||||
|
└─→ Phase 9 (Interrupts)
|
||||||
└─→ Phase 4 (Relationships)
|
└─→ Phase 4 (Relationships)
|
||||||
└─→ Phase 5 (Reactivity)
|
└─→ Phase 5 (Reactivity)
|
||||||
└─→ Phase 6 (Singletons)
|
└─→ Phase 6 (Singletons)
|
||||||
|
|
@ -576,7 +634,8 @@ Phase 1 (Core)
|
||||||
└─→ Phase 8 (Polish)
|
└─→ 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue