Modifier authoring guide
A Modifier mutates an Entity once per frame. The Director sorts
registered modifiers by (phase, priority, registration_order) before
the first tick; each later modifier observes the cumulative effect of
all earlier ones.
The trait
pub trait Modifier {
fn meta(&self) -> &'static ModifierMeta;
fn update(&mut self, entity: &mut Entity);
}
meta returns a &'static ModifierMeta constant declaring name,
description, phase, priority, and the reads / writes field
sets. update is the only mutation surface; time flows in via
entity.tick.now (stamped by Director::run).
Adding a new modifier
-
Create
crates/stackchan-core/src/modifiers/<your_name>.rs. -
Implement the state machine:
use crate::director::{Field, ModifierMeta, Phase}; use crate::entity::Entity; use crate::modifier::Modifier; pub struct YourModifier { // state: timers, RNG, last-value } impl YourModifier { #[must_use] pub const fn new() -> Self { /* ... */ } } impl Modifier for YourModifier { fn meta(&self) -> &'static ModifierMeta { static META: ModifierMeta = ModifierMeta { name: "YourModifier", description: "One sentence: what triggers this and what \ entity fields it writes.", phase: Phase::Expression, priority: 0, reads: &[/* Field::... */], writes: &[/* Field::... */], }; &META } fn update(&mut self, entity: &mut Entity) { let now = entity.tick.now; // Read + mutate entity fields. } } -
Pick a phase.
Affectdecides emotions,Expressionmodulates face style,Motionwrites head pose,Audiodrives visual from audio. -
Add unit tests in a
mod testsat the bottom of the file. Setentity.tick.now, callupdate, assert. -
Register in
crates/stackchan-firmware/src/main.rs::render_task:director.add_modifier(&mut your_modifier).expect("registry full"). Update the boot info-line listing. -
If the behavior is non-local, add a
stackchan-simintegration test that drives the new modifier alongside related ones.
Reads vs writes
Entity fields fall into a few buckets:
- Pixel-affecting (
face.*): listed inFace::frame_eq. New pixel-affecting fields must be added toframe_eqso the render task’s dirty-check works correctly. - Sensor inputs (
perception.*): firmware writes; modifiers read. - Pending inputs (
input.tap_pending,input.remote_pending): firmware writes; the consuming modifier reads + clears in the same tick. - Cognitive state (
mind.*): emotion modifiers writemind.affect.emotion+mind.autonomy.manual_until; downstream modifiers read. - Output requests (
voice.chirp_request): modifiers write; firmware reads + clears afterDirector::run.
The reads / writes slices on ModifierMeta are documentation, and
cfg(debug_assertions) builds assert that each modifier only writes its
declared Fields after every update — see Director::run.
Field granularity
Field is fine-grained per-leaf-field (e.g. LeftEyePhase vs
LeftEyeWeight) so different sub-fields of the same component don’t
false-flag as conflicts. Field::group() buckets fine variants into
coarse FieldGroups for human-readable conflict reports.
Ordering
The Affect phase ordering matters:
EmotionFromTouchruns first (priority-100) so a tap takes effect before any environmental override.EmotionCycleruns last in Affect; the autonomous advancer only fires when no input modifier setmind.autonomy.manual_until.StyleFromEmotionruns inExpressionand readsmind.affect.emotion; Blink / Breath / IdleDrift then add per-frame deltas on top.IdleHeadDrift(Motion) contributes occasional brief head glances tomotor.head_pose;HeadFromEmotion(registered later in Motion) adds an emotion-keyed bias on top.MouthFromAudioruns inAudioso audio-driven mouth-open isn’t overwritten by a stale earlier write.
When in doubt, mirror the registration order in render_task and add
a sim integration test that asserts the visible behavior.
Skills
A Skill has should_fire(&Entity) -> bool and
invoke(&mut Entity) -> SkillStatus, plus richer metadata than a
modifier. Use it when the behavior is discoverable — selected from a
menu rather than always-on. Skills don’t write face or motor
directly; they go through mind / voice / events and modifiers in
later phases translate that into rendered face and physical motion.
The current population lives at crates/stackchan-core/src/skills/.