Skip to main content

nightshade_api/
command.rs

1//! A serializable command form of the API.
2//!
3//! Every call here also exists as a free function. The command layer mirrors the
4//! API as a single [`Command`] enum so the same work can be driven three ways:
5//! call the free function directly, [`submit_command`] one [`Command`], or
6//! [`submit_commands`] a whole batch. The enum derives serde, so it doubles as
7//! the wire format a binding (FFI, network, scripting) targets: a binding only
8//! builds `Command` values and reads [`CommandReply`] back.
9//!
10//! Entities cross the boundary as the engine [`Entity`] itself, which is
11//! serializable and stable, so a reply hands one back and a binding stores it.
12//! Within a [`submit_commands`] batch a later command can also reference an
13//! entity an earlier command produced through [`Ref::Result`], with no round
14//! trip, which is what makes a single batch able to build and wire up a scene.
15//!
16//! The free functions stay the real implementations. The whole enum and its
17//! dispatch are generated from one registry by the [`commands`] macro, so the
18//! command surface cannot drift from the functions, and reference resolution and
19//! reply wrapping are uniform across every command rather than retyped per arm.
20
21use crate::scene::{Body, Object, Shape};
22use nightshade::prelude::{Entity, KeyCode, MouseButton, TextAlignment, Vec3, World, vec3};
23use serde::{Deserialize, Serialize};
24
25/// How a command names an entity: an existing [`Entity`] a reply handed back, or
26/// the entity produced by an earlier command in the same [`submit_commands`]
27/// batch. `Entity` is itself serializable and stable, so a binding stores it and
28/// hands it back verbatim, no separate handle type needed.
29#[derive(Serialize, Deserialize, Clone, Copy, Debug, enum2schema::Schema)]
30pub enum Ref {
31    Entity(#[schema(with = entity_schema)] Entity),
32    Result(u32),
33    /// A live entity named by its id alone, resolved against the world at
34    /// dispatch. Lets a caller reference an existing entity without tracking
35    /// its generation, which is what a picked editor selection or a script's
36    /// entity handle has.
37    Existing(u32),
38}
39
40/// What a [`Command`] returns. Setters reply [`CommandReply::None`], spawns reply
41/// the [`Entity`] they made, queries reply their value, and a failed reference
42/// resolution replies [`CommandReply::Error`].
43#[derive(Serialize, Deserialize, Clone, Debug, enum2schema::Schema)]
44pub enum CommandReply {
45    None,
46    Entity(#[schema(with = entity_schema)] Entity),
47    Bool(bool),
48    Float(f32),
49    Int(i64),
50    Text(String),
51    Vector([f32; 3]),
52    Entities(#[schema(with = entities_schema)] Vec<Entity>),
53    Strings(Vec<String>),
54    Bytes(Vec<u8>),
55    /// A structured value (a struct, array, or object) serialized as json, for
56    /// the queries whose result does not fit a scalar reply: an entity
57    /// description, the scene tree, a material, bounds, and so on. A binding
58    /// deserializes it into its own shape.
59    Json(#[schema(with = any_schema)] enum2schema::serde_json::Value),
60    Error(String),
61}
62
63/// One field of a [`Command`] as data: its name, its Rust type as written, and
64/// the dispatch role that says how the value is bound. Emitted by the same
65/// registry that defines the commands, so a binding generator reads the surface
66/// from a compiled artifact instead of parsing source.
67#[derive(Serialize, Clone, Debug)]
68pub struct FieldSpec {
69    pub name: &'static str,
70    pub type_name: &'static str,
71    pub role: &'static str,
72}
73
74/// One [`Command`] as data: its variant name, fields, and reply kind.
75#[derive(Serialize, Clone, Debug)]
76pub struct CommandSpec {
77    pub name: &'static str,
78    pub fields: Vec<FieldSpec>,
79    pub reply: &'static str,
80}
81
82/// [`command_manifest`] as a json string, the input a binding code generator
83/// reads alongside [`command_schema`].
84pub fn command_manifest_json() -> String {
85    enum2schema::serde_json::to_string(&command_manifest()).unwrap_or_default()
86}
87
88fn entity_schema() -> enum2schema::serde_json::Value {
89    enum2schema::serde_json::json!({
90        "type": "object",
91        "properties": {
92            "id": { "type": "integer" },
93            "generation": { "type": "integer" }
94        },
95        "required": ["id", "generation"]
96    })
97}
98
99fn entities_schema() -> enum2schema::serde_json::Value {
100    enum2schema::serde_json::json!({ "type": "array", "items": entity_schema() })
101}
102
103fn any_schema() -> enum2schema::serde_json::Value {
104    enum2schema::serde_json::json!({})
105}
106
107/// The json schema for [`Command`], the wire form a binding builds. Derived from
108/// the command enum by the same registry that defines it, so it always matches
109/// the surface. Pair with [`command_reply_schema`] for the output shape.
110pub fn command_schema() -> enum2schema::serde_json::Value {
111    <Command as enum2schema::Schema>::schema()
112}
113
114/// The json schema for [`CommandReply`], what a binding reads back.
115pub fn command_reply_schema() -> enum2schema::serde_json::Value {
116    <CommandReply as enum2schema::Schema>::schema()
117}
118
119/// Runs one command and returns its reply.
120pub fn submit_command(world: &mut World, command: &Command) -> CommandReply {
121    dispatch(world, command, &[])
122}
123
124/// Runs a batch in order and returns one reply per command. A command may name
125/// an entity an earlier command in the same batch produced with [`Ref::Result`],
126/// so a batch can spawn entities and then configure and parent them in one call.
127pub fn submit_commands(world: &mut World, commands: &[Command]) -> Vec<CommandReply> {
128    let mut produced: Vec<Option<Entity>> = Vec::with_capacity(commands.len());
129    let mut replies = Vec::with_capacity(commands.len());
130    for command in commands {
131        let reply = dispatch(world, command, &produced);
132        produced.push(match &reply {
133            CommandReply::Entity(entity) => Some(*entity),
134            _ => None,
135        });
136        replies.push(reply);
137    }
138    replies
139}
140
141fn resolve(world: &World, reference: Ref, produced: &[Option<Entity>]) -> Option<Entity> {
142    match reference {
143        Ref::Entity(entity) => Some(entity),
144        Ref::Result(index) => produced.get(index as usize).copied().flatten(),
145        Ref::Existing(id) => world
146            .core
147            .entity_locations
148            .get(id)
149            .filter(|location| location.allocated)
150            .map(|location| Entity {
151                id,
152                generation: location.generation,
153            }),
154    }
155}
156
157fn array_to_vec3(values: [f32; 3]) -> Vec3 {
158    vec3(values[0], values[1], values[2])
159}
160
161fn array_to_vec2(values: [f32; 2]) -> nightshade::prelude::Vec2 {
162    nightshade::prelude::vec2(values[0], values[1])
163}
164
165/// Adapts the flat command fields to [`spawn_object`](crate::scene::spawn_object),
166/// which takes an [`Object`] struct. The dispatch macro calls functions with
167/// positional arguments, so the one command that builds a struct goes through
168/// this rather than special casing the macro.
169fn spawn_object_command(
170    world: &mut World,
171    shape: Shape,
172    position: Vec3,
173    scale: Vec3,
174    color: [f32; 4],
175    body: Body,
176) -> Entity {
177    crate::scene::spawn_object(
178        world,
179        Object {
180            shape,
181            position,
182            scale,
183            color,
184            body,
185        },
186    )
187}
188
189/// Wire form of an [`InstanceTransform`](nightshade::prelude::InstanceTransform)
190/// for the instancing commands: a position, a rotation quaternion as
191/// `[x, y, z, w]`, and a scale.
192#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, enum2schema::Schema)]
193pub struct InstanceWire {
194    pub position: [f32; 3],
195    pub rotation: [f32; 4],
196    pub scale: [f32; 3],
197}
198
199fn instance_from_wire(wire: &InstanceWire) -> nightshade::prelude::InstanceTransform {
200    use nightshade::prelude::nalgebra_glm::Quat;
201    nightshade::prelude::InstanceTransform::new(
202        array_to_vec3(wire.position),
203        Quat::new(
204            wire.rotation[3],
205            wire.rotation[0],
206            wire.rotation[1],
207            wire.rotation[2],
208        ),
209        array_to_vec3(wire.scale),
210    )
211}
212
213fn register_material_command(
214    world: &mut World,
215    name: &str,
216    base_color: [f32; 4],
217    metallic: f32,
218    roughness: f32,
219    emissive: [f32; 3],
220) -> String {
221    crate::materials::register_material(
222        world,
223        name,
224        nightshade::ecs::material::components::Material {
225            base_color,
226            metallic,
227            roughness,
228            emissive_factor: emissive,
229            ..Default::default()
230        },
231    )
232}
233
234fn spawn_objects_command(
235    world: &mut World,
236    shape: Shape,
237    scale: Vec3,
238    color: [f32; 4],
239    body: Body,
240    positions: &[Vec3],
241) -> Vec<Entity> {
242    crate::scene::spawn_objects(
243        world,
244        Object {
245            shape,
246            position: vec3(0.0, 0.0, 0.0),
247            scale,
248            color,
249            body,
250        },
251        positions,
252    )
253}
254
255#[cfg(feature = "navmesh")]
256#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, enum2schema::Schema)]
257pub struct RecastConfigWire {
258    pub agent_radius: f32,
259    pub agent_height: f32,
260    pub cell_size_fraction: f32,
261    pub cell_height_fraction: f32,
262    pub walkable_climb: f32,
263    pub walkable_slope_angle: f32,
264    pub min_region_size: u32,
265    pub merge_region_size: u32,
266    pub max_simplification_error: f32,
267    pub edge_max_len_factor: u32,
268    pub max_vertices_per_polygon: u32,
269    pub detail_sample_dist: f32,
270    pub detail_sample_max_error: f32,
271}
272
273#[cfg(feature = "navmesh")]
274fn bake_navmesh_with_command(world: &mut World, config: RecastConfigWire) {
275    crate::navigation::bake_navmesh_with(
276        world,
277        &nightshade::prelude::RecastNavMeshConfig {
278            agent_radius: config.agent_radius,
279            agent_height: config.agent_height,
280            cell_size_fraction: config.cell_size_fraction,
281            cell_height_fraction: config.cell_height_fraction,
282            walkable_climb: config.walkable_climb,
283            walkable_slope_angle: config.walkable_slope_angle,
284            min_region_size: config.min_region_size as u16,
285            merge_region_size: config.merge_region_size as u16,
286            max_simplification_error: config.max_simplification_error,
287            edge_max_len_factor: config.edge_max_len_factor as u16,
288            max_vertices_per_polygon: config.max_vertices_per_polygon as u16,
289            detail_sample_dist: config.detail_sample_dist,
290            detail_sample_max_error: config.detail_sample_max_error,
291        },
292    );
293}
294
295/// Wire form of a curated [`ParticleEmitter`](nightshade::prelude::ParticleEmitter):
296/// the common knobs, with a single base color expanded into an explosion
297/// gradient. The Rust `spawn_particle_emitter` takes the full struct for finer
298/// control.
299#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, enum2schema::Schema)]
300pub struct EmitterWire {
301    pub position: [f32; 3],
302    pub color: [f32; 3],
303    pub spawn_rate: f32,
304    pub burst_count: u32,
305    pub lifetime: [f32; 2],
306    pub size: [f32; 2],
307    pub gravity: [f32; 3],
308    pub drag: f32,
309    pub one_shot: bool,
310}
311
312fn emitter_from_wire(wire: &EmitterWire) -> nightshade::prelude::ParticleEmitter {
313    let mut emitter = nightshade::prelude::ParticleEmitter::firework_explosion(
314        array_to_vec3(wire.position),
315        array_to_vec3(wire.color),
316        wire.burst_count,
317    );
318    emitter.spawn_rate = wire.spawn_rate;
319    emitter.particle_lifetime_min = wire.lifetime[0];
320    emitter.particle_lifetime_max = wire.lifetime[1];
321    emitter.size_start = wire.size[0];
322    emitter.size_end = wire.size[1];
323    emitter.gravity = array_to_vec3(wire.gravity);
324    emitter.drag = wire.drag;
325    emitter.one_shot = wire.one_shot;
326    emitter.enabled = true;
327    emitter
328}
329
330fn spawn_particle_emitter_command(world: &mut World, emitter: EmitterWire) -> Entity {
331    crate::effects::spawn_particle_emitter(world, emitter_from_wire(&emitter))
332}
333
334fn set_emitter_command(world: &mut World, emitter_entity: Entity, emitter: EmitterWire) {
335    crate::effects::set_emitter(world, emitter_entity, emitter_from_wire(&emitter));
336}
337
338fn update_material_command(
339    world: &mut World,
340    name: &str,
341    base_color: [f32; 4],
342    metallic: f32,
343    roughness: f32,
344    emissive: [f32; 3],
345) {
346    crate::materials::update_material(
347        world,
348        name,
349        nightshade::ecs::material::components::Material {
350            base_color,
351            metallic,
352            roughness,
353            emissive_factor: emissive,
354            ..Default::default()
355        },
356    );
357}
358
359fn set_material_variant_command(world: &mut World, variant: &str) -> usize {
360    let variant = if variant.is_empty() {
361        None
362    } else {
363        Some(variant)
364    };
365    crate::materials::set_material_variant(world, variant)
366}
367
368fn set_fog_command(world: &mut World, enabled: bool, color: [f32; 3], start: f32, end: f32) {
369    let fog = if enabled {
370        Some(nightshade::ecs::graphics::resources::Fog { color, start, end })
371    } else {
372        None
373    };
374    crate::environment::set_fog(world, fog);
375}
376
377fn set_depth_of_field_command(
378    world: &mut World,
379    enabled: bool,
380    focus_distance: f32,
381    focus_range: f32,
382    max_blur_radius: f32,
383    bokeh_threshold: f32,
384) {
385    crate::environment::set_depth_of_field(
386        world,
387        nightshade::ecs::graphics::resources::DepthOfField {
388            enabled,
389            focus_distance,
390            focus_range,
391            max_blur_radius,
392            bokeh_threshold,
393            ..Default::default()
394        },
395    );
396}
397
398fn panel_data_grid_command(
399    world: &mut World,
400    panel: Entity,
401    headers: &[&str],
402    widths: &[f32],
403    pool_size: usize,
404) -> Entity {
405    let columns: Vec<(&str, f32)> = headers
406        .iter()
407        .zip(widths.iter())
408        .map(|(header, width)| (*header, *width))
409        .collect();
410    crate::ui::panel_data_grid(world, panel, &columns, pool_size)
411}
412
413fn panel_selectable_command(
414    world: &mut World,
415    panel: Entity,
416    text: &str,
417    group: u32,
418    grouped: bool,
419) -> Entity {
420    crate::ui::panel_selectable(world, panel, text, grouped.then_some(group))
421}
422
423fn panel_splitter_command(
424    world: &mut World,
425    panel: Entity,
426    horizontal: bool,
427    ratio: f32,
428) -> Entity {
429    let direction = if horizontal {
430        nightshade::prelude::SplitDirection::Horizontal
431    } else {
432        nightshade::prelude::SplitDirection::Vertical
433    };
434    crate::ui::panel_splitter(world, panel, direction, ratio)
435}
436
437fn screenshot_command(world: &mut World, path: &str) {
438    crate::environment::screenshot(world, std::path::PathBuf::from(path));
439}
440
441fn easing_from_name(name: &str) -> nightshade::prelude::EasingFunction {
442    use nightshade::prelude::EasingFunction::*;
443    match name.to_ascii_lowercase().as_str() {
444        "quadin" => QuadIn,
445        "quadout" => QuadOut,
446        "quadinout" => QuadInOut,
447        "cubicin" => CubicIn,
448        "cubicout" => CubicOut,
449        "cubicinout" => CubicInOut,
450        "quartin" => QuartIn,
451        "quartout" => QuartOut,
452        "quartinout" => QuartInOut,
453        "quintin" => QuintIn,
454        "quintout" => QuintOut,
455        "quintinout" => QuintInOut,
456        "sinein" => SineIn,
457        "sineout" => SineOut,
458        "sineinout" => SineInOut,
459        "expoin" => ExpoIn,
460        "expoout" => ExpoOut,
461        "expoinout" => ExpoInOut,
462        _ => Linear,
463    }
464}
465
466fn animate_position_command(
467    world: &mut World,
468    entity: Entity,
469    to: Vec3,
470    seconds: f32,
471    easing: &str,
472) {
473    crate::animate::animate_position(world, entity, to, seconds, easing_from_name(easing));
474}
475
476fn animate_scale_command(world: &mut World, entity: Entity, to: Vec3, seconds: f32, easing: &str) {
477    crate::animate::animate_scale(world, entity, to, seconds, easing_from_name(easing));
478}
479
480fn animate_color_command(
481    world: &mut World,
482    entity: Entity,
483    to: [f32; 4],
484    seconds: f32,
485    easing: &str,
486) {
487    crate::animate::animate_color(world, entity, to, seconds, easing_from_name(easing));
488}
489
490fn set_shading_mode_command(world: &mut World, mode: &str) {
491    use nightshade::prelude::ShadingMode;
492    let mode = match mode.to_ascii_lowercase().as_str() {
493        "wireframe" => ShadingMode::Wireframe,
494        "flat" => ShadingMode::Flat,
495        "rendered" => ShadingMode::Rendered,
496        _ => ShadingMode::Solid,
497    };
498    crate::camera::set_shading_mode(world, mode);
499}
500
501#[cfg(feature = "physics")]
502#[derive(Serialize)]
503struct RaycastResultWire {
504    entity_id: u32,
505    distance: f32,
506    point: [f32; 3],
507    normal: [f32; 3],
508}
509
510#[cfg(feature = "physics")]
511fn raycast_command(
512    world: &mut World,
513    origin: Vec3,
514    direction: Vec3,
515    max_distance: f32,
516) -> Option<RaycastResultWire> {
517    crate::physics::raycast(world, origin, direction, max_distance).map(|hit| RaycastResultWire {
518        entity_id: hit.entity.id,
519        distance: hit.distance,
520        point: [hit.point.x, hit.point.y, hit.point.z],
521        normal: [hit.normal.x, hit.normal.y, hit.normal.z],
522    })
523}
524
525#[cfg(feature = "physics")]
526fn attach_fixed_command(world: &mut World, parent: Entity, child: Entity) -> bool {
527    crate::physics::attach_fixed(world, parent, child).is_some()
528}
529
530#[cfg(feature = "physics")]
531fn attach_hinge_command(world: &mut World, parent: Entity, child: Entity, axis: &str) -> bool {
532    use nightshade::ecs::physics::joints::JointAxisDirection;
533    let axis = match axis.to_ascii_lowercase().as_str() {
534        "y" => JointAxisDirection::Y,
535        "z" => JointAxisDirection::Z,
536        _ => JointAxisDirection::X,
537    };
538    crate::physics::attach_hinge(world, parent, child, axis).is_some()
539}
540
541#[cfg(feature = "physics")]
542fn attach_spring_command(
543    world: &mut World,
544    parent: Entity,
545    child: Entity,
546    rest_length: f32,
547    stiffness: f32,
548    damping: f32,
549) -> bool {
550    crate::physics::attach_spring(world, parent, child, rest_length, stiffness, damping).is_some()
551}
552
553#[cfg(feature = "physics")]
554fn attach_rope_command(
555    world: &mut World,
556    parent: Entity,
557    child: Entity,
558    max_distance: f32,
559) -> bool {
560    crate::physics::attach_rope(world, parent, child, max_distance).is_some()
561}
562
563#[cfg(feature = "picking")]
564#[derive(Serialize)]
565struct SurfacePickWire {
566    world_position: [f32; 3],
567    world_normal: [f32; 3],
568    depth: f32,
569    entity_id: Option<u32>,
570}
571
572#[cfg(feature = "picking")]
573fn take_surface_pick_command(world: &mut World) -> Option<SurfacePickWire> {
574    crate::picking::take_surface_pick(world).map(|result| SurfacePickWire {
575        world_position: [
576            result.world_position.x,
577            result.world_position.y,
578            result.world_position.z,
579        ],
580        world_normal: [
581            result.world_normal.x,
582            result.world_normal.y,
583            result.world_normal.z,
584        ],
585        depth: result.depth,
586        entity_id: result.entity_id,
587    })
588}
589
590fn save_scene_command(world: &mut World, name: &str) -> Vec<u8> {
591    crate::serialize::save_scene(world, name).unwrap_or_default()
592}
593
594fn load_scene_command(world: &mut World, bytes: &[u8]) -> Vec<Entity> {
595    crate::serialize::load_scene(world, bytes).unwrap_or_default()
596}
597
598/// Maps a plain key name to a [`KeyCode`] for the input-query commands, so the
599/// wire form names keys as strings like `"a"`, `"space"`, or `"left"` rather
600/// than carrying the engine's key enum.
601fn key_from_name(name: &str) -> Option<KeyCode> {
602    let lower = name.to_ascii_lowercase();
603    Some(match lower.as_str() {
604        "a" => KeyCode::KeyA,
605        "b" => KeyCode::KeyB,
606        "c" => KeyCode::KeyC,
607        "d" => KeyCode::KeyD,
608        "e" => KeyCode::KeyE,
609        "f" => KeyCode::KeyF,
610        "g" => KeyCode::KeyG,
611        "h" => KeyCode::KeyH,
612        "i" => KeyCode::KeyI,
613        "j" => KeyCode::KeyJ,
614        "k" => KeyCode::KeyK,
615        "l" => KeyCode::KeyL,
616        "m" => KeyCode::KeyM,
617        "n" => KeyCode::KeyN,
618        "o" => KeyCode::KeyO,
619        "p" => KeyCode::KeyP,
620        "q" => KeyCode::KeyQ,
621        "r" => KeyCode::KeyR,
622        "s" => KeyCode::KeyS,
623        "t" => KeyCode::KeyT,
624        "u" => KeyCode::KeyU,
625        "v" => KeyCode::KeyV,
626        "w" => KeyCode::KeyW,
627        "x" => KeyCode::KeyX,
628        "y" => KeyCode::KeyY,
629        "z" => KeyCode::KeyZ,
630        "0" => KeyCode::Digit0,
631        "1" => KeyCode::Digit1,
632        "2" => KeyCode::Digit2,
633        "3" => KeyCode::Digit3,
634        "4" => KeyCode::Digit4,
635        "5" => KeyCode::Digit5,
636        "6" => KeyCode::Digit6,
637        "7" => KeyCode::Digit7,
638        "8" => KeyCode::Digit8,
639        "9" => KeyCode::Digit9,
640        "space" => KeyCode::Space,
641        "enter" | "return" => KeyCode::Enter,
642        "escape" | "esc" => KeyCode::Escape,
643        "tab" => KeyCode::Tab,
644        "backspace" => KeyCode::Backspace,
645        "delete" => KeyCode::Delete,
646        "left" => KeyCode::ArrowLeft,
647        "right" => KeyCode::ArrowRight,
648        "up" => KeyCode::ArrowUp,
649        "down" => KeyCode::ArrowDown,
650        "shift" | "lshift" => KeyCode::ShiftLeft,
651        "rshift" => KeyCode::ShiftRight,
652        "ctrl" | "control" | "lctrl" => KeyCode::ControlLeft,
653        "rctrl" => KeyCode::ControlRight,
654        "alt" | "lalt" => KeyCode::AltLeft,
655        "ralt" => KeyCode::AltRight,
656        _ => return None,
657    })
658}
659
660/// Maps a button index to a [`MouseButton`]: 0 left, 1 middle, 2 right.
661fn mouse_button_from_index(index: u8) -> MouseButton {
662    match index {
663        1 => MouseButton::Middle,
664        2 => MouseButton::Right,
665        _ => MouseButton::Left,
666    }
667}
668
669fn key_down_command(world: &World, key: &str) -> bool {
670    key_from_name(key)
671        .map(|key| crate::input::key_down(world, key))
672        .unwrap_or(false)
673}
674
675fn key_pressed_command(world: &World, key: &str) -> bool {
676    key_from_name(key)
677        .map(|key| crate::input::key_pressed(world, key))
678        .unwrap_or(false)
679}
680
681fn mouse_down_command(world: &World, button: u8) -> bool {
682    crate::input::mouse_down(world, mouse_button_from_index(button))
683}
684
685fn mouse_clicked_command(world: &World, button: u8) -> bool {
686    crate::input::mouse_clicked(world, mouse_button_from_index(button))
687}
688
689macro_rules! bind_argument {
690    ($field:ident, entity, $produced:ident, $world:ident) => {
691        let $field = match resolve($world, *$field, $produced) {
692            Some(entity) => entity,
693            None => {
694                return CommandReply::Error(
695                    concat!(stringify!($field), ": unresolved entity reference").to_string(),
696                );
697            }
698        };
699    };
700    ($field:ident, opt_entity, $produced:ident, $world:ident) => {
701        let $field = match $field {
702            Some(reference) => match resolve($world, *reference, $produced) {
703                Some(entity) => Some(entity),
704                None => {
705                    return CommandReply::Error(
706                        concat!(stringify!($field), ": unresolved entity reference").to_string(),
707                    );
708                }
709            },
710            None => None,
711        };
712    };
713    ($field:ident, vec3, $produced:ident, $world:ident) => {
714        let $field = array_to_vec3(*$field);
715    };
716    ($field:ident, vec2, $produced:ident, $world:ident) => {
717        let $field = array_to_vec2(*$field);
718    };
719    ($field:ident, copy, $produced:ident, $world:ident) => {
720        let $field = *$field;
721    };
722    ($field:ident, owned, $produced:ident, $world:ident) => {
723        let $field = $field.clone();
724    };
725    ($field:ident, text, $produced:ident, $world:ident) => {
726        let $field = $field.as_str();
727    };
728    ($field:ident, bytes, $produced:ident, $world:ident) => {
729        let $field = $field.as_slice();
730    };
731    ($field:ident, strs, $produced:ident, $world:ident) => {
732        let $field: Vec<&str> = $field.iter().map(|value| value.as_str()).collect();
733        let $field = $field.as_slice();
734    };
735    ($field:ident, vec3_list, $produced:ident, $world:ident) => {
736        let $field: Vec<Vec3> = $field.iter().map(|value| array_to_vec3(*value)).collect();
737        let $field = $field.as_slice();
738    };
739    ($field:ident, floats, $produced:ident, $world:ident) => {
740        let $field = $field.as_slice();
741    };
742    ($field:ident, indices, $produced:ident, $world:ident) => {
743        let $field: Vec<usize> = $field.iter().map(|value| *value as usize).collect();
744        let $field = $field.as_slice();
745    };
746    ($field:ident, opt_vec3, $produced:ident, $world:ident) => {
747        let $field = (*$field).map(array_to_vec3);
748    };
749    ($field:ident, transforms, $produced:ident, $world:ident) => {
750        let $field: Vec<nightshade::prelude::InstanceTransform> =
751            $field.iter().map(instance_from_wire).collect();
752    };
753    ($field:ident, refs, $produced:ident, $world:ident) => {
754        let mut resolved = Vec::with_capacity($field.len());
755        for reference in $field.iter() {
756            match resolve($world, *reference, $produced) {
757                Some(entity) => resolved.push(entity),
758                None => {
759                    return CommandReply::Error(
760                        concat!(stringify!($field), ": unresolved entity reference").to_string(),
761                    );
762                }
763            }
764        }
765        let $field = resolved.as_slice();
766    };
767}
768
769macro_rules! wrap_reply {
770    (none, $call:expr) => {{
771        $call;
772        CommandReply::None
773    }};
774    (entity, $call:expr) => {
775        CommandReply::Entity($call)
776    };
777    (opt_entity, $call:expr) => {
778        match $call {
779            Some(entity) => CommandReply::Entity(entity),
780            None => CommandReply::None,
781        }
782    };
783    (bool, $call:expr) => {
784        CommandReply::Bool($call)
785    };
786    (float, $call:expr) => {
787        CommandReply::Float($call)
788    };
789    (vector, $call:expr) => {{
790        let value = $call;
791        CommandReply::Vector([value.x, value.y, value.z])
792    }};
793    (opt_vector, $call:expr) => {
794        match $call {
795            Some(value) => CommandReply::Vector([value.x, value.y, value.z]),
796            None => CommandReply::None,
797        }
798    };
799    (entities, $call:expr) => {
800        CommandReply::Entities($call)
801    };
802    (strings, $call:expr) => {
803        CommandReply::Strings($call)
804    };
805    (int, $call:expr) => {
806        CommandReply::Int($call as i64)
807    };
808    (text, $call:expr) => {
809        CommandReply::Text($call)
810    };
811    (bytes, $call:expr) => {
812        CommandReply::Bytes($call)
813    };
814    (json, $call:expr) => {
815        CommandReply::Json(
816            enum2schema::serde_json::to_value($call)
817                .unwrap_or(enum2schema::serde_json::Value::Null),
818        )
819    };
820}
821
822macro_rules! commands {
823    (
824        $(
825            $(#[$meta:meta])*
826            $variant:ident { $( $field:ident : $field_type:ty [$role:ident] ),* $(,)? }
827                => $func:path , $reply:ident ;
828        )*
829    ) => {
830        /// One API call as data. Field names and types match the free function
831        /// it mirrors. Positions, axes, and colors are plain arrays so the wire
832        /// form is clean json rather than a math library's internal layout.
833        #[derive(Serialize, Deserialize, Clone, Debug, enum2schema::Schema)]
834        pub enum Command {
835            $(
836                $(#[$meta])*
837                $variant { $( $field : $field_type ),* },
838            )*
839        }
840
841        impl Command {
842            /// This command's variant name, the key the manifest and a binding use,
843            /// read straight off the value with no serialization.
844            pub fn name(&self) -> &'static str {
845                match self {
846                    $(
847                        $(#[$meta])*
848                        Command::$variant { .. } => stringify!($variant),
849                    )*
850                }
851            }
852        }
853
854        fn dispatch(
855            world: &mut World,
856            command: &Command,
857            produced: &[Option<Entity>],
858        ) -> CommandReply {
859            match command {
860                $(
861                    $(#[$meta])*
862                    Command::$variant { $( $field ),* } => {
863                        $( bind_argument!($field, $role, produced, world); )*
864                        wrap_reply!($reply, $func(world $(, $field)*))
865                    }
866                )*
867            }
868        }
869
870        /// The command surface as data, generated from the same registry as
871        /// [`Command`] and its dispatch. cfg-gated commands appear only when
872        /// their feature is compiled, so the manifest matches the surface a
873        /// binding can actually reach.
874        pub fn command_manifest() -> Vec<CommandSpec> {
875            let mut specs = Vec::new();
876            $(
877                $(#[$meta])*
878                specs.extend([CommandSpec {
879                    name: stringify!($variant),
880                    fields: vec![
881                        $( FieldSpec {
882                            name: stringify!($field),
883                            type_name: stringify!($field_type),
884                            role: stringify!($role),
885                        } ),*
886                    ],
887                    reply: stringify!($reply),
888                }]);
889            )*
890            specs
891        }
892    };
893}
894
895commands! {
896    SpawnCube { position: [f32; 3] [vec3] } => crate::scene::spawn_cube, entity;
897    SpawnSphere { position: [f32; 3] [vec3] } => crate::scene::spawn_sphere, entity;
898    SpawnCylinder { position: [f32; 3] [vec3] } => crate::scene::spawn_cylinder, entity;
899    SpawnCone { position: [f32; 3] [vec3] } => crate::scene::spawn_cone, entity;
900    SpawnPlane { position: [f32; 3] [vec3] } => crate::scene::spawn_plane, entity;
901    SpawnTorus { position: [f32; 3] [vec3] } => crate::scene::spawn_torus, entity;
902    SpawnFloor { half_extent: f32 [copy] } => crate::scene::spawn_floor, entity;
903    SpawnGroup { position: [f32; 3] [vec3] } => crate::scene::spawn_group, entity;
904    SpawnModel { glb: Vec<u8> [bytes], position: [f32; 3] [vec3] } => crate::scene::spawn_model, entity;
905    SpawnObject { shape: Shape [copy], position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy], body: Body [copy] } => spawn_object_command, entity;
906
907    SetColor { entity: Ref [entity], color: [f32; 4] [copy] } => crate::appearance::set_color, none;
908    SetMetallicRoughness { entity: Ref [entity], metallic: f32 [copy], roughness: f32 [copy] } => crate::appearance::set_metallic_roughness, none;
909    SetEmissive { entity: Ref [entity], color: [f32; 3] [copy], strength: f32 [copy] } => crate::appearance::set_emissive, none;
910    SetUnlit { entity: Ref [entity], unlit: bool [copy] } => crate::appearance::set_unlit, none;
911    SetTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_texture, none;
912    SetTextureTiling { entity: Ref [entity], repeats: f32 [copy] } => crate::appearance::set_texture_tiling, none;
913    SetNormalTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_normal_texture, none;
914    SetMetallicRoughnessTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_metallic_roughness_texture, none;
915    SetEmissiveTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_emissive_texture, none;
916    SetOcclusionTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_occlusion_texture, none;
917
918    SetPosition { entity: Ref [entity], position: [f32; 3] [vec3] } => crate::placement::set_position, none;
919    SetScale { entity: Ref [entity], scale: [f32; 3] [vec3] } => crate::placement::set_scale, none;
920    SetRotation { entity: Ref [entity], axis: [f32; 3] [vec3], radians: f32 [copy] } => crate::placement::set_rotation, none;
921    Rotate { entity: Ref [entity], axis: [f32; 3] [vec3], radians: f32 [copy] } => crate::placement::rotate, none;
922    Position { entity: Ref [entity] } => crate::placement::position, vector;
923
924    SetParent { child: Ref [entity], parent: Option<Ref> [opt_entity] } => crate::scene::set_parent, none;
925    SetVisible { entity: Ref [entity], visible: bool [copy] } => crate::scene::set_visible, none;
926    Despawn { entity: Ref [entity] } => crate::scene::despawn, none;
927
928    Tag { entity: Ref [entity], label: String [text] } => crate::groups::tag, none;
929    Untag { entity: Ref [entity], label: String [text] } => crate::groups::untag, none;
930    HasTag { entity: Ref [entity], label: String [text] } => crate::groups::has_tag, bool;
931    QueryTagged { label: String [text] } => crate::groups::tagged, entities;
932
933    PointLight { position: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::point_light, entity;
934    SpotLight { position: [f32; 3] [vec3], target: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::spot_light, entity;
935    SetSun { color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::set_sun, none;
936
937    SetBackground { background: crate::environment::Background [owned] } => crate::environment::set_background, none;
938    ShowGrid { enabled: bool [copy] } => crate::environment::show_grid, none;
939    SetAmbient { color: [f32; 4] [copy] } => crate::environment::set_ambient, none;
940    SetBloom { enabled: bool [copy] } => crate::environment::set_bloom, none;
941    SetBloomIntensity { intensity: f32 [copy] } => crate::environment::set_bloom_intensity, none;
942    SetSsao { enabled: bool [copy] } => crate::environment::set_ssao, none;
943    SetSsr { enabled: bool [copy] } => crate::environment::set_ssr, none;
944    SetSsgi { enabled: bool [copy] } => crate::environment::set_ssgi, none;
945    SetFxaa { enabled: bool [copy] } => crate::environment::set_fxaa, none;
946    SetExposure { exposure: f32 [copy] } => crate::environment::set_exposure, none;
947    SetColorGrading { saturation: f32 [copy], contrast: f32 [copy], brightness: f32 [copy] } => crate::environment::set_color_grading, none;
948    SetTimeOfDay { hour: f32 [copy] } => crate::environment::set_time_of_day, none;
949    SetTitle { title: String [text] } => crate::environment::set_title, none;
950
951    EmitFire { position: [f32; 3] [vec3] } => crate::effects::emit_fire, entity;
952    EmitSmoke { position: [f32; 3] [vec3] } => crate::effects::emit_smoke, entity;
953    EmitBurst { position: [f32; 3] [vec3], color: [f32; 4] [copy], count: u32 [copy] } => crate::effects::emit_burst, entity;
954
955    DrawCube { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cube, none;
956    DrawSphere { position: [f32; 3] [vec3], radius: f32 [copy], color: [f32; 4] [copy] } => crate::draw::draw_sphere, none;
957    DrawCylinder { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cylinder, none;
958    DrawCone { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cone, none;
959    DrawTorus { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_torus, none;
960    DrawLine { start: [f32; 3] [vec3], end: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_line, none;
961    DrawText3d { text: String [text], position: [f32; 3] [vec3] } => crate::draw::draw_text_3d, none;
962
963    SpawnLabel { text: String [text], position: [f32; 3] [vec3] } => crate::text::spawn_label, entity;
964    SpawnText { text: String [text], anchor: crate::text::ScreenAnchor [copy] } => crate::text::spawn_text, entity;
965    SetText { entity: Ref [entity], text: String [text] } => crate::text::set_text, none;
966    SetTextColor { entity: Ref [entity], color: [f32; 4] [copy] } => crate::text::set_text_color, none;
967    SetTextSize { entity: Ref [entity], size: f32 [copy] } => crate::text::set_text_size, none;
968
969    SpawnPanel { anchor: crate::text::ScreenAnchor [copy], width: f32 [copy], height: f32 [copy] } => crate::ui::spawn_panel, entity;
970    PanelLabel { panel: Ref [entity], text: String [text] } => crate::ui::panel_label, entity;
971    PanelButton { panel: Ref [entity], text: String [text] } => crate::ui::panel_button, entity;
972    ButtonClicked { button: Ref [entity] } => crate::ui::button_clicked, bool;
973    ButtonHovered { button: Ref [entity] } => crate::ui::button_hovered, bool;
974    DespawnPanel { panel: Ref [entity] } => crate::ui::despawn_panel, none;
975    PanelRow { panel: Ref [entity], height: f32 [copy] } => crate::ui::panel_row, entity;
976    PanelGrid { panel: Ref [entity], columns: usize [copy], row_height: f32 [copy], height: f32 [copy] } => crate::ui::panel_grid, entity;
977    PanelScroll { panel: Ref [entity], height: f32 [copy] } => crate::ui::panel_scroll, entity;
978    SetScrollOffset { scroll_area: Ref [entity], offset: f32 [copy] } => crate::ui::set_scroll_offset, none;
979    SetFocusOrder { entity: Ref [entity], order: i32 [copy] } => crate::ui::set_focus_order, none;
980    FocusWidget { entity: Ref [entity] } => crate::ui::focus_widget, none;
981    SpawnPanelAt { anchor: crate::text::ScreenAnchor [copy], offset: [f32; 2] [vec2], size: [f32; 2] [vec2], color: [f32; 4] [copy] } => crate::ui::spawn_panel_at, entity;
982    PanelText { parent: Ref [entity], text: String [text], rect: [f32; 4] [copy], font_size: f32 [copy], color: [f32; 4] [copy], align: TextAlignment [copy] } => crate::ui::panel_text, entity;
983    PanelBox { parent: Ref [entity], offset: [f32; 2] [vec2], size: [f32; 2] [vec2], color: [f32; 4] [copy] } => crate::ui::panel_box, entity;
984    PanelButtonAt { parent: Ref [entity], label: String [text], offset: [f32; 2] [vec2], size: [f32; 2] [vec2] } => crate::ui::panel_button_at, entity;
985    SetPanelRect { node: Ref [entity], offset: [f32; 2] [vec2], size: [f32; 2] [vec2] } => crate::ui::set_panel_rect, none;
986    SetPanelColor { node: Ref [entity], color: [f32; 4] [copy] } => crate::ui::set_panel_color, none;
987    SetPanelText { label: Ref [entity], text: String [text] } => crate::ui::set_panel_text, none;
988    SetPanelTextColor { label: Ref [entity], color: [f32; 4] [copy] } => crate::ui::set_panel_text_color, none;
989    SetPanelSelected { button: Ref [entity], selected: bool [copy], accent: [f32; 4] [copy] } => crate::ui::set_panel_selected, none;
990    SetPanelVisible { node: Ref [entity], visible: bool [copy] } => crate::ui::set_panel_visible, none;
991
992    PlayAnimation { entity: Ref [entity], clip: usize [copy] } => crate::scene::play_animation, none;
993    PlayAnimationNamed { entity: Ref [entity], name: String [text] } => crate::scene::play_animation_named, bool;
994    SetAnimationLooping { entity: Ref [entity], looping: bool [copy] } => crate::scene::set_animation_looping, none;
995    SetAnimationSpeed { entity: Ref [entity], speed: f32 [copy] } => crate::scene::set_animation_speed, none;
996    BlendToAnimation { entity: Ref [entity], clip: usize [copy], seconds: f32 [copy] } => crate::scene::blend_to_animation, none;
997    PauseAnimation { entity: Ref [entity] } => crate::scene::pause_animation, none;
998    ResumeAnimation { entity: Ref [entity] } => crate::scene::resume_animation, none;
999    StopAnimation { entity: Ref [entity] } => crate::scene::stop_animation, none;
1000    AnimationClips { entity: Ref [entity] } => crate::scene::animation_clips, strings;
1001    AddAnimationEvent { entity: Ref [entity], clip_index: usize [copy], time: f32 [copy], name: String [text] } => crate::scene::add_animation_event, bool;
1002    AddAnimationEventNamed { entity: Ref [entity], clip_name: String [text], time: f32 [copy], name: String [text] } => crate::scene::add_animation_event_named, bool;
1003    SetAnimationLayerWeight { entity: Ref [entity], layer_index: usize [copy], weight: f32 [copy] } => crate::scene::set_animation_layer_weight, none;
1004    ClearAnimationLayers { entity: Ref [entity] } => crate::scene::clear_animation_layers, none;
1005    AimAt { bone: Ref [entity], target: [f32; 3] [vec3], forward: [f32; 3] [vec3] } => crate::animate::aim_at, none;
1006
1007    OrbitCamera { focus: [f32; 3] [vec3], radius: f32 [copy] } => crate::camera::orbit_camera, entity;
1008    FlyCamera { position: [f32; 3] [vec3] } => crate::camera::fly_camera, entity;
1009    FixedCamera { eye: [f32; 3] [vec3], target: [f32; 3] [vec3] } => crate::camera::fixed_camera, entity;
1010    LookAt { eye: [f32; 3] [vec3], target: [f32; 3] [vec3] } => crate::camera::look_at, none;
1011    SetOrbitFocus { focus: [f32; 3] [vec3] } => crate::camera::set_orbit_focus, none;
1012    SetOrbitView { focus: [f32; 3] [vec3], radius: f32 [copy], yaw: f32 [copy], pitch: f32 [copy] } => crate::camera::set_orbit_view, none;
1013    SetOrbitZoom { enabled: bool [copy] } => crate::camera::set_orbit_zoom, none;
1014    SetFieldOfView { degrees: f32 [copy] } => crate::camera::set_field_of_view, none;
1015    SetOrthographic { half_height: f32 [copy] } => crate::camera::set_orthographic, none;
1016    SetPerspective { degrees: f32 [copy] } => crate::camera::set_perspective, none;
1017    CameraPosition {} => crate::camera::camera_position, vector;
1018    CameraForward {} => crate::camera::camera_forward, vector;
1019    #[cfg(feature = "physics")]
1020    FirstPerson { position: [f32; 3] [vec3] } => crate::camera::first_person, entity;
1021
1022    DeltaTime {} => crate::input::delta_time, float;
1023    ElapsedSeconds {} => crate::input::elapsed_seconds, float;
1024    KeyDown { key: String [text] } => key_down_command, bool;
1025    KeyPressed { key: String [text] } => key_pressed_command, bool;
1026    MouseDown { button: u8 [copy] } => mouse_down_command, bool;
1027    MouseClicked { button: u8 [copy] } => mouse_clicked_command, bool;
1028    Wasd {} => crate::input::wasd, vector;
1029    PointerOverUi {} => crate::input::pointer_over_ui, bool;
1030    MouseScroll {} => crate::input::mouse_scroll, float;
1031
1032    #[cfg(feature = "physics")]
1033    Push { entity: Ref [entity], impulse: [f32; 3] [vec3] } => crate::physics::push, none;
1034    #[cfg(feature = "physics")]
1035    SetVelocity { entity: Ref [entity], velocity: [f32; 3] [vec3] } => crate::physics::set_velocity, none;
1036    #[cfg(feature = "physics")]
1037    ApplyForce { entity: Ref [entity], force: [f32; 3] [vec3] } => crate::physics::apply_force, none;
1038    #[cfg(feature = "physics")]
1039    ApplyTorque { entity: Ref [entity], torque: [f32; 3] [vec3] } => crate::physics::apply_torque, none;
1040    #[cfg(feature = "physics")]
1041    SetAngularVelocity { entity: Ref [entity], velocity: [f32; 3] [vec3] } => crate::physics::set_angular_velocity, none;
1042    #[cfg(feature = "physics")]
1043    Velocity { entity: Ref [entity] } => crate::physics::velocity, opt_vector;
1044    #[cfg(feature = "physics")]
1045    AngularVelocity { entity: Ref [entity] } => crate::physics::angular_velocity, opt_vector;
1046    #[cfg(feature = "physics")]
1047    MakeSensor { entity: Ref [entity] } => crate::physics::make_sensor, none;
1048    #[cfg(feature = "physics")]
1049    OverlapSphere { center: [f32; 3] [vec3], radius: f32 [copy] } => crate::physics::overlap_sphere, entities;
1050    #[cfg(feature = "physics")]
1051    SetCollisionGroups { entity: Ref [entity], membership: u32 [copy], filter: u32 [copy] } => crate::physics::set_collision_groups, none;
1052    #[cfg(feature = "physics")]
1053    SetFriction { entity: Ref [entity], friction: f32 [copy] } => crate::physics::set_friction, none;
1054    #[cfg(feature = "physics")]
1055    SetRestitution { entity: Ref [entity], restitution: f32 [copy] } => crate::physics::set_restitution, none;
1056    #[cfg(feature = "physics")]
1057    SetLinearDamping { entity: Ref [entity], damping: f32 [copy] } => crate::physics::set_linear_damping, none;
1058    #[cfg(feature = "physics")]
1059    SetAngularDamping { entity: Ref [entity], damping: f32 [copy] } => crate::physics::set_angular_damping, none;
1060    #[cfg(feature = "physics")]
1061    SetMass { entity: Ref [entity], mass: f32 [copy] } => crate::physics::set_mass, none;
1062    #[cfg(feature = "physics")]
1063    SetGravityScale { entity: Ref [entity], scale: f32 [copy] } => crate::physics::set_gravity_scale, none;
1064
1065    #[cfg(feature = "navmesh")]
1066    BakeNavmesh {} => crate::navigation::bake_navmesh, none;
1067    #[cfg(feature = "navmesh")]
1068    SpawnWalker { position: [f32; 3] [vec3] } => crate::navigation::spawn_walker, entity;
1069    #[cfg(feature = "navmesh")]
1070    WalkTo { agent: Ref [entity], destination: [f32; 3] [vec3] } => crate::navigation::walk_to, none;
1071    #[cfg(feature = "navmesh")]
1072    SetWalkSpeed { agent: Ref [entity], speed: f32 [copy] } => crate::navigation::set_walk_speed, none;
1073    #[cfg(feature = "navmesh")]
1074    StopWalking { agent: Ref [entity] } => crate::navigation::stop_walking, none;
1075
1076    #[cfg(feature = "picking")]
1077    ClickedEntity {} => crate::picking::clicked_entity, opt_entity;
1078    #[cfg(feature = "picking")]
1079    EntityUnderCursor {} => crate::picking::entity_under_cursor, opt_entity;
1080    #[cfg(feature = "picking")]
1081    CursorOnGround {} => crate::picking::cursor_on_ground, opt_vector;
1082    #[cfg(feature = "picking")]
1083    SpawnWorldPanel { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy], color: [f32; 4] [copy] } => crate::world_ui::spawn_world_panel, entity;
1084    #[cfg(feature = "picking")]
1085    WorldPanelButton { panel: Ref [entity], x: f32 [copy], y: f32 [copy], width: f32 [copy], height: f32 [copy], color: [f32; 4] [copy] } => crate::world_ui::world_panel_button, entity;
1086    #[cfg(feature = "picking")]
1087    WorldPanelLabel { panel: Ref [entity], text: String [text], x: f32 [copy], y: f32 [copy] } => crate::world_ui::world_panel_label, entity;
1088    #[cfg(feature = "picking")]
1089    WorldButtonClicked { button: Ref [entity] } => crate::world_ui::world_button_clicked, bool;
1090
1091    #[cfg(feature = "audio")]
1092    PauseSound { entity: Ref [entity] } => crate::audio::pause_sound, none;
1093    #[cfg(feature = "audio")]
1094    ResumeSound { entity: Ref [entity] } => crate::audio::resume_sound, none;
1095    #[cfg(feature = "audio")]
1096    FadeVolume { entity: Ref [entity], volume: f32 [copy], seconds: f32 [copy] } => crate::audio::fade_volume, none;
1097    #[cfg(feature = "audio")]
1098    Crossfade { fade_out: Ref [entity], fade_in: Ref [entity], volume: f32 [copy], seconds: f32 [copy] } => crate::audio::crossfade, none;
1099    #[cfg(feature = "audio")]
1100    SetBusVolume { bus: nightshade::prelude::AudioBus [copy], decibels: f32 [copy], fade_seconds: f32 [copy] } => crate::audio::set_bus_volume, none;
1101    #[cfg(feature = "audio")]
1102    DuckVoice { amount: f32 [copy], fade_seconds: f32 [copy] } => crate::audio::duck_voice, none;
1103
1104    DirectionalLight { direction: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::directional_light, entity;
1105    AreaLight { position: [f32; 3] [vec3], target: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::area_light, entity;
1106    SetLightShadows { light: Ref [entity], enabled: bool [copy] } => crate::lighting::set_light_shadows, none;
1107
1108    EmitSparks { position: [f32; 3] [vec3] } => crate::effects::emit_sparks, entity;
1109    EmitFirework { position: [f32; 3] [vec3], velocity: [f32; 3] [vec3] } => crate::effects::emit_firework, entity;
1110    EmitParticles { position: [f32; 3] [vec3], rate: f32 [copy], lifetime: f32 [copy], size: f32 [copy], gravity: [f32; 3] [vec3] } => crate::effects::emit_particles, entity;
1111
1112    SetAlphaBlend { entity: Ref [entity], enabled: bool [copy] } => crate::appearance::set_alpha_blend, none;
1113    SetAlphaCutoff { entity: Ref [entity], cutoff: f32 [copy] } => crate::appearance::set_alpha_cutoff, none;
1114    SetDoubleSided { entity: Ref [entity], double_sided: bool [copy] } => crate::appearance::set_double_sided, none;
1115    SetIor { entity: Ref [entity], ior: f32 [copy] } => crate::appearance::set_ior, none;
1116    SetTransmission { entity: Ref [entity], factor: f32 [copy] } => crate::appearance::set_transmission, none;
1117    SetClearcoat { entity: Ref [entity], factor: f32 [copy], roughness: f32 [copy] } => crate::appearance::set_clearcoat, none;
1118    SetAnisotropy { entity: Ref [entity], strength: f32 [copy], rotation: f32 [copy] } => crate::appearance::set_anisotropy, none;
1119    SetUvTransform { entity: Ref [entity], offset: [f32; 2] [copy], scale: [f32; 2] [copy], rotation: f32 [copy] } => crate::appearance::set_uv_transform, none;
1120    SetSheen { entity: Ref [entity], color: [f32; 3] [copy], roughness: f32 [copy] } => crate::appearance::set_sheen, none;
1121    SetIridescence { entity: Ref [entity], factor: f32 [copy], ior: f32 [copy] } => crate::appearance::set_iridescence, none;
1122    SetSpecular { entity: Ref [entity], factor: f32 [copy], color: [f32; 3] [copy] } => crate::appearance::set_specular, none;
1123    SetNormalScale { entity: Ref [entity], scale: f32 [copy] } => crate::appearance::set_normal_scale, none;
1124    SetOcclusionStrength { entity: Ref [entity], strength: f32 [copy] } => crate::appearance::set_occlusion_strength, none;
1125    SetEmissiveStrength { entity: Ref [entity], strength: f32 [copy] } => crate::appearance::set_emissive_strength, none;
1126    SetThickness { entity: Ref [entity], thickness: f32 [copy] } => crate::appearance::set_thickness, none;
1127
1128    SetTextOutline { entity: Ref [entity], width: f32 [copy], color: [f32; 4] [copy] } => crate::text::set_text_outline, none;
1129
1130    SetMorphWeight { entity: Ref [entity], index: u32 [copy], weight: f32 [copy] } => crate::morph::set_morph_weight, none;
1131
1132    SetWindowTitle { title: String [text] } => crate::window::set_window_title, none;
1133    LockCursor { locked: bool [copy] } => crate::window::lock_cursor, none;
1134    RequestExit {} => crate::window::request_exit, none;
1135
1136    SetRenderLayer { entity: Ref [entity], layer: u32 [copy] } => crate::render::set_render_layer, none;
1137    SetCameraLayers { camera: Ref [entity], mask: u32 [copy] } => crate::render::set_camera_layers, none;
1138
1139    ThirdPersonCamera { target: Ref [entity], distance: f32 [copy] } => crate::camera::third_person_camera, entity;
1140
1141    SpawnCloth { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy], columns: u32 [copy], rows: u32 [copy] } => crate::cloth::spawn_cloth, entity;
1142    ResetCloth { entity: Ref [entity] } => crate::cloth::reset_cloth, none;
1143    SetWind { direction: [f32; 3] [vec3], strength: f32 [copy] } => crate::cloth::set_wind, none;
1144
1145    PauseCutscene {} => crate::cutscene::pause_cutscene, none;
1146    ResumeCutscene {} => crate::cutscene::resume_cutscene, none;
1147    StopCutscene {} => crate::cutscene::stop_cutscene, none;
1148    SeekCutscene { seconds: f32 [copy] } => crate::cutscene::seek_cutscene, none;
1149    SetCutsceneCamera { camera: Ref [entity] } => crate::cutscene::set_cutscene_camera, none;
1150    BindCutsceneActor { name: String [text], entity: Ref [entity] } => crate::cutscene::bind_cutscene_actor, none;
1151
1152    #[cfg(feature = "physics")]
1153    SpawnCylinderBody { position: [f32; 3] [vec3], half_height: f32 [copy], radius: f32 [copy], mass: f32 [copy], color: [f32; 4] [copy] } => crate::physics::spawn_cylinder_body, entity;
1154    #[cfg(feature = "physics")]
1155    SpawnCapsuleBody { position: [f32; 3] [vec3], half_height: f32 [copy], radius: f32 [copy], mass: f32 [copy], color: [f32; 4] [copy] } => crate::scene::spawn_capsule_body, entity;
1156    #[cfg(feature = "physics")]
1157    SetControllerSpeed { entity: Ref [entity], speed: f32 [copy] } => crate::character::set_controller_speed, none;
1158    #[cfg(feature = "physics")]
1159    SetControllerJump { entity: Ref [entity], impulse: f32 [copy] } => crate::character::set_controller_jump, none;
1160    #[cfg(feature = "physics")]
1161    IsGrounded { entity: Ref [entity] } => crate::character::is_grounded, bool;
1162
1163    #[cfg(feature = "terrain")]
1164    EnableTerrain { seed: u32 [copy] } => crate::terrain::enable_terrain, none;
1165    #[cfg(feature = "terrain")]
1166    DisableTerrain {} => crate::terrain::disable_terrain, none;
1167    #[cfg(feature = "terrain")]
1168    SetTerrainHeightRange { min: f32 [copy], max: f32 [copy] } => crate::terrain::set_terrain_height_range, none;
1169    #[cfg(feature = "terrain")]
1170    SetTerrainSnowHeight { height: f32 [copy] } => crate::terrain::set_terrain_snow_height, none;
1171    #[cfg(feature = "grass")]
1172    EnableGrass {} => crate::terrain::enable_grass, none;
1173    #[cfg(feature = "grass")]
1174    DisableGrass {} => crate::terrain::disable_grass, none;
1175
1176    LoadTexture { name: String [text], image_bytes: Vec<u8> [bytes] } => crate::appearance::load_texture, none;
1177    LoadTextureLinear { name: String [text], image_bytes: Vec<u8> [bytes] } => crate::appearance::load_texture_linear, none;
1178    RegisterTexture { name: String [text], width: u32 [copy], height: u32 [copy], rgba: Vec<u8> [bytes] } => crate::appearance::register_texture, none;
1179
1180    ListMaterials {} => crate::materials::list_materials, json;
1181    GetMaterial { name: String [text] } => crate::materials::get_material, json;
1182    RegisterMaterial { name: String [text], base_color: [f32; 4] [copy], metallic: f32 [copy], roughness: f32 [copy], emissive: [f32; 3] [copy] } => register_material_command, text;
1183    UpdateMaterial { name: String [text], base_color: [f32; 4] [copy], metallic: f32 [copy], roughness: f32 [copy], emissive: [f32; 3] [copy] } => update_material_command, none;
1184    SetMaterialVariant { variant: String [text] } => set_material_variant_command, int;
1185
1186    SpawnObjects { shape: Shape [copy], scale: [f32; 3] [vec3], color: [f32; 4] [copy], body: Body [copy], positions: Vec<[f32; 3]> [vec3_list] } => spawn_objects_command, entities;
1187    SpawnInstanced { shape: Shape [copy], transforms: Vec<InstanceWire> [transforms], color: [f32; 4] [copy] } => crate::scene::spawn_instanced, entity;
1188    SpawnInstancedWithMaterial { shape: Shape [copy], transforms: Vec<InstanceWire> [transforms], material: String [text] } => crate::scene::spawn_instanced_with_material, entity;
1189    SetInstances { batch: Ref [entity], transforms: Vec<InstanceWire> [transforms] } => crate::scene::set_instances, none;
1190    SpawnClothSheet { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy] } => crate::scene::spawn_cloth_sheet, entity;
1191    BlendToAnimationNamed { entity: Ref [entity], name: String [text], seconds: f32 [copy] } => crate::scene::blend_to_animation_named, bool;
1192    AddAnimationLayer { entity: Ref [entity], clip_index: usize [copy], weight: f32 [copy] } => crate::scene::add_animation_layer, none;
1193    NameEntity { name: String [text], entity: Ref [entity] } => crate::scene::name_entity, none;
1194
1195    Name { entity: Ref [entity] } => crate::hierarchy::name, text;
1196    SetEntityName { entity: Ref [entity], name: String [text] } => crate::hierarchy::set_name, none;
1197    Children { entity: Ref [entity] } => crate::hierarchy::children, entities;
1198    Descendants { entity: Ref [entity] } => crate::hierarchy::descendants, entities;
1199    Roots {} => crate::hierarchy::roots, entities;
1200    SceneTree {} => crate::hierarchy::scene_tree, json;
1201
1202    MaterialOf { entity: Ref [entity] } => crate::inspect::material_of, json;
1203    GetColor { entity: Ref [entity] } => crate::inspect::get_color, json;
1204    GetMetallicRoughness { entity: Ref [entity] } => crate::inspect::get_metallic_roughness, json;
1205    GetEmissive { entity: Ref [entity] } => crate::inspect::get_emissive, json;
1206    GetUnlit { entity: Ref [entity] } => crate::inspect::get_unlit, json;
1207    GetTexture { entity: Ref [entity] } => crate::inspect::get_texture, json;
1208    DescribeEntity { entity: Ref [entity] } => crate::inspect::describe_entity, json;
1209
1210    SetTextAlignment { entity: Ref [entity], alignment: TextAlignment [copy] } => crate::text::set_text_alignment, none;
1211
1212    SetMorphWeights { entity: Ref [entity], weights: Vec<f32> [floats] } => crate::morph::set_morph_weights, none;
1213    MorphWeight { entity: Ref [entity], index: u32 [copy] } => crate::morph::morph_weight, float;
1214    MorphTargetCount { entity: Ref [entity] } => crate::morph::morph_target_count, int;
1215
1216    SetFog { enabled: bool [copy], color: [f32; 3] [copy], start: f32 [copy], end: f32 [copy] } => set_fog_command, none;
1217    SetDepthOfField { enabled: bool [copy], focus_distance: f32 [copy], focus_range: f32 [copy], max_blur_radius: f32 [copy], bokeh_threshold: f32 [copy] } => set_depth_of_field_command, none;
1218    Screenshot { path: String [text] } => screenshot_command, none;
1219
1220    SetShadingMode { mode: String [text] } => set_shading_mode_command, none;
1221
1222    AnimatePosition { entity: Ref [entity], to: [f32; 3] [vec3], seconds: f32 [copy], easing: String [text] } => animate_position_command, none;
1223    AnimateScale { entity: Ref [entity], to: [f32; 3] [vec3], seconds: f32 [copy], easing: String [text] } => animate_scale_command, none;
1224    AnimateColor { entity: Ref [entity], to: [f32; 4] [copy], seconds: f32 [copy], easing: String [text] } => animate_color_command, none;
1225    ShakeCamera { strength: f32 [copy], seconds: f32 [copy] } => crate::animate::shake_camera, none;
1226    ReachTo { root: Ref [entity], mid: Ref [entity], tip: Ref [entity], target: [f32; 3] [vec3], pole: Option<[f32; 3]> [opt_vec3] } => crate::animate::reach_to, none;
1227
1228    Bounds { entity: Ref [entity] } => crate::bounds::bounds, json;
1229    BoundsOf { entities: Vec<Ref> [refs] } => crate::bounds::bounds_of, json;
1230    FrameEntities { entities: Vec<Ref> [refs] } => crate::bounds::frame_entities, none;
1231
1232    SpawnParticleEmitter { emitter: EmitterWire [owned] } => spawn_particle_emitter_command, entity;
1233    SetEmitter { emitter_entity: Ref [entity], emitter: EmitterWire [owned] } => set_emitter_command, none;
1234
1235    SpawnDecal { texture: String [text], position: [f32; 3] [vec3], normal: [f32; 3] [vec3], size: f32 [copy] } => crate::decals::spawn_decal, entity;
1236
1237    SaveScene { name: String [text] } => save_scene_command, bytes;
1238    LoadScene { bytes: Vec<u8> [bytes] } => load_scene_command, entities;
1239
1240    WindowSize {} => crate::window::window_size, json;
1241    CursorLocked {} => crate::window::cursor_locked, bool;
1242    FramesPerSecond {} => crate::window::frames_per_second, float;
1243    FrameCount {} => crate::window::frame_count, int;
1244    UptimeMilliseconds {} => crate::window::uptime_milliseconds, int;
1245
1246    #[cfg(feature = "navmesh")]
1247    BakeNavmeshWith { config: RecastConfigWire [owned] } => bake_navmesh_with_command, none;
1248
1249    #[cfg(feature = "physics")]
1250    Raycast { origin: [f32; 3] [vec3], direction: [f32; 3] [vec3], max_distance: f32 [copy] } => raycast_command, json;
1251    #[cfg(feature = "physics")]
1252    AttachFixed { parent: Ref [entity], child: Ref [entity] } => attach_fixed_command, bool;
1253    #[cfg(feature = "physics")]
1254    AttachHinge { parent: Ref [entity], child: Ref [entity], axis: String [text] } => attach_hinge_command, bool;
1255    #[cfg(feature = "physics")]
1256    AttachSpring { parent: Ref [entity], child: Ref [entity], rest_length: f32 [copy], stiffness: f32 [copy], damping: f32 [copy] } => attach_spring_command, bool;
1257    #[cfg(feature = "physics")]
1258    AttachRope { parent: Ref [entity], child: Ref [entity], max_distance: f32 [copy] } => attach_rope_command, bool;
1259    #[cfg(feature = "physics")]
1260    ControllerVelocity { entity: Ref [entity] } => crate::character::controller_velocity, vector;
1261    #[cfg(feature = "physics")]
1262    MoveCharacter { entity: Ref [entity], movement: [f32; 2] [vec2], jump: bool [copy] } => crate::character::move_character, none;
1263
1264    #[cfg(feature = "picking")]
1265    RequestSurfacePick { screen_pos: [f32; 2] [vec2] } => crate::picking::request_surface_pick, none;
1266    #[cfg(feature = "picking")]
1267    TakeSurfacePick {} => take_surface_pick_command, json;
1268    #[cfg(feature = "picking")]
1269    WorldButtonHovered { button: Ref [entity] } => crate::world_ui::world_button_hovered, bool;
1270
1271    #[cfg(feature = "audio")]
1272    LoadSound { name: String [text], bytes: Vec<u8> [bytes] } => crate::audio::load_sound, none;
1273    #[cfg(feature = "audio")]
1274    PlaySound { name: String [text] } => crate::audio::play_sound, entity;
1275    #[cfg(feature = "audio")]
1276    PlaySoundLooping { name: String [text] } => crate::audio::play_sound_looping, entity;
1277    #[cfg(feature = "audio")]
1278    PlaySoundAt { name: String [text], position: [f32; 3] [vec3] } => crate::audio::play_sound_at, entity;
1279    #[cfg(feature = "audio")]
1280    SetVolume { entity: Ref [entity], volume: f32 [copy] } => crate::audio::set_volume, none;
1281    #[cfg(feature = "audio")]
1282    StopSound { entity: Ref [entity] } => crate::audio::stop_sound, none;
1283    #[cfg(feature = "audio")]
1284    SetPitch { entity: Ref [entity], rate: f32 [copy] } => crate::audio::set_pitch, none;
1285    #[cfg(feature = "audio")]
1286    SetSpatialDistance { entity: Ref [entity], min: f32 [copy], max: f32 [copy] } => crate::audio::set_spatial_distance, none;
1287
1288    PanelCheckbox { panel: Ref [entity], label: String [text], initial: bool [copy] } => crate::ui::panel_checkbox, entity;
1289    CheckboxValue { checkbox: Ref [entity] } => crate::ui::checkbox_value, bool;
1290    PanelSlider { panel: Ref [entity], min: f32 [copy], max: f32 [copy], initial: f32 [copy] } => crate::ui::panel_slider, entity;
1291    SliderValue { slider: Ref [entity] } => crate::ui::slider_value, float;
1292    SetSliderValue { slider: Ref [entity], value: f32 [copy] } => crate::ui::set_slider_value, none;
1293    PanelTextInput { panel: Ref [entity], placeholder: String [text] } => crate::ui::panel_text_input, entity;
1294    TextInputChanged { input: Ref [entity] } => crate::ui::text_input_changed, json;
1295    PanelDropdown { panel: Ref [entity], options: Vec<String> [strs], initial: usize [copy] } => crate::ui::panel_dropdown, entity;
1296    DropdownSelected { dropdown: Ref [entity] } => crate::ui::dropdown_selected, json;
1297    PanelProgressBar { panel: Ref [entity], initial: f32 [copy] } => crate::ui::panel_progress_bar, entity;
1298    SetProgress { bar: Ref [entity], value: f32 [copy] } => crate::ui::set_progress, none;
1299    PanelToggle { panel: Ref [entity], initial: bool [copy] } => crate::ui::panel_toggle, entity;
1300    ToggleValue { toggle: Ref [entity] } => crate::ui::toggle_value, bool;
1301    PanelRadio { panel: Ref [entity], label: String [text], group_id: u32 [copy], option_index: usize [copy] } => crate::ui::panel_radio, entity;
1302    RadioSelected { group_id: u32 [copy] } => crate::ui::radio_selected, json;
1303    PanelRangeSlider { panel: Ref [entity], min: f32 [copy], max: f32 [copy], low: f32 [copy], high: f32 [copy] } => crate::ui::panel_range_slider, entity;
1304    SetRange { slider: Ref [entity], low: f32 [copy], high: f32 [copy] } => crate::ui::set_range, none;
1305    PanelTabs { panel: Ref [entity], labels: Vec<String> [strs], initial: usize [copy] } => crate::ui::panel_tabs, entity;
1306    SetTab { tabs: Ref [entity], index: usize [copy] } => crate::ui::set_tab, none;
1307    PanelCollapsing { panel: Ref [entity], label: String [text], open: bool [copy] } => crate::ui::panel_collapsing, entity;
1308    PanelColorPicker { panel: Ref [entity], initial: [f32; 4] [copy] } => crate::ui::panel_color_picker, entity;
1309    ColorValue { picker: Ref [entity] } => crate::ui::color_value, json;
1310    PanelTextArea { panel: Ref [entity], placeholder: String [text], rows: usize [copy] } => crate::ui::panel_text_area, entity;
1311    PanelTextAreaWithValue { panel: Ref [entity], placeholder: String [text], rows: usize [copy], initial: String [text] } => crate::ui::panel_text_area_with_value, entity;
1312    SetTextArea { area: Ref [entity], text: String [text] } => crate::ui::set_text_area, none;
1313    PanelMultiSelect { panel: Ref [entity], options: Vec<String> [strs] } => crate::ui::panel_multi_select, entity;
1314    SetMultiSelect { widget: Ref [entity], indices: Vec<u32> [indices] } => crate::ui::set_multi_select, none;
1315    PanelDatePicker { panel: Ref [entity], year: i32 [copy], month: u32 [copy], day: u32 [copy] } => crate::ui::panel_date_picker, entity;
1316    SetDate { picker: Ref [entity], year: i32 [copy], month: u32 [copy], day: u32 [copy] } => crate::ui::set_date, none;
1317    PanelMenu { panel: Ref [entity], label: String [text], items: Vec<String> [strs] } => crate::ui::panel_menu, entity;
1318    PanelColorPickerHsv { panel: Ref [entity], initial: [f32; 4] [copy] } => crate::ui::panel_color_picker_hsv, entity;
1319    PanelSplitter { panel: Ref [entity], horizontal: bool [copy], ratio: f32 [copy] } => panel_splitter_command, entity;
1320    PanelBreadcrumb { panel: Ref [entity], segments: Vec<String> [strs] } => crate::ui::panel_breadcrumb, entity;
1321    PanelVirtualList { panel: Ref [entity], item_height: f32 [copy], pool_size: usize [copy] } => crate::ui::panel_virtual_list, entity;
1322    PanelTable { panel: Ref [entity], headers: Vec<String> [strs], widths: Vec<f32> [floats] } => crate::ui::panel_table, entity;
1323    PanelDataGrid { panel: Ref [entity], headers: Vec<String> [strs], widths: Vec<f32> [floats], pool_size: usize [copy] } => panel_data_grid_command, entity;
1324    SetDataGridRows { grid: Ref [entity], count: usize [copy] } => crate::ui::set_data_grid_rows, none;
1325    SetDataGridCell { grid: Ref [entity], row: usize [copy], column: usize [copy], text: String [text] } => crate::ui::set_data_grid_cell, none;
1326    DataGridSelectionChanged { grid: Ref [entity] } => crate::ui::data_grid_selection_changed, bool;
1327    PanelCommandPalette { panel: Ref [entity], pool_size: usize [copy] } => crate::ui::panel_command_palette, entity;
1328    PanelPropertyGrid { panel: Ref [entity], label_width: f32 [copy] } => crate::ui::panel_property_grid, entity;
1329    PanelPropertyRow { grid: Ref [entity], label: String [text] } => crate::ui::panel_property_row, entity;
1330    PanelTreeView { panel: Ref [entity], multi_select: bool [copy] } => crate::ui::panel_tree_view, entity;
1331    TreeContent { tree_view: Ref [entity] } => crate::ui::tree_content, entity;
1332    TreeNode { tree_view: Ref [entity], parent_container: Ref [entity], label: String [text], depth: usize [copy], user_data: u64 [copy] } => crate::ui::tree_node, entity;
1333    TreeNodeChildren { node: Ref [entity] } => crate::ui::tree_node_children, entity;
1334    SetTreeNodeExpanded { node: Ref [entity], expanded: bool [copy] } => crate::ui::set_tree_node_expanded, none;
1335    TreeViewSelected { tree_view: Ref [entity] } => crate::ui::tree_view_selected, entities;
1336    PanelDragValue { panel: Ref [entity], min: f32 [copy], max: f32 [copy], initial: f32 [copy] } => crate::ui::panel_drag_value, entity;
1337    DragValue { widget: Ref [entity] } => crate::ui::drag_value, float;
1338    PanelSelectable { panel: Ref [entity], text: String [text], group: u32 [copy], grouped: bool [copy] } => panel_selectable_command, entity;
1339    PanelModal { panel: Ref [entity], title: String [text], width: f32 [copy], height: f32 [copy] } => crate::ui::panel_modal, entity;
1340    PanelSpinner { panel: Ref [entity] } => crate::ui::panel_spinner, entity;
1341    PanelSeparator { panel: Ref [entity] } => crate::ui::panel_separator, entity;
1342    PanelHeading { panel: Ref [entity], text: String [text] } => crate::ui::panel_heading, entity;
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347    use super::*;
1348
1349    #[test]
1350    fn command_schema_covers_the_surface() {
1351        let schema = command_schema();
1352        assert!(schema.get("oneOf").is_some());
1353        let text = enum2schema::serde_json::to_string(&schema).unwrap();
1354        for variant in [
1355            "SpawnCube",
1356            "SpawnObject",
1357            "SetColor",
1358            "Rotate",
1359            "QueryTagged",
1360        ] {
1361            assert!(text.contains(variant), "schema missing {variant}");
1362        }
1363    }
1364
1365    #[test]
1366    fn reply_schema_is_generated() {
1367        assert!(command_reply_schema().get("oneOf").is_some());
1368    }
1369
1370    #[test]
1371    fn manifest_covers_the_surface() {
1372        let manifest = command_manifest();
1373        assert!(!manifest.is_empty());
1374        let names: Vec<&str> = manifest.iter().map(|spec| spec.name).collect();
1375        for variant in [
1376            "SpawnCube",
1377            "SpawnObject",
1378            "SetColor",
1379            "Rotate",
1380            "QueryTagged",
1381        ] {
1382            assert!(names.contains(&variant), "manifest missing {variant}");
1383        }
1384        let replies = [
1385            "none",
1386            "entity",
1387            "opt_entity",
1388            "bool",
1389            "float",
1390            "int",
1391            "text",
1392            "vector",
1393            "opt_vector",
1394            "entities",
1395            "strings",
1396            "bytes",
1397            "json",
1398        ];
1399        for spec in &manifest {
1400            assert!(
1401                replies.contains(&spec.reply),
1402                "unknown reply {}",
1403                spec.reply
1404            );
1405        }
1406    }
1407
1408    #[test]
1409    fn entity_reference_serializes_as_its_schema_claims() {
1410        let entity = Entity {
1411            id: 3,
1412            generation: 1,
1413        };
1414        let value = enum2schema::serde_json::to_value(Ref::Entity(entity)).unwrap();
1415        let inner = value
1416            .get("Entity")
1417            .and_then(|tagged| tagged.as_object())
1418            .expect("Ref::Entity serializes as an externally tagged object");
1419        assert!(inner.contains_key("id"));
1420        assert!(inner.contains_key("generation"));
1421        assert_eq!(inner.len(), 2);
1422    }
1423}