Skip to main content

vifu_runtime/
runtime.rs

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