Skip to main content

vifu_runtime/
runtime.rs

1use std::collections::VecDeque;
2
3use bevy_app::{App, Plugin};
4use bevy_ecs::prelude::*;
5use bevy_ecs::schedule::ScheduleLabel;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Clone, Debug, Hash, PartialEq, Eq, ScheduleLabel)]
10pub struct RuntimeSchedule;
11
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct RuntimeCommand {
15    pub id: String,
16    pub name: String,
17    #[serde(default)]
18    pub payload: Value,
19}
20
21impl RuntimeCommand {
22    pub fn new(id: impl Into<String>, name: impl Into<String>, payload: Value) -> Self {
23        Self {
24            id: id.into(),
25            name: name.into(),
26            payload,
27        }
28    }
29}
30
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct RuntimeEvent {
34    pub sequence: u64,
35    pub name: String,
36    #[serde(default)]
37    pub payload: Value,
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct EffectRequest {
43    pub id: String,
44    pub kind: String,
45    #[serde(default)]
46    pub payload: Value,
47}
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct EffectResult {
52    pub effect_id: String,
53    pub succeeded: bool,
54    #[serde(default)]
55    pub output: Value,
56}
57
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct RuntimeSnapshot {
61    pub revision: u64,
62    #[serde(default)]
63    pub state: Value,
64}
65
66impl Default for RuntimeSnapshot {
67    fn default() -> Self {
68        Self {
69            revision: 0,
70            state: Value::Object(Default::default()),
71        }
72    }
73}
74
75#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct RuntimeAdvance {
78    pub snapshot: RuntimeSnapshot,
79    pub events: Vec<RuntimeEvent>,
80    pub effects: Vec<EffectRequest>,
81}
82
83#[derive(Resource, Default)]
84pub struct RuntimeCommandQueue {
85    commands: VecDeque<RuntimeCommand>,
86}
87
88impl RuntimeCommandQueue {
89    pub fn push(&mut self, command: RuntimeCommand) {
90        self.commands.push_back(command);
91    }
92
93    pub fn pop_front(&mut self) -> Option<RuntimeCommand> {
94        self.commands.pop_front()
95    }
96
97    pub fn drain(&mut self) -> impl Iterator<Item = RuntimeCommand> + '_ {
98        self.commands.drain(..)
99    }
100
101    pub fn is_empty(&self) -> bool {
102        self.commands.is_empty()
103    }
104}
105
106#[derive(Resource, Default)]
107pub struct RuntimeEventQueue {
108    next_sequence: u64,
109    events: Vec<RuntimeEvent>,
110}
111
112impl RuntimeEventQueue {
113    pub fn emit(&mut self, name: impl Into<String>, payload: Value) -> u64 {
114        let sequence = self.next_sequence;
115        self.next_sequence = self.next_sequence.saturating_add(1);
116        self.events.push(RuntimeEvent {
117            sequence,
118            name: name.into(),
119            payload,
120        });
121        sequence
122    }
123
124    pub fn drain(&mut self) -> impl Iterator<Item = RuntimeEvent> + '_ {
125        self.events.drain(..)
126    }
127}
128
129#[derive(Resource, Default)]
130pub struct EffectRequestQueue {
131    next_id: u64,
132    effects: Vec<EffectRequest>,
133}
134
135impl EffectRequestQueue {
136    pub fn request(&mut self, kind: impl Into<String>, payload: Value) -> String {
137        let id = format!("effect-{}", self.next_id);
138        self.next_id = self.next_id.saturating_add(1);
139        self.effects.push(EffectRequest {
140            id: id.clone(),
141            kind: kind.into(),
142            payload,
143        });
144        id
145    }
146
147    pub fn drain(&mut self) -> impl Iterator<Item = EffectRequest> + '_ {
148        self.effects.drain(..)
149    }
150}
151
152#[derive(Resource, Default)]
153pub struct EffectResultQueue {
154    results: VecDeque<EffectResult>,
155}
156
157impl EffectResultQueue {
158    pub fn push(&mut self, result: EffectResult) {
159        self.results.push_back(result);
160    }
161
162    pub fn pop_front(&mut self) -> Option<EffectResult> {
163        self.results.pop_front()
164    }
165
166    pub fn drain(&mut self) -> impl Iterator<Item = EffectResult> + '_ {
167        self.results.drain(..)
168    }
169}
170
171#[derive(Resource, Clone, Debug)]
172pub struct RuntimeState {
173    pub revision: u64,
174    pub value: Value,
175}
176
177impl Default for RuntimeState {
178    fn default() -> Self {
179        Self {
180            revision: 0,
181            value: Value::Object(Default::default()),
182        }
183    }
184}
185
186impl RuntimeState {
187    pub fn snapshot(&self) -> RuntimeSnapshot {
188        RuntimeSnapshot {
189            revision: self.revision,
190            state: self.value.clone(),
191        }
192    }
193}
194
195#[derive(Clone, Copy, Debug, Default)]
196pub struct VifuRuntimePlugin;
197
198impl Plugin for VifuRuntimePlugin {
199    fn build(&self, app: &mut App) {
200        app.init_schedule(RuntimeSchedule)
201            .init_resource::<RuntimeCommandQueue>()
202            .init_resource::<RuntimeEventQueue>()
203            .init_resource::<EffectRequestQueue>()
204            .init_resource::<EffectResultQueue>()
205            .init_resource::<RuntimeState>();
206    }
207}
208
209pub struct HeadlessRuntime {
210    app: App,
211}
212
213impl Default for HeadlessRuntime {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219impl HeadlessRuntime {
220    pub fn new() -> Self {
221        let mut app = App::empty();
222        app.add_plugins(VifuRuntimePlugin);
223        Self { app }
224    }
225
226    pub fn restore(snapshot: RuntimeSnapshot) -> Self {
227        let mut runtime = Self::new();
228        runtime.app.insert_resource(RuntimeState {
229            revision: snapshot.revision,
230            value: snapshot.state,
231        });
232        runtime
233    }
234
235    pub fn app(&self) -> &App {
236        &self.app
237    }
238
239    pub fn app_mut(&mut self) -> &mut App {
240        &mut self.app
241    }
242
243    pub fn snapshot(&self) -> RuntimeSnapshot {
244        self.app.world().resource::<RuntimeState>().snapshot()
245    }
246
247    pub fn complete_effect(&mut self, result: EffectResult) {
248        self.app
249            .world_mut()
250            .resource_mut::<EffectResultQueue>()
251            .push(result);
252    }
253
254    pub fn enqueue_command(&mut self, command: RuntimeCommand) {
255        self.app
256            .world_mut()
257            .resource_mut::<RuntimeCommandQueue>()
258            .push(command);
259    }
260
261    pub fn run_schedule(&mut self, schedule: impl ScheduleLabel) -> RuntimeAdvance {
262        self.app.world_mut().run_schedule(schedule);
263        self.take_advance()
264    }
265
266    pub fn dispatch(&mut self, command: RuntimeCommand) -> RuntimeAdvance {
267        self.enqueue_command(command);
268        self.app.world_mut().run_schedule(RuntimeSchedule);
269
270        {
271            let mut state = self.app.world_mut().resource_mut::<RuntimeState>();
272            state.revision = state.revision.saturating_add(1);
273        }
274
275        self.take_advance()
276    }
277
278    fn take_advance(&mut self) -> RuntimeAdvance {
279        let snapshot = self.snapshot();
280        let events = self
281            .app
282            .world_mut()
283            .resource_mut::<RuntimeEventQueue>()
284            .drain()
285            .collect();
286        let effects = self
287            .app
288            .world_mut()
289            .resource_mut::<EffectRequestQueue>()
290            .drain()
291            .collect();
292
293        RuntimeAdvance {
294            snapshot,
295            events,
296            effects,
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use bevy_ecs::prelude::{ResMut, Resource};
304    use serde_json::json;
305
306    use super::*;
307
308    #[derive(Resource, Default)]
309    struct ProcessedCommands(u64);
310
311    fn echo_commands(
312        mut commands: ResMut<RuntimeCommandQueue>,
313        mut events: ResMut<RuntimeEventQueue>,
314        mut effects: ResMut<EffectRequestQueue>,
315        mut state: ResMut<RuntimeState>,
316        mut processed: ResMut<ProcessedCommands>,
317    ) {
318        for command in commands.drain() {
319            processed.0 += 1;
320            state.value["lastCommand"] = Value::String(command.name.clone());
321            events.emit("command.processed", json!({ "commandId": command.id }));
322            effects.request("agent.invoke", command.payload);
323        }
324    }
325
326    #[test]
327    fn plugins_define_runtime_behavior() {
328        let mut runtime = HeadlessRuntime::new();
329        runtime
330            .app_mut()
331            .init_resource::<ProcessedCommands>()
332            .add_systems(RuntimeSchedule, echo_commands);
333
334        let advance = runtime.dispatch(RuntimeCommand::new(
335            "command-1",
336            "player.message",
337            json!({ "text": "Hello" }),
338        ));
339
340        assert_eq!(advance.snapshot.revision, 1);
341        assert_eq!(advance.snapshot.state["lastCommand"], "player.message");
342        assert_eq!(advance.events[0].name, "command.processed");
343        assert_eq!(advance.effects[0].kind, "agent.invoke");
344    }
345
346    #[test]
347    fn snapshots_can_be_restored() {
348        let runtime = HeadlessRuntime::restore(RuntimeSnapshot {
349            revision: 7,
350            state: json!({ "scene": "platform" }),
351        });
352
353        assert_eq!(runtime.snapshot().revision, 7);
354        assert_eq!(runtime.snapshot().state["scene"], "platform");
355    }
356
357    #[derive(Clone, Debug, Hash, PartialEq, Eq, ScheduleLabel)]
358    struct ExtensionSchedule;
359
360    fn run_extension(
361        mut commands: ResMut<RuntimeCommandQueue>,
362        mut events: ResMut<RuntimeEventQueue>,
363        mut state: ResMut<RuntimeState>,
364    ) {
365        let command = commands.pop_front().expect("extension command");
366        state.revision = 9;
367        state.value = command.payload;
368        events.emit("extension.completed", json!({ "commandId": command.id }));
369    }
370
371    #[test]
372    fn extensions_run_on_the_shared_runtime_host() {
373        let mut runtime = HeadlessRuntime::new();
374        runtime
375            .app_mut()
376            .init_schedule(ExtensionSchedule)
377            .add_systems(ExtensionSchedule, run_extension);
378        runtime.enqueue_command(RuntimeCommand::new(
379            "command-9",
380            "extension.run",
381            json!({ "result": "ok" }),
382        ));
383
384        let advance = runtime.run_schedule(ExtensionSchedule);
385
386        assert_eq!(advance.snapshot.revision, 9);
387        assert_eq!(advance.snapshot.state["result"], "ok");
388        assert_eq!(advance.events[0].name, "extension.completed");
389    }
390}