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`.
|
||||
All component types used with `World` generic methods are automatically
|
||||
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 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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue