From 5c94bb34d5efca5770098fe640d951d4fff48c15 Mon Sep 17 00:00:00 2001 From: Jonas H Date: Sat, 28 Mar 2026 13:23:36 +0100 Subject: [PATCH] agent update --- .../filter-cargo-warnings.py | 0 .pi/skills/self-gating-systems/SKILL.md | 10 ++ docs/self-gating-systems.md | 135 ++++++++++++++++++ 3 files changed, 145 insertions(+) rename .pi/{hooks => extensions}/filter-cargo-warnings.py (100%) create mode 100644 .pi/skills/self-gating-systems/SKILL.md create mode 100644 docs/self-gating-systems.md diff --git a/.pi/hooks/filter-cargo-warnings.py b/.pi/extensions/filter-cargo-warnings.py similarity index 100% rename from .pi/hooks/filter-cargo-warnings.py rename to .pi/extensions/filter-cargo-warnings.py diff --git a/.pi/skills/self-gating-systems/SKILL.md b/.pi/skills/self-gating-systems/SKILL.md new file mode 100644 index 0000000..7cbb6fe --- /dev/null +++ b/.pi/skills/self-gating-systems/SKILL.md @@ -0,0 +1,10 @@ +# Intent-Based Architecture Skill + +Read the architecture document at `docs/self-gating-systems.md` (resolve relative to the project root `/home/jonas/projects/snow_trail_sdl/`) and follow its patterns when: + +- Adding cross-system communication (use intents, not direct function calls) +- Adding new systems to the main loop +- Changing how game modes work (editor, dialog, gameplay) +- Working with camera, input, or mode-switching logic + +Key rule: **systems communicate through typed intent data in shared queues** — producers insert intents, consumers process and remove them. No system calls another system's functions directly. diff --git a/docs/self-gating-systems.md b/docs/self-gating-systems.md new file mode 100644 index 0000000..b128f00 --- /dev/null +++ b/docs/self-gating-systems.md @@ -0,0 +1,135 @@ +# Intent-Based Architecture + +Systems communicate through **intents** — one-frame typed data in shared queues — not through direct function calls. This decouples producers from consumers: the system that wants something to happen does not know or care which system processes it. + +## The Pattern + +``` +Producer World (shared state) Consumer +──────── ──────────────────── ──────── +detects dialog change + ↓ +insert(camera, CameraTransitionIntent) + ↓ + camera_transition_intents + camera_intent_system reads it + sets up CameraTransition component + removes intent +``` + +An intent is ephemeral (one frame). The state change it triggers (a component mutation) persists. + +## Intent Types + +Intents are plain structs stored in `Storage` on `World`, keyed by target entity: + +```rust +// components/intent.rs +pub struct FollowPlayerIntent; +pub struct StopFollowingIntent; +pub struct CameraTransitionIntent { pub duration: f32 } +``` + +```rust +// world.rs +pub follow_player_intents: Storage, +pub stop_following_intents: Storage, +pub camera_transition_intents: Storage, +``` + +Adding a new intent = one struct + one storage field. Nothing else changes. + +## Producing Intents + +Any code with access to the storage can submit. The entity is the target: + +```rust +// Event handler wants camera to follow player — doesn't call camera functions +world.follow_player_intents.insert(camera_entity, FollowPlayerIntent); + +// Dialog system detects state change — doesn't know how transitions work +world.camera_transition_intents.insert(camera_entity, CameraTransitionIntent { duration: 0.8 }); +``` + +Producers don't import consumer modules. They only know about the intent type and the storage. + +## Consuming Intents + +A consumer system reads, acts, and removes: + +```rust +pub fn camera_intent_system(world: &mut World) { + // 1. Read + let follow_entities = world.follow_player_intents.all(); + for entity in follow_entities { + // 2. Act — all the follow setup logic lives here + start_camera_following(world, entity); + // 3. Remove (consume) + world.follow_player_intents.remove(entity); + } + // ... same for stop_following_intents, camera_transition_intents +} +``` + +The consumer owns the implementation. `start_camera_following` is a private function inside the camera module — no other module can call it directly. + +## Why This Matters + +**Decoupling.** The dialog system doesn't import camera functions. It inserts a `CameraTransitionIntent`. If the camera system changes how transitions work, the dialog system is unaffected. + +**Multiple producers, same path.** Editor toggle, dialog state changes, and future cutscene systems all produce the same `CameraTransitionIntent`. They all go through the same processing. No special cases. + +**Testability.** To test camera transitions: insert a `CameraTransitionIntent`, call `camera_intent_system`, check the result. No need to simulate dialog state or editor toggles. + +**Additive behavior.** To add a new reaction to `StopFollowingIntent` (e.g., play a sound), write a new system that reads the intent before the camera system consumes it. No existing code changes. + +## Execution Order + +The main loop is a flat pipeline. Order encodes causality: + +``` +Input + intent generation (camera_input, player_input, dialog_transition_detect) + ↓ +Intent processing (camera_intent_system) + ↓ +Camera behavior (noclip, dialog_camera, follow, transition, ground_clamp) + ↓ +Editor overlay (UI only — not a game system) + ↓ +Fixed-step physics (state_machine, physics, triggers, dialog) + ↓ +Per-frame systems (state_machine, rotate, trees, spotlights, snow) + ↓ +Render +``` + +Moving a system changes the data flow. The order is the contract. + +## Systems Also Self-Gate + +Because intents express what should happen, systems naturally have nothing to do when their data is absent: + +- `camera_follow_system` — no `FollowComponent` = no work +- `dialog_camera_system` — no active bubbles = no work +- `player_input_system` — camera not following = no player input +- `camera_noclip_system` — camera has `FollowComponent` = skip + +The main loop doesn't branch on mode flags. Systems check their own data. + +## When to Use Intents vs. Direct Mutation + +| Situation | Approach | +|---|---| +| One system wants another to do something | Intent | +| A system updating its own components | Direct mutation | +| Per-frame continuous computation | Components + tick system | +| Persistent state (is the camera following?) | Component (`FollowComponent`) | +| One-shot request (start following) | Intent (`FollowPlayerIntent`) | + +## Adding New Intents + +1. Define the struct in `components/intent.rs` +2. Add a `Storage` field to `World` (+ `new()` + `despawn()`) +3. Producers insert into the storage +4. A consumer system reads, acts, and removes +5. Done — no other code changes