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}