use bevy::prelude::*;
use bevy::remote::{BrpResult, RemoteMethodSystemId, RemoteMethods};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::plugin::commands::DebugCommands;
use crate::plugin::ready::DebugReady;
pub(crate) const FORMAT: &str = "bevy_fairy_scene";
pub(crate) const VERSION: u32 = 1;
pub(crate) const SCENE_ENV: &str = "BEVY_FAIRY_SCENE";
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct SceneStep {
pub method: String,
pub params: Value,
}
#[derive(Resource, Default)]
pub struct SceneLog(pub Vec<SceneStep>);
#[derive(Resource)]
pub struct ScenePlayback {
pub path: String,
pub steps: Vec<SceneStep>,
pub next: usize,
pub failed: usize,
}
#[derive(Deserialize)]
struct SceneFile {
#[allow(dead_code)]
format: String,
#[allow(dead_code)]
version: u32,
steps: Vec<SceneStep>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SceneParams {
action: String,
}
pub(crate) fn scene_json(steps: &[SceneStep]) -> Value {
json!({ "format": FORMAT, "version": VERSION, "steps": steps })
}
pub(crate) fn load(path: &str) -> Result<Vec<SceneStep>, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("cannot read: {e}"))?;
let value: Value = serde_json::from_str(&text).map_err(|e| format!("not JSON: {e}"))?;
let format = value.get("format").and_then(Value::as_str).unwrap_or("");
if format != FORMAT {
return Err(format!("format is \"{format}\", expected \"{FORMAT}\""));
}
let version = value.get("version").and_then(Value::as_u64);
if version != Some(u64::from(VERSION)) {
return Err(format!("version is {version:?}, expected {VERSION}"));
}
let file: SceneFile =
serde_json::from_value(value).map_err(|e| format!("bad steps: {e}"))?;
Ok(file.steps)
}
pub(crate) fn start_playback(app: &mut App, path: &str) {
match load(path) {
Ok(steps) => {
if steps.is_empty() {
info!("bevy_fairy: scene {path} replayed (0 steps, 0 failed)");
}
app.world_mut().insert_resource(ScenePlayback {
path: path.to_string(),
steps,
next: 0,
failed: 0,
});
}
Err(why) => error!("bevy_fairy: scene {path}: {why}"),
}
}
pub(crate) fn handle_scene(In(params): In<Option<Value>>, mut log: ResMut<SceneLog>) -> BrpResult {
let params: SceneParams = serde_json::from_value(params.unwrap_or_else(|| json!({}))).map_err(|e| {
crate::plugin::brp_error(
bevy::remote::error_codes::INVALID_PARAMS,
format!("bevy_fairy/scene: {e}; example: {}", json!({"action": "get"})),
)
})?;
match params.action.as_str() {
"get" => Ok(scene_json(&log.0)),
"clear" => {
let cleared = log.0.len();
log.0.clear();
Ok(json!({ "cleared": cleared }))
}
other => Err(crate::plugin::brp_error(
bevy::remote::error_codes::INVALID_PARAMS,
format!("action must be get or clear, not \"{other}\""),
)),
}
}
pub(crate) fn replay_next_step(world: &mut World) {
if !world.get_resource::<DebugReady>().is_some_and(|r| r.0) {
return;
}
let Some(playback) = world.get_resource::<ScenePlayback>() else {
return;
};
let Some(step) = playback.steps.get(playback.next).cloned() else {
return;
};
let outcome = run_step(world, &step);
let Some(mut playback) = world.get_resource_mut::<ScenePlayback>() else {
return;
};
playback.next += 1;
if let Err(why) = &outcome {
playback.failed += 1;
warn!("bevy_fairy: scene step {} ({}) failed: {why}", playback.next, step.method);
}
if playback.next == playback.steps.len() {
info!(
"bevy_fairy: scene {} replayed ({} steps, {} failed)",
playback.path,
playback.steps.len(),
playback.failed
);
}
}
fn run_step(world: &mut World, step: &SceneStep) -> Result<(), String> {
let recorded = world
.get_resource::<DebugCommands>()
.is_some_and(|commands| commands.iter().any(|c| c.name == step.method && c.records));
if !recorded {
return Err(format!("{} is not a recorded debug command here", step.method));
}
let id = match world
.get_resource::<RemoteMethods>()
.and_then(|methods| methods.get(&step.method))
{
Some(RemoteMethodSystemId::Instant(id)) => *id,
_ => return Err(format!("{} has no instant handler", step.method)),
};
match world.run_system_with(id, Some(step.params.clone())) {
Ok(Ok(_)) => Ok(()),
Ok(Err(brp)) => Err(brp.message),
Err(e) => Err(format!("{e:?}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scene_json_has_the_format_and_version() {
let steps = vec![SceneStep { method: "demo/x".into(), params: json!({"n": 1}) }];
let file = scene_json(&steps);
assert_eq!(file["format"], FORMAT);
assert_eq!(file["version"], VERSION);
assert_eq!(file["steps"][0]["method"], "demo/x");
}
}