use bevy::prelude::*;
use bevy::remote::{error_codes, BrpResult, RemoteMethodSystemId, RemoteMethods};
use serde::de::DeserializeOwned;
use serde_json::{json, Value};
use bevy::ecs::system::SystemId;
use crate::plugin::ready::DebugReady;
use crate::plugin::scene::{SceneLog, SceneStep};
#[derive(Debug, Clone)]
pub struct DebugCommandInfo {
pub name: String,
pub description: String,
pub params: String,
pub example: Value,
pub records: bool,
}
impl DebugCommandInfo {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
params: String::new(),
example: json!({}),
records: true,
}
}
pub fn params(mut self, doc: impl Into<String>) -> Self {
self.params = doc.into();
self
}
pub fn example(mut self, example: Value) -> Self {
self.example = example;
self
}
pub fn read_only(mut self) -> Self {
self.records = false;
self
}
}
#[derive(Resource, Default)]
pub struct DebugCommands(Vec<DebugCommandInfo>);
impl DebugCommands {
pub fn iter(&self) -> impl Iterator<Item = &DebugCommandInfo> {
self.0.iter()
}
pub fn contains(&self, name: &str) -> bool {
self.0.iter().any(|c| c.name == name)
}
}
pub trait DebugCommandsAppExt {
fn add_debug_command<P, M>(
&mut self,
info: DebugCommandInfo,
handler: impl IntoSystem<In<P>, Result<Value, String>, M> + 'static,
) -> &mut Self
where
P: DeserializeOwned + Send + Sync + 'static;
}
impl DebugCommandsAppExt for App {
fn add_debug_command<P, M>(
&mut self,
info: DebugCommandInfo,
handler: impl IntoSystem<In<P>, Result<Value, String>, M> + 'static,
) -> &mut Self
where
P: DeserializeOwned + Send + Sync + 'static,
{
register(self, info, handler, false);
self
}
}
pub(crate) fn add_builtin_command<P, M>(
app: &mut App,
info: DebugCommandInfo,
handler: impl IntoSystem<In<P>, Result<Value, String>, M> + 'static,
) where
P: DeserializeOwned + Send + Sync + 'static,
{
register(app, info, handler, true);
}
const RESERVED_PREFIXES: [&str; 5] = ["bevy_fairy/", "world.", "registry.", "rpc.", "schedule."];
fn is_reserved(name: &str) -> bool {
name.is_empty() || RESERVED_PREFIXES.iter().any(|prefix| name.starts_with(prefix))
}
fn register<P, M>(
app: &mut App,
info: DebugCommandInfo,
handler: impl IntoSystem<In<P>, Result<Value, String>, M> + 'static,
builtin: bool,
) where
P: DeserializeOwned + Send + Sync + 'static,
{
let name = info.name.clone();
if !app.world().contains_resource::<RemoteMethods>() {
panic!("add DebuggerPlugin before add_debug_command");
}
if !builtin && is_reserved(&name) {
panic!("debug command `{name}` is reserved or empty; use `<game>/<verb>`");
}
if app.world().resource::<RemoteMethods>().get(&name).is_some() {
panic!("debug command `{name}` registered twice");
}
if let Err(err) = serde_json::from_value::<P>(info.example.clone()) {
panic!("debug command `{name}`: its example does not parse as its params: {err}");
}
let example = info.example.clone();
let records = info.records;
let method = name.clone();
let world = app.world_mut();
let handler_id = world.register_system(handler);
let wrapper = move |In(raw): In<Option<Value>>, world: &mut World| -> BrpResult {
run_command::<P>(world, &method, handler_id, &example, records, raw)
};
let wrapper_id = world.register_system(wrapper);
world
.resource_mut::<RemoteMethods>()
.insert(name, RemoteMethodSystemId::Instant(wrapper_id));
let mut registry = world.get_resource_or_init::<DebugCommands>();
let at = registry.0.partition_point(|c| c.name < info.name);
registry.0.insert(at, info);
}
fn run_command<P>(
world: &mut World,
name: &str,
handler: SystemId<In<P>, Result<Value, String>>,
example: &Value,
records: bool,
raw: Option<Value>,
) -> BrpResult
where
P: DeserializeOwned + Send + Sync + 'static,
{
if !world.get_resource::<DebugReady>().is_some_and(|r| r.0) {
return Err(crate::plugin::brp_error(
error_codes::INTERNAL_ERROR,
"game not ready yet (bevy_fairy/status says ready: false); retry shortly",
));
}
let raw = raw.unwrap_or_else(|| json!({}));
let params: P = serde_json::from_value(raw.clone()).map_err(|e| {
crate::plugin::brp_error(
error_codes::INVALID_PARAMS,
format!("{name}: {e}; example: {example}"),
)
})?;
let outcome = world
.run_system_with(handler, params)
.map_err(|e| crate::plugin::brp_error(error_codes::INTERNAL_ERROR, format!("{name}: {e:?}")))?;
let value = outcome.map_err(|message| crate::plugin::brp_error(error_codes::INTERNAL_ERROR, message))?;
if records {
world.get_resource_or_init::<SceneLog>().0.push(SceneStep {
method: name.to_string(),
params: raw,
});
}
Ok(value)
}
pub(crate) fn handle_commands(In(_): In<Option<Value>>, registry: Res<DebugCommands>) -> BrpResult {
let commands: Vec<Value> = registry
.iter()
.map(|c| {
json!({
"name": c.name,
"description": c.description,
"params": c.params,
"example": c.example,
})
})
.collect();
Ok(json!({ "commands": commands }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reserved_names_are_the_crate_and_bevy_namespaces() {
assert!(is_reserved(""));
assert!(is_reserved("bevy_fairy/time"));
assert!(is_reserved("world.query"));
assert!(is_reserved("registry.schema"));
assert!(is_reserved("rpc.discover"));
assert!(is_reserved("schedule.list"));
assert!(!is_reserved("sanctuary/spawn"));
assert!(!is_reserved("worldly/spawn"));
}
#[test]
fn info_builders_set_their_fields() {
let info = DebugCommandInfo::new("demo/x", "does x")
.params("n: how many")
.example(json!({"n": 2}));
assert_eq!(info.params, "n: how many");
assert_eq!(info.example, json!({"n": 2}));
assert!(info.records);
assert!(!info.read_only().records);
}
}