use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use bevy_remote::{BrpError, BrpRequest, BrpResponse};
pub use bevy_remote::BrpResult;
pub use bevy_remote::BrpPayload;
pub use bevy_remote::builtin_methods;
fn feature_registering(method: &str) -> Option<&'static str> {
match method {
"bevy_fairy/screenshot" | "bevy_fairy/capture" => Some("plugin-screenshot"),
"bevy_fairy/guide" | "bevy_fairy/guide+watch" => Some("plugin-guide"),
"bevy_fairy/ui_text" => Some("plugin-ui-text"),
"bevy_fairy/input" | "bevy_fairy/status" | "bevy_fairy/commands" | "bevy_fairy/scene"
| "bevy_fairy/quit" | "bevy_fairy/time" => Some("plugin"),
_ => None,
}
}
pub fn refusal_text(method: &str, error: &BrpError) -> String {
match (error.code, feature_registering(method)) {
(bevy_remote::error_codes::METHOD_NOT_FOUND, Some(feature)) => format!(
"{} — the game does not register `{method}`: add DebuggerPlugin, from bevy_fairy \
built with the `{feature}` feature",
error.message
),
_ => error.message.clone(),
}
}
pub fn entity_data_from_query_result(value: &serde_json::Value) -> Option<Vec<EntityData>> {
let rows = value.as_array()?;
let mut out = Vec::with_capacity(rows.len());
for row in rows {
let id = row.get("entity")?.as_u64()?;
let mut components: HashMap<ComponentTypeId, ComponentValue> = HashMap::new();
if let Some(map) = row.get("components").and_then(|c| c.as_object()) {
for (k, v) in map {
components.insert(k.clone(), v.clone());
}
}
for (k, v) in row.get("has").and_then(|c| c.as_object()).into_iter().flatten() {
components.insert(k.clone(), v.clone());
}
out.push(EntityData { id, components });
}
Some(out)
}
pub fn entity_data_from_get_result(
entity: EntityId,
value: &serde_json::Value,
) -> Option<EntityData> {
let map = value
.get("components")
.and_then(|c| c.as_object())
.or_else(|| value.as_object())?;
let components = map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
Some(EntityData { id: entity, components })
}
pub fn component_type_info_from_list_result(value: &serde_json::Value) -> Vec<ComponentTypeInfo> {
value
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|name| ComponentTypeInfo {
id: name.to_string(),
name: name.to_string(),
schema: None,
})
.collect()
})
.unwrap_or_default()
}
pub fn query_params(filter: Option<&QueryFilter>, components_all: bool) -> serde_json::Value {
let with: Vec<&str> = filter
.and_then(|f| f.with.as_deref())
.map(|v| v.iter().map(String::as_str).collect())
.unwrap_or_default();
let without: Vec<&str> = filter
.and_then(|f| f.without.as_deref())
.map(|v| v.iter().map(String::as_str).collect())
.unwrap_or_default();
let option = if components_all {
serde_json::Value::from("all")
} else {
serde_json::Value::Array(vec![])
};
serde_json::json!({
"data": {
"components": [],
"option": option,
"has": []
},
"filter": {
"with": with,
"without": without
},
"strict": false
})
}
pub type EntityId = u64;
pub type ComponentTypeId = String;
pub type ComponentValue = serde_json::Value;
#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DebugOverlayType {
EntityHighlight,
Colliders,
ColliderVisualization,
Transforms,
TransformGizmos,
SystemFlow,
PerformanceMetrics,
DebugMarkers,
Custom(String),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct QueryFilter {
pub with: Option<Vec<ComponentTypeId>>,
pub without: Option<Vec<ComponentTypeId>>,
pub where_clause: Option<Vec<ComponentFilter>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentFilter {
pub component: ComponentTypeId,
pub field: Option<String>,
pub op: FilterOp,
pub value: ComponentValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FilterOp {
#[serde(rename = "eq")]
Equal,
#[serde(rename = "ne")]
NotEqual,
#[serde(rename = "gt")]
GreaterThan,
#[serde(rename = "gte")]
GreaterThanOrEqual,
#[serde(rename = "lt")]
LessThan,
#[serde(rename = "lte")]
LessThanOrEqual,
#[serde(rename = "contains")]
Contains,
#[serde(rename = "regex")]
Regex,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatedQuery {
pub id: String,
pub filter: QueryFilter,
pub estimated_cost: QueryCost,
pub hints: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryCost {
pub estimated_entities: usize,
pub estimated_time_us: u64,
pub estimated_memory: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "params")]
#[non_exhaustive]
pub enum DebugCommand {
InspectEntity {
entity_id: EntityId,
include_metadata: Option<bool>,
include_relationships: Option<bool>,
},
InspectBatch {
entity_ids: Vec<EntityId>,
include_metadata: Option<bool>,
include_relationships: Option<bool>,
limit: Option<usize>,
},
ProfileSystem {
system_name: String,
duration_ms: Option<u64>,
track_allocations: Option<bool>,
},
SetVisualDebug {
overlay_type: DebugOverlayType,
enabled: bool,
config: Option<serde_json::Value>,
},
ExecuteQuery {
query: ValidatedQuery,
offset: Option<usize>,
limit: Option<usize>,
},
ValidateQuery {
params: serde_json::Value,
},
EstimateCost {
params: serde_json::Value,
},
GetQuerySuggestions {
params: serde_json::Value,
},
BuildAndExecuteQuery {
params: serde_json::Value,
},
ProfileMemory {
capture_backtraces: Option<bool>,
target_systems: Option<Vec<String>>,
duration_seconds: Option<u64>,
},
StopMemoryProfiling {
session_id: Option<String>,
},
GetMemoryProfile,
DetectMemoryLeaks {
target_systems: Option<Vec<String>>,
},
AnalyzeMemoryTrends {
target_systems: Option<Vec<String>>,
},
TakeMemorySnapshot,
GetMemoryStatistics,
SessionControl {
operation: SessionOperation,
session_id: Option<String>,
},
GetStatus,
GetHierarchy {
root_entity: Option<EntityId>,
max_depth: Option<usize>,
},
GetSystemInfo {
system_name: Option<String>,
include_scheduling: Option<bool>,
},
StartIssueDetection,
StopIssueDetection,
GetDetectedIssues {
limit: Option<usize>,
},
AcknowledgeIssue {
alert_id: String,
},
ReportFalsePositive {
alert_id: String,
},
GetIssueDetectionStats,
UpdateDetectionRule {
name: String,
enabled: Option<bool>,
sensitivity: Option<f32>,
},
ClearIssueHistory,
StartBudgetMonitoring,
StopBudgetMonitoring,
SetPerformanceBudget {
config: serde_json::Value,
},
GetPerformanceBudget,
CheckBudgetViolations,
GetBudgetViolationHistory {
limit: Option<usize>,
},
GenerateComplianceReport {
duration_seconds: Option<u64>,
},
GetBudgetRecommendations,
ClearBudgetHistory,
GetBudgetStatistics,
Screenshot {
path: Option<String>,
warmup_duration: Option<u64>,
capture_delay: Option<u64>,
wait_for_render: Option<bool>,
description: Option<String>,
},
Custom {
name: String,
params: serde_json::Value,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SessionOperation {
Create,
Resume,
Checkpoint,
Restore { checkpoint_id: String },
End,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SessionState {
Active,
Paused,
Replaying,
Ended,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointInfo {
pub id: String,
pub timestamp: u64,
pub description: Option<String>,
pub size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
#[non_exhaustive]
pub enum DebugResponse {
EntityInspection {
entity: EntityData,
metadata: Option<EntityMetadata>,
relationships: Option<EntityRelationships>,
},
BatchEntityInspection {
entities: Vec<EntityInspectionResult>,
requested_count: usize,
found_count: usize,
missing_entities: Vec<EntityId>,
inspection_time_us: u64,
},
SystemProfile(SystemProfile),
ProfilingStarted {
system_name: String,
duration_ms: Option<u64>,
},
ProfileHistory {
system_name: String,
samples: Vec<ProfileSample>,
frame_count: usize,
},
PerformanceAnomalies {
count: usize,
anomalies: Vec<serde_json::Value>,
},
VisualDebugStatus {
overlay_type: DebugOverlayType,
enabled: bool,
config: Option<serde_json::Value>,
},
QueryResult {
entities: Vec<EntityData>,
total_count: usize,
execution_time_us: u64,
has_more: bool,
},
MemoryProfile {
total_allocated: usize,
allocations_per_system: HashMap<String, usize>,
top_allocations: Vec<AllocationInfo>,
},
SessionStatus {
session_id: String,
state: SessionState,
command_count: usize,
checkpoints: Vec<CheckpointInfo>,
},
Status {
version: String,
active_sessions: usize,
command_queue_size: usize,
performance_overhead_percent: f32,
},
QueryValidation {
valid: bool,
query: Option<ValidatedQuery>,
errors: Vec<String>,
suggestions: Vec<String>,
},
QueryCost {
cost: QueryCost,
performance_budget_exceeded: bool,
suggestions: Vec<String>,
},
QuerySuggestions {
suggestions: Vec<String>,
query_complexity: u32,
},
QueryExecution {
success: bool,
result: Option<serde_json::Value>,
execution_time_us: u64,
entities_processed: Option<usize>,
},
Success {
message: String,
data: Option<serde_json::Value>,
},
Custom(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityData {
pub id: EntityId,
pub components: HashMap<ComponentTypeId, ComponentValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentTypeInfo {
pub id: ComponentTypeId,
pub name: String,
pub schema: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityMetadata {
pub component_count: usize,
pub memory_size: usize,
pub last_modified: Option<u64>,
pub generation: u32,
pub index: u32,
pub component_types: Vec<DetailedComponentTypeInfo>,
pub modified_components: Vec<String>,
pub archetype_id: Option<u32>,
pub location_info: Option<EntityLocationInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetailedComponentTypeInfo {
pub type_id: String,
pub type_name: String,
pub size_bytes: usize,
pub is_reflected: bool,
pub schema: Option<serde_json::Value>,
pub is_modified: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityLocationInfo {
pub archetype_id: u32,
pub index: u32,
pub table_id: Option<u32>,
pub table_row: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityRelationships {
pub parent: Option<EntityId>,
pub children: Vec<EntityId>,
pub related: HashMap<String, Vec<EntityId>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityInspectionResult {
pub entity_id: EntityId,
pub found: bool,
pub entity: Option<EntityData>,
pub metadata: Option<EntityMetadata>,
pub relationships: Option<EntityRelationships>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemMetrics {
pub total_time_us: u64,
pub min_time_us: u64,
pub max_time_us: u64,
pub avg_time_us: u64,
pub median_time_us: u64,
pub p95_time_us: u64,
pub p99_time_us: u64,
pub total_allocations: usize,
pub allocation_rate: f32,
pub overhead_percent: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileSample {
pub timestamp: u64,
pub duration_us: u64,
pub allocations: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemProfile {
pub system_name: String,
pub metrics: SystemMetrics,
pub samples: Vec<ProfileSample>,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllocationInfo {
pub size: usize,
pub location: String,
pub backtrace: Option<Vec<String>>,
pub count: usize,
}
pub fn format_brp_error(error: &BrpError) -> String {
format!("BRP error {}: {}", error.code, error.message)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_query_filter_serialization() {
let filter = QueryFilter {
with: Some(vec!["Transform".to_string(), "Velocity".to_string()]),
without: None,
where_clause: None,
};
let json = serde_json::to_string(&filter).unwrap();
let deserialized: QueryFilter = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.with.as_ref().unwrap().len(), 2);
}
#[test]
fn test_debug_command_serialization() {
let command = DebugCommand::InspectEntity {
entity_id: 123,
include_metadata: Some(true),
include_relationships: None,
};
let json = serde_json::to_string(&command).unwrap();
let _deserialized: DebugCommand = serde_json::from_str(&json).unwrap();
}
#[test]
fn test_wire_request_round_trip() {
let request = BrpRequest {
method: "world.query".to_string(),
id: Some(json!(7)),
params: Some(json!({
"data": { "components": ["bevy_transform::components::transform::Transform"] },
"filter": { "with": [], "without": [] }
})),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains(r#""jsonrpc":"2.0""#));
assert!(json.contains(r#""method":"world.query""#));
let deserialized: BrpRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.method, "world.query");
assert_eq!(deserialized.id, Some(json!(7)));
}
}