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`](crate::prelude::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`](crate::prelude::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`](crate::prelude::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, reply kind, and a one-line
75/// description of what it does. A binding generator emits the description as a
76/// doc comment so every language surface is documented from one source.
77#[derive(Serialize, Clone, Debug)]
78pub struct CommandSpec {
79    pub name: &'static str,
80    pub fields: Vec<FieldSpec>,
81    pub reply: &'static str,
82    pub description: &'static str,
83}
84
85/// [`command_manifest`] as a json string, the input a binding code generator
86/// reads alongside [`command_schema`].
87pub fn command_manifest_json() -> String {
88    enum2schema::serde_json::to_string(&command_manifest()).unwrap_or_default()
89}
90
91fn entity_schema() -> enum2schema::serde_json::Value {
92    enum2schema::serde_json::json!({
93        "type": "object",
94        "properties": {
95            "id": { "type": "integer" },
96            "generation": { "type": "integer" }
97        },
98        "required": ["id", "generation"]
99    })
100}
101
102fn entities_schema() -> enum2schema::serde_json::Value {
103    enum2schema::serde_json::json!({ "type": "array", "items": entity_schema() })
104}
105
106fn any_schema() -> enum2schema::serde_json::Value {
107    enum2schema::serde_json::json!({})
108}
109
110/// The json schema for [`Command`], the wire form a binding builds. Derived from
111/// the command enum by the same registry that defines it, so it always matches
112/// the surface. Pair with [`command_reply_schema`] for the output shape.
113pub fn command_schema() -> enum2schema::serde_json::Value {
114    <Command as enum2schema::Schema>::schema()
115}
116
117/// The json schema for [`CommandReply`], what a binding reads back.
118pub fn command_reply_schema() -> enum2schema::serde_json::Value {
119    <CommandReply as enum2schema::Schema>::schema()
120}
121
122/// Runs one command and returns its reply.
123pub fn submit_command(world: &mut World, command: &Command) -> CommandReply {
124    dispatch(world, command, &[])
125}
126
127/// Runs a batch in order and returns one reply per command. A command may name
128/// an entity an earlier command in the same batch produced with [`Ref::Result`],
129/// so a batch can spawn entities and then configure and parent them in one call.
130pub fn submit_commands(world: &mut World, commands: &[Command]) -> Vec<CommandReply> {
131    let mut produced: Vec<Option<Entity>> = Vec::with_capacity(commands.len());
132    let mut replies = Vec::with_capacity(commands.len());
133    for command in commands {
134        let reply = dispatch(world, command, &produced);
135        produced.push(match &reply {
136            CommandReply::Entity(entity) => Some(*entity),
137            _ => None,
138        });
139        replies.push(reply);
140    }
141    replies
142}
143
144fn resolve(world: &World, reference: Ref, produced: &[Option<Entity>]) -> Option<Entity> {
145    match reference {
146        Ref::Entity(entity) => Some(entity),
147        Ref::Result(index) => produced.get(index as usize).copied().flatten(),
148        Ref::Existing(id) => world
149            .core
150            .entity_locations
151            .get(id)
152            .filter(|location| location.allocated)
153            .map(|location| Entity {
154                id,
155                generation: location.generation,
156            }),
157    }
158}
159
160fn array_to_vec3(values: [f32; 3]) -> Vec3 {
161    vec3(values[0], values[1], values[2])
162}
163
164fn array_to_vec2(values: [f32; 2]) -> nightshade::prelude::Vec2 {
165    nightshade::prelude::vec2(values[0], values[1])
166}
167
168/// Adapts the flat command fields to [`spawn_object`](crate::scene::spawn_object),
169/// which takes an [`Object`] struct. The dispatch macro calls functions with
170/// positional arguments, so the one command that builds a struct goes through
171/// this rather than special casing the macro.
172fn spawn_object_command(
173    world: &mut World,
174    shape: Shape,
175    position: Vec3,
176    scale: Vec3,
177    color: [f32; 4],
178    body: Body,
179) -> Entity {
180    crate::scene::spawn_object(
181        world,
182        Object {
183            shape,
184            position,
185            scale,
186            color,
187            body,
188        },
189    )
190}
191
192/// Wire form of an [`InstanceTransform`](nightshade::prelude::InstanceTransform)
193/// for the instancing commands: a position, a rotation quaternion as
194/// `[x, y, z, w]`, and a scale.
195#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, enum2schema::Schema)]
196pub struct InstanceWire {
197    pub position: [f32; 3],
198    pub rotation: [f32; 4],
199    pub scale: [f32; 3],
200}
201
202fn instance_from_wire(wire: &InstanceWire) -> nightshade::prelude::InstanceTransform {
203    use nightshade::prelude::nalgebra_glm::Quat;
204    nightshade::prelude::InstanceTransform::new(
205        array_to_vec3(wire.position),
206        Quat::new(
207            wire.rotation[3],
208            wire.rotation[0],
209            wire.rotation[1],
210            wire.rotation[2],
211        ),
212        array_to_vec3(wire.scale),
213    )
214}
215
216fn register_material_command(
217    world: &mut World,
218    name: &str,
219    base_color: [f32; 4],
220    metallic: f32,
221    roughness: f32,
222    emissive: [f32; 3],
223) -> String {
224    crate::materials::register_material(
225        world,
226        name,
227        nightshade::ecs::material::components::Material {
228            base_color,
229            metallic,
230            roughness,
231            emissive_factor: emissive,
232            ..Default::default()
233        },
234    )
235}
236
237fn spawn_objects_command(
238    world: &mut World,
239    shape: Shape,
240    scale: Vec3,
241    color: [f32; 4],
242    body: Body,
243    positions: &[Vec3],
244) -> Vec<Entity> {
245    crate::scene::spawn_objects(
246        world,
247        Object {
248            shape,
249            position: vec3(0.0, 0.0, 0.0),
250            scale,
251            color,
252            body,
253        },
254        positions,
255    )
256}
257
258#[cfg(feature = "navmesh")]
259#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, enum2schema::Schema)]
260pub struct RecastConfigWire {
261    pub agent_radius: f32,
262    pub agent_height: f32,
263    pub cell_size_fraction: f32,
264    pub cell_height_fraction: f32,
265    pub walkable_climb: f32,
266    pub walkable_slope_angle: f32,
267    pub min_region_size: u32,
268    pub merge_region_size: u32,
269    pub max_simplification_error: f32,
270    pub edge_max_len_factor: u32,
271    pub max_vertices_per_polygon: u32,
272    pub detail_sample_dist: f32,
273    pub detail_sample_max_error: f32,
274}
275
276#[cfg(feature = "navmesh")]
277fn bake_navmesh_with_command(world: &mut World, config: RecastConfigWire) {
278    crate::navigation::bake_navmesh_with(
279        world,
280        &nightshade::prelude::RecastNavMeshConfig {
281            agent_radius: config.agent_radius,
282            agent_height: config.agent_height,
283            cell_size_fraction: config.cell_size_fraction,
284            cell_height_fraction: config.cell_height_fraction,
285            walkable_climb: config.walkable_climb,
286            walkable_slope_angle: config.walkable_slope_angle,
287            min_region_size: config.min_region_size as u16,
288            merge_region_size: config.merge_region_size as u16,
289            max_simplification_error: config.max_simplification_error,
290            edge_max_len_factor: config.edge_max_len_factor as u16,
291            max_vertices_per_polygon: config.max_vertices_per_polygon as u16,
292            detail_sample_dist: config.detail_sample_dist,
293            detail_sample_max_error: config.detail_sample_max_error,
294        },
295    );
296}
297
298/// Wire form of a curated [`ParticleEmitter`](nightshade::prelude::ParticleEmitter):
299/// the common knobs, with a single base color expanded into an explosion
300/// gradient. The Rust `spawn_particle_emitter` takes the full struct for finer
301/// control.
302#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, enum2schema::Schema)]
303pub struct EmitterWire {
304    pub position: [f32; 3],
305    pub color: [f32; 3],
306    pub spawn_rate: f32,
307    pub burst_count: u32,
308    pub lifetime: [f32; 2],
309    pub size: [f32; 2],
310    pub gravity: [f32; 3],
311    pub drag: f32,
312    pub one_shot: bool,
313}
314
315fn emitter_from_wire(wire: &EmitterWire) -> nightshade::prelude::ParticleEmitter {
316    let mut emitter = nightshade::prelude::ParticleEmitter::firework_explosion(
317        array_to_vec3(wire.position),
318        array_to_vec3(wire.color),
319        wire.burst_count,
320    );
321    emitter.spawn_rate = wire.spawn_rate;
322    emitter.particle_lifetime_min = wire.lifetime[0];
323    emitter.particle_lifetime_max = wire.lifetime[1];
324    emitter.size_start = wire.size[0];
325    emitter.size_end = wire.size[1];
326    emitter.gravity = array_to_vec3(wire.gravity);
327    emitter.drag = wire.drag;
328    emitter.one_shot = wire.one_shot;
329    emitter.enabled = true;
330    emitter
331}
332
333fn spawn_particle_emitter_command(world: &mut World, emitter: EmitterWire) -> Entity {
334    crate::effects::spawn_particle_emitter(world, emitter_from_wire(&emitter))
335}
336
337fn set_emitter_command(world: &mut World, emitter_entity: Entity, emitter: EmitterWire) {
338    crate::effects::set_emitter(world, emitter_entity, emitter_from_wire(&emitter));
339}
340
341fn update_material_command(
342    world: &mut World,
343    name: &str,
344    base_color: [f32; 4],
345    metallic: f32,
346    roughness: f32,
347    emissive: [f32; 3],
348) {
349    crate::materials::update_material(
350        world,
351        name,
352        nightshade::ecs::material::components::Material {
353            base_color,
354            metallic,
355            roughness,
356            emissive_factor: emissive,
357            ..Default::default()
358        },
359    );
360}
361
362fn set_material_variant_command(world: &mut World, variant: &str) -> usize {
363    let variant = if variant.is_empty() {
364        None
365    } else {
366        Some(variant)
367    };
368    crate::materials::set_material_variant(world, variant)
369}
370
371fn set_fog_command(world: &mut World, enabled: bool, color: [f32; 3], start: f32, end: f32) {
372    let fog = if enabled {
373        Some(nightshade::ecs::graphics::resources::Fog { color, start, end })
374    } else {
375        None
376    };
377    crate::environment::set_fog(world, fog);
378}
379
380fn set_depth_of_field_command(
381    world: &mut World,
382    enabled: bool,
383    focus_distance: f32,
384    focus_range: f32,
385    max_blur_radius: f32,
386    bokeh_threshold: f32,
387) {
388    crate::environment::set_depth_of_field(
389        world,
390        nightshade::ecs::graphics::resources::DepthOfField {
391            enabled,
392            focus_distance,
393            focus_range,
394            max_blur_radius,
395            bokeh_threshold,
396            ..Default::default()
397        },
398    );
399}
400
401fn panel_data_grid_command(
402    world: &mut World,
403    panel: Entity,
404    headers: &[&str],
405    widths: &[f32],
406    pool_size: usize,
407) -> Entity {
408    let columns: Vec<(&str, f32)> = headers
409        .iter()
410        .zip(widths.iter())
411        .map(|(header, width)| (*header, *width))
412        .collect();
413    crate::ui::panel_data_grid(world, panel, &columns, pool_size)
414}
415
416fn panel_selectable_command(
417    world: &mut World,
418    panel: Entity,
419    text: &str,
420    group: u32,
421    grouped: bool,
422) -> Entity {
423    crate::ui::panel_selectable(world, panel, text, grouped.then_some(group))
424}
425
426fn panel_splitter_command(
427    world: &mut World,
428    panel: Entity,
429    horizontal: bool,
430    ratio: f32,
431) -> Entity {
432    let direction = if horizontal {
433        nightshade::prelude::SplitDirection::Horizontal
434    } else {
435        nightshade::prelude::SplitDirection::Vertical
436    };
437    crate::ui::panel_splitter(world, panel, direction, ratio)
438}
439
440fn screenshot_command(world: &mut World, path: &str) {
441    crate::environment::screenshot(world, std::path::PathBuf::from(path));
442}
443
444fn easing_from_name(name: &str) -> nightshade::prelude::EasingFunction {
445    use nightshade::prelude::EasingFunction::*;
446    match name.to_ascii_lowercase().as_str() {
447        "quadin" => QuadIn,
448        "quadout" => QuadOut,
449        "quadinout" => QuadInOut,
450        "cubicin" => CubicIn,
451        "cubicout" => CubicOut,
452        "cubicinout" => CubicInOut,
453        "quartin" => QuartIn,
454        "quartout" => QuartOut,
455        "quartinout" => QuartInOut,
456        "quintin" => QuintIn,
457        "quintout" => QuintOut,
458        "quintinout" => QuintInOut,
459        "sinein" => SineIn,
460        "sineout" => SineOut,
461        "sineinout" => SineInOut,
462        "expoin" => ExpoIn,
463        "expoout" => ExpoOut,
464        "expoinout" => ExpoInOut,
465        _ => Linear,
466    }
467}
468
469fn animate_position_command(
470    world: &mut World,
471    entity: Entity,
472    to: Vec3,
473    seconds: f32,
474    easing: &str,
475) {
476    crate::animate::animate_position(world, entity, to, seconds, easing_from_name(easing));
477}
478
479fn animate_scale_command(world: &mut World, entity: Entity, to: Vec3, seconds: f32, easing: &str) {
480    crate::animate::animate_scale(world, entity, to, seconds, easing_from_name(easing));
481}
482
483fn animate_color_command(
484    world: &mut World,
485    entity: Entity,
486    to: [f32; 4],
487    seconds: f32,
488    easing: &str,
489) {
490    crate::animate::animate_color(world, entity, to, seconds, easing_from_name(easing));
491}
492
493fn set_shading_mode_command(world: &mut World, mode: &str) {
494    use nightshade::prelude::ShadingMode;
495    let mode = match mode.to_ascii_lowercase().as_str() {
496        "wireframe" => ShadingMode::Wireframe,
497        "flat" => ShadingMode::Flat,
498        "rendered" => ShadingMode::Rendered,
499        _ => ShadingMode::Solid,
500    };
501    crate::camera::set_shading_mode(world, mode);
502}
503
504#[cfg(feature = "physics")]
505#[derive(Serialize)]
506struct RaycastResultWire {
507    entity_id: u32,
508    distance: f32,
509    point: [f32; 3],
510    normal: [f32; 3],
511}
512
513#[cfg(feature = "physics")]
514fn raycast_command(
515    world: &mut World,
516    origin: Vec3,
517    direction: Vec3,
518    max_distance: f32,
519) -> Option<RaycastResultWire> {
520    crate::physics::raycast(world, origin, direction, max_distance).map(|hit| RaycastResultWire {
521        entity_id: hit.entity.id,
522        distance: hit.distance,
523        point: [hit.point.x, hit.point.y, hit.point.z],
524        normal: [hit.normal.x, hit.normal.y, hit.normal.z],
525    })
526}
527
528#[cfg(feature = "physics")]
529fn attach_fixed_command(world: &mut World, parent: Entity, child: Entity) -> bool {
530    crate::physics::attach_fixed(world, parent, child).is_some()
531}
532
533#[cfg(feature = "physics")]
534fn attach_hinge_command(world: &mut World, parent: Entity, child: Entity, axis: &str) -> bool {
535    use nightshade::ecs::physics::joints::JointAxisDirection;
536    let axis = match axis.to_ascii_lowercase().as_str() {
537        "y" => JointAxisDirection::Y,
538        "z" => JointAxisDirection::Z,
539        _ => JointAxisDirection::X,
540    };
541    crate::physics::attach_hinge(world, parent, child, axis).is_some()
542}
543
544#[cfg(feature = "physics")]
545fn attach_spring_command(
546    world: &mut World,
547    parent: Entity,
548    child: Entity,
549    rest_length: f32,
550    stiffness: f32,
551    damping: f32,
552) -> bool {
553    crate::physics::attach_spring(world, parent, child, rest_length, stiffness, damping).is_some()
554}
555
556#[cfg(feature = "physics")]
557fn attach_rope_command(
558    world: &mut World,
559    parent: Entity,
560    child: Entity,
561    max_distance: f32,
562) -> bool {
563    crate::physics::attach_rope(world, parent, child, max_distance).is_some()
564}
565
566#[cfg(feature = "picking")]
567#[derive(Serialize)]
568struct SurfacePickWire {
569    world_position: [f32; 3],
570    world_normal: [f32; 3],
571    depth: f32,
572    entity_id: Option<u32>,
573}
574
575#[cfg(feature = "picking")]
576fn take_surface_pick_command(world: &mut World) -> Option<SurfacePickWire> {
577    crate::picking::take_surface_pick(world).map(|result| SurfacePickWire {
578        world_position: [
579            result.world_position.x,
580            result.world_position.y,
581            result.world_position.z,
582        ],
583        world_normal: [
584            result.world_normal.x,
585            result.world_normal.y,
586            result.world_normal.z,
587        ],
588        depth: result.depth,
589        entity_id: result.entity_id,
590    })
591}
592
593fn save_scene_command(world: &mut World, name: &str) -> Vec<u8> {
594    crate::serialize::save_scene(world, name).unwrap_or_default()
595}
596
597fn load_scene_command(world: &mut World, bytes: &[u8]) -> Vec<Entity> {
598    crate::serialize::load_scene(world, bytes).unwrap_or_default()
599}
600
601/// Maps a plain key name to a [`KeyCode`](crate::prelude::KeyCode) for the input-query commands, so the
602/// wire form names keys as strings like `"a"`, `"space"`, or `"left"` rather
603/// than carrying the engine's key enum.
604fn key_from_name(name: &str) -> Option<KeyCode> {
605    let lower = name.to_ascii_lowercase();
606    Some(match lower.as_str() {
607        "a" => KeyCode::KeyA,
608        "b" => KeyCode::KeyB,
609        "c" => KeyCode::KeyC,
610        "d" => KeyCode::KeyD,
611        "e" => KeyCode::KeyE,
612        "f" => KeyCode::KeyF,
613        "g" => KeyCode::KeyG,
614        "h" => KeyCode::KeyH,
615        "i" => KeyCode::KeyI,
616        "j" => KeyCode::KeyJ,
617        "k" => KeyCode::KeyK,
618        "l" => KeyCode::KeyL,
619        "m" => KeyCode::KeyM,
620        "n" => KeyCode::KeyN,
621        "o" => KeyCode::KeyO,
622        "p" => KeyCode::KeyP,
623        "q" => KeyCode::KeyQ,
624        "r" => KeyCode::KeyR,
625        "s" => KeyCode::KeyS,
626        "t" => KeyCode::KeyT,
627        "u" => KeyCode::KeyU,
628        "v" => KeyCode::KeyV,
629        "w" => KeyCode::KeyW,
630        "x" => KeyCode::KeyX,
631        "y" => KeyCode::KeyY,
632        "z" => KeyCode::KeyZ,
633        "0" => KeyCode::Digit0,
634        "1" => KeyCode::Digit1,
635        "2" => KeyCode::Digit2,
636        "3" => KeyCode::Digit3,
637        "4" => KeyCode::Digit4,
638        "5" => KeyCode::Digit5,
639        "6" => KeyCode::Digit6,
640        "7" => KeyCode::Digit7,
641        "8" => KeyCode::Digit8,
642        "9" => KeyCode::Digit9,
643        "space" => KeyCode::Space,
644        "enter" | "return" => KeyCode::Enter,
645        "escape" | "esc" => KeyCode::Escape,
646        "tab" => KeyCode::Tab,
647        "backspace" => KeyCode::Backspace,
648        "delete" => KeyCode::Delete,
649        "left" => KeyCode::ArrowLeft,
650        "right" => KeyCode::ArrowRight,
651        "up" => KeyCode::ArrowUp,
652        "down" => KeyCode::ArrowDown,
653        "shift" | "lshift" => KeyCode::ShiftLeft,
654        "rshift" => KeyCode::ShiftRight,
655        "ctrl" | "control" | "lctrl" => KeyCode::ControlLeft,
656        "rctrl" => KeyCode::ControlRight,
657        "alt" | "lalt" => KeyCode::AltLeft,
658        "ralt" => KeyCode::AltRight,
659        _ => return None,
660    })
661}
662
663/// Maps a button index to a [`MouseButton`](crate::prelude::MouseButton): 0 left, 1 middle, 2 right.
664fn mouse_button_from_index(index: u8) -> MouseButton {
665    match index {
666        1 => MouseButton::Middle,
667        2 => MouseButton::Right,
668        _ => MouseButton::Left,
669    }
670}
671
672fn key_down_command(world: &World, key: &str) -> bool {
673    key_from_name(key)
674        .map(|key| crate::input::key_down(world, key))
675        .unwrap_or(false)
676}
677
678fn key_pressed_command(world: &World, key: &str) -> bool {
679    key_from_name(key)
680        .map(|key| crate::input::key_pressed(world, key))
681        .unwrap_or(false)
682}
683
684fn mouse_down_command(world: &World, button: u8) -> bool {
685    crate::input::mouse_down(world, mouse_button_from_index(button))
686}
687
688fn mouse_clicked_command(world: &World, button: u8) -> bool {
689    crate::input::mouse_clicked(world, mouse_button_from_index(button))
690}
691
692macro_rules! bind_argument {
693    ($field:ident, entity, $produced:ident, $world:ident) => {
694        let $field = match resolve($world, *$field, $produced) {
695            Some(entity) => entity,
696            None => {
697                return CommandReply::Error(
698                    concat!(stringify!($field), ": unresolved entity reference").to_string(),
699                );
700            }
701        };
702    };
703    ($field:ident, opt_entity, $produced:ident, $world:ident) => {
704        let $field = match $field {
705            Some(reference) => match resolve($world, *reference, $produced) {
706                Some(entity) => Some(entity),
707                None => {
708                    return CommandReply::Error(
709                        concat!(stringify!($field), ": unresolved entity reference").to_string(),
710                    );
711                }
712            },
713            None => None,
714        };
715    };
716    ($field:ident, vec3, $produced:ident, $world:ident) => {
717        let $field = array_to_vec3(*$field);
718    };
719    ($field:ident, vec2, $produced:ident, $world:ident) => {
720        let $field = array_to_vec2(*$field);
721    };
722    ($field:ident, copy, $produced:ident, $world:ident) => {
723        let $field = *$field;
724    };
725    ($field:ident, owned, $produced:ident, $world:ident) => {
726        let $field = $field.clone();
727    };
728    ($field:ident, text, $produced:ident, $world:ident) => {
729        let $field = $field.as_str();
730    };
731    ($field:ident, bytes, $produced:ident, $world:ident) => {
732        let $field = $field.as_slice();
733    };
734    ($field:ident, strs, $produced:ident, $world:ident) => {
735        let $field: Vec<&str> = $field.iter().map(|value| value.as_str()).collect();
736        let $field = $field.as_slice();
737    };
738    ($field:ident, vec3_list, $produced:ident, $world:ident) => {
739        let $field: Vec<Vec3> = $field.iter().map(|value| array_to_vec3(*value)).collect();
740        let $field = $field.as_slice();
741    };
742    ($field:ident, floats, $produced:ident, $world:ident) => {
743        let $field = $field.as_slice();
744    };
745    ($field:ident, indices, $produced:ident, $world:ident) => {
746        let $field: Vec<usize> = $field.iter().map(|value| *value as usize).collect();
747        let $field = $field.as_slice();
748    };
749    ($field:ident, opt_vec3, $produced:ident, $world:ident) => {
750        let $field = (*$field).map(array_to_vec3);
751    };
752    ($field:ident, transforms, $produced:ident, $world:ident) => {
753        let $field: Vec<nightshade::prelude::InstanceTransform> =
754            $field.iter().map(instance_from_wire).collect();
755    };
756    ($field:ident, refs, $produced:ident, $world:ident) => {
757        let mut resolved = Vec::with_capacity($field.len());
758        for reference in $field.iter() {
759            match resolve($world, *reference, $produced) {
760                Some(entity) => resolved.push(entity),
761                None => {
762                    return CommandReply::Error(
763                        concat!(stringify!($field), ": unresolved entity reference").to_string(),
764                    );
765                }
766            }
767        }
768        let $field = resolved.as_slice();
769    };
770}
771
772macro_rules! wrap_reply {
773    (none, $call:expr) => {{
774        $call;
775        CommandReply::None
776    }};
777    (entity, $call:expr) => {
778        CommandReply::Entity($call)
779    };
780    (opt_entity, $call:expr) => {
781        match $call {
782            Some(entity) => CommandReply::Entity(entity),
783            None => CommandReply::None,
784        }
785    };
786    (bool, $call:expr) => {
787        CommandReply::Bool($call)
788    };
789    (float, $call:expr) => {
790        CommandReply::Float($call)
791    };
792    (vector, $call:expr) => {{
793        let value = $call;
794        CommandReply::Vector([value.x, value.y, value.z])
795    }};
796    (opt_vector, $call:expr) => {
797        match $call {
798            Some(value) => CommandReply::Vector([value.x, value.y, value.z]),
799            None => CommandReply::None,
800        }
801    };
802    (entities, $call:expr) => {
803        CommandReply::Entities($call)
804    };
805    (strings, $call:expr) => {
806        CommandReply::Strings($call)
807    };
808    (int, $call:expr) => {
809        CommandReply::Int($call as i64)
810    };
811    (text, $call:expr) => {
812        CommandReply::Text($call)
813    };
814    (bytes, $call:expr) => {
815        CommandReply::Bytes($call)
816    };
817    (json, $call:expr) => {
818        CommandReply::Json(
819            enum2schema::serde_json::to_value($call)
820                .unwrap_or(enum2schema::serde_json::Value::Null),
821        )
822    };
823}
824
825/// The rhai method name a command variant is called by: its name in snake_case,
826/// with acronym runs kept together and a digit split off after a letter, so
827/// `SpawnCube` becomes `spawn_cube`, `SetIblIntensity` becomes
828/// `set_ibl_intensity`, and `DrawText3d` becomes `draw_text_3d`. A tool that
829/// highlights or documents the script surface uses this to map a
830/// [`CommandSpec`] name to the identifier a script actually writes.
831#[cfg(feature = "scripting")]
832pub fn command_method_name(variant: &str) -> String {
833    let characters: Vec<char> = variant.chars().collect();
834    let mut name = String::new();
835    for index in 0..characters.len() {
836        let character = characters[index];
837        if character.is_uppercase() {
838            let previous_lower = index > 0
839                && (characters[index - 1].is_lowercase() || characters[index - 1].is_ascii_digit());
840            let previous_upper = index > 0 && characters[index - 1].is_uppercase();
841            let next_lower = index + 1 < characters.len() && characters[index + 1].is_lowercase();
842            if index != 0 && (previous_lower || (previous_upper && next_lower)) {
843                name.push('_');
844            }
845            name.extend(character.to_lowercase());
846        } else if character.is_ascii_digit() {
847            if index > 0 && characters[index - 1].is_alphabetic() {
848                name.push('_');
849            }
850            name.push(character);
851        } else {
852            name.push(character);
853        }
854    }
855    name
856}
857
858/// Wraps a command's named fields in the `{ Variant: { fields } }` shape the
859/// script command collector deserializes into a typed [`Command`], identical to
860/// what the map-literal form produces.
861#[cfg(feature = "scripting")]
862fn command_method_map(name: &str, pairs: Vec<(&'static str, rhai::Dynamic)>) -> rhai::Dynamic {
863    let mut fields = rhai::Map::new();
864    for (key, value) in pairs {
865        fields.insert(key.into(), value);
866    }
867    let mut outer = rhai::Map::new();
868    outer.insert(name.into(), rhai::Dynamic::from_map(fields));
869    rhai::Dynamic::from_map(outer)
870}
871
872/// A one-line description of what each command does, keyed by variant name.
873/// Generated from the command registry, surfaced through [`command_manifest`] so
874/// every binding documents its surface from one source. A command with no entry
875/// reads as empty, which the `every_command_has_a_description` test forbids.
876fn command_description(variant: &str) -> &'static str {
877    match variant {
878        "SpawnCube" => "Spawn a cube at the given position",
879        "SpawnSphere" => "Spawn a sphere at the given position",
880        "SpawnCylinder" => "Spawn a cylinder at the given position",
881        "SpawnCone" => "Spawn a cone at the given position",
882        "SpawnPlane" => "Spawn a plane at the given position",
883        "SpawnTorus" => "Spawn a torus at the given position",
884        "SpawnFloor" => "Spawn a flat ground plane reaching the half extent in each direction",
885        "SpawnGroup" => "Spawn an invisible group at a position for building hierarchies",
886        "SpawnModel" => "Spawn a glb model with its textures, materials, skins, and animations",
887        "SpawnObject" => "Spawn an object with mesh, color, and optional physics body in one call",
888        "SetColor" => "Set the entity's base color as linear RGBA",
889        "SetMetallicRoughness" => "Set the entity's metallic and roughness factors",
890        "SetEmissive" => "Make the entity glow with the given color and strength",
891        "SetUnlit" => "Disable lighting on the entity so its color renders as is",
892        "SetTexture" => "Set the entity's base color texture by name",
893        "SetTextureTiling" => {
894            "Tile the entity's base color texture the given number of times per axis"
895        }
896        "SetNormalTexture" => "Set the entity's normal map by texture name",
897        "SetMetallicRoughnessTexture" => {
898            "Set the entity's metallic and roughness map by texture name"
899        }
900        "SetEmissiveTexture" => "Set the entity's emissive map by texture name",
901        "SetOcclusionTexture" => "Set the entity's ambient occlusion map by texture name",
902        "SetPosition" => "Set the entity's position in its parent's space",
903        "SetScale" => "Set the entity's scale",
904        "SetRotation" => "Replace the entity's rotation with the given angle around an axis",
905        "Rotate" => "Rotate the entity around an axis on top of its current rotation",
906        "Position" => "Get the entity's position in world space",
907        "SetParent" => "Parent a child to a parent or unparent it, keeping its world position",
908        "SetVisible" => "Show or hide the entity without despawning it",
909        "Despawn" => "Despawn the entity and its descendants",
910        "Tag" => "Tag the entity with a label",
911        "Untag" => "Remove a label from the entity",
912        "HasTag" => "Check whether the entity carries the label",
913        "QueryTagged" => "Get every entity carrying the label",
914        "PointLight" => "Add a point light at the given position",
915        "SpotLight" => "Add a shadow casting spot light aimed at a target",
916        "SetSun" => "Adjust the default sun's color and intensity",
917        "SetBackground" => "Set the scene background",
918        "ShowGrid" => "Show or hide the reference grid",
919        "SetAmbient" => "Set the ambient light color as linear RGBA",
920        "SetBloom" => "Toggle bloom",
921        "SetBloomIntensity" => "Set the bloom strength",
922        "SetSsao" => "Toggle screen-space ambient occlusion",
923        "SetSsr" => "Toggle screen-space reflections on glossy surfaces",
924        "SetSsgi" => "Toggle screen-space global illumination",
925        "SetFxaa" => "Toggle FXAA full-screen antialiasing",
926        "SetExposure" => "Set the manual exposure multiplier",
927        "SetColorGrading" => "Set saturation, contrast, and brightness color grading",
928        "SetTimeOfDay" => "Set the hour of the day from 0 to 24",
929        "SetTitle" => "Set the window title",
930        "EmitFire" => "Emit a continuous fire at the given position",
931        "EmitSmoke" => "Emit a continuous smoke column at the given position",
932        "EmitBurst" => "Emit a one-shot burst of colored particles at the given position",
933        "DrawCube" => "Draw a cube with the given size and color for one frame",
934        "DrawSphere" => "Draw a sphere with the given radius and color for one frame",
935        "DrawCylinder" => "Draw an upright cylinder with the given size and color for one frame",
936        "DrawCone" => "Draw an upright cone with the given size and color for one frame",
937        "DrawTorus" => "Draw a flat torus with the given size and color for one frame",
938        "DrawLine" => "Draw a line from start to end for one frame",
939        "DrawText3d" => "Draw billboard text at a 3d position for one frame",
940        "SpawnLabel" => "Spawn 3d text at a position that always faces the camera",
941        "SpawnText" => "Spawn screen text at the given anchor",
942        "SetText" => "Replace the content of a text entity",
943        "SetTextColor" => "Set a text entity's color as linear RGBA",
944        "SetTextSize" => "Set a text entity's font size",
945        "SpawnPanel" => {
946            "Spawn an empty panel anchored to a window corner or center, sized in pixels"
947        }
948        "PanelLabel" => "Add a line of text to a panel",
949        "PanelButton" => "Add a themed button to a panel",
950        "ButtonClicked" => "Check whether the button was clicked this frame",
951        "ButtonHovered" => "Check whether the pointer is over the button",
952        "DespawnPanel" => "Remove a panel and everything in it",
953        "PanelRow" => "Add a horizontal row to a panel",
954        "PanelGrid" => "Add a fixed-column grid to a panel",
955        "PanelScroll" => "Add a scrollable region to a panel and return its content container",
956        "SetScrollOffset" => "Scroll a panel-scroll region to a pixel offset from the top",
957        "SetFocusOrder" => "Set a widget's keyboard focus order",
958        "FocusWidget" => "Give keyboard focus to a widget immediately",
959        "SpawnPanelAt" => {
960            "Spawn a panel at any of the nine screen positions with a pixel offset and size"
961        }
962        "PanelText" => "Add a text label to a parent in a pixel rectangle with alignment",
963        "PanelBox" => "Add a solid colored rectangle to a parent at a pixel offset and size",
964        "PanelButtonAt" => "Add an interactive button to a parent at a pixel offset and size",
965        "SetPanelRect" => "Reposition and resize a UI node within its parent, in pixels",
966        "SetPanelColor" => "Set a UI node's background color as linear RGBA",
967        "SetPanelText" => "Replace a panel text label's content",
968        "SetPanelTextColor" => "Recolor a panel text label",
969        "SetPanelSelected" => "Toggle a button's selected highlight with an accent tint",
970        "SetPanelVisible" => "Show or hide a UI node and its children",
971        "PlayAnimation" => "Start playing the model's animation clip at the given index",
972        "PlayAnimationNamed" => "Play the animation clip with the given name",
973        "SetAnimationLooping" => "Set whether the entity's current animation repeats",
974        "SetAnimationSpeed" => "Set the playback speed of the entity's animation",
975        "BlendToAnimation" => "Cross-fade from the current clip to another over the given seconds",
976        "PauseAnimation" => "Pause the entity's animation at the current frame",
977        "ResumeAnimation" => "Resume a paused animation from where it left off",
978        "StopAnimation" => "Stop the entity's animation and reset it to the first frame",
979        "AnimationClips" => "Get the names of the entity's animation clips in index order",
980        "AddAnimationEvent" => "Add a named marker at a time on the clip at the given index",
981        "AddAnimationEventNamed" => "Add a named marker at a time on the clip with the given name",
982        "SetAnimationLayerWeight" => "Set the blend weight of an animation layer",
983        "ClearAnimationLayers" => "Remove every animation layer, leaving only the base animation",
984        "AimAt" => "Point a bone's forward axis at a world target, recomputed each frame",
985        "OrbitCamera" => "Add an orbit camera focused on a point at the given radius",
986        "FlyCamera" => "Add a free-flying camera at the given position",
987        "FixedCamera" => "Add a stationary camera at an eye looking at a target",
988        "LookAt" => "Repoint the active camera to look at a target from an eye",
989        "SetOrbitFocus" => "Move the orbit camera's focus point",
990        "SetOrbitView" => "Set the orbit camera's focus, distance, yaw, and pitch at once",
991        "SetOrbitZoom" => "Enable or disable scroll-wheel zoom on the orbit camera",
992        "SetOrbitModifier" => {
993            "Set the modifier key the orbit camera requires before drag orbits, or clear it"
994        }
995        "SetFieldOfView" => "Set the active perspective camera's vertical field of view in degrees",
996        "SetOrthographic" => "Switch the active camera to an orthographic projection",
997        "SetPerspective" => "Switch the active camera to a perspective projection",
998        "CameraPosition" => "Get the active camera's world position",
999        "CameraForward" => "Get the active camera's forward direction as a unit vector",
1000        "FirstPerson" => {
1001            "Add a walking first-person player with mouse look, WASD, sprint, and jump"
1002        }
1003        "DeltaTime" => "Get the seconds the previous frame took",
1004        "ElapsedSeconds" => "Get the seconds since the app started",
1005        "KeyDown" => "Check whether a key is held down",
1006        "KeyPressed" => "Check whether a key went down this frame",
1007        "MouseDown" => "Check whether a mouse button is held down",
1008        "MouseClicked" => "Check whether a mouse button went down this frame",
1009        "Wasd" => "Get the WASD movement direction on the ground plane",
1010        "PointerOverUi" => "Check whether the pointer is over a UI element this frame",
1011        "MouseScroll" => "Get the scroll wheel delta this frame",
1012        "Push" => "Apply an instant impulse to a dynamic entity",
1013        "SetVelocity" => "Set a dynamic body's linear velocity directly",
1014        "ApplyForce" => "Apply a continuous force to a dynamic entity for this step",
1015        "ApplyTorque" => "Apply a continuous torque to a dynamic entity for this step",
1016        "SetAngularVelocity" => "Set a dynamic body's angular velocity directly",
1017        "Velocity" => "Get the entity's current linear velocity if it has a body",
1018        "AngularVelocity" => "Get the entity's current angular velocity if it has a body",
1019        "MakeSensor" => "Turn the entity's collider into an overlap-reporting sensor",
1020        "OverlapSphere" => "Get every entity whose collider overlaps a sphere",
1021        "SetCollisionGroups" => "Set the entity collider's membership and filter collision masks",
1022        "SetFriction" => "Set an entity collider's friction",
1023        "SetRestitution" => "Set an entity collider's restitution or bounciness",
1024        "SetLinearDamping" => "Set a dynamic body's linear damping",
1025        "SetAngularDamping" => "Set a dynamic body's angular damping",
1026        "SetMass" => "Set a dynamic body's mass in kilograms",
1027        "SetGravityScale" => "Set a body's per-body gravity multiplier",
1028        "BakeNavmesh" => "Bake a navmesh over the current static geometry",
1029        "SpawnWalker" => "Spawn a navmesh agent that walks to ordered destinations",
1030        "WalkTo" => "Order an agent to walk to a destination along the navmesh",
1031        "SetWalkSpeed" => "Set an agent's walk speed in units per second",
1032        "StopWalking" => "Stop an agent where it stands and clear its path",
1033        "ClickedEntity" => "Get the entity clicked this frame, if any",
1034        "EntityUnderCursor" => "Get the entity currently under the cursor, if any",
1035        "CursorOnGround" => "Get where the cursor ray meets the ground plane, if it does",
1036        "SpawnWorldPanel" => "Spawn a flat panel in the 3d world facing the camera",
1037        "WorldPanelButton" => "Add a button to a world panel at local coordinates",
1038        "WorldPanelLabel" => "Add a text label to a world panel at local coordinates",
1039        "WorldButtonClicked" => "Check whether a world-panel button was clicked this frame",
1040        "PauseSound" => "Pause a playing sound, keeping its position",
1041        "ResumeSound" => "Resume a paused sound from where it stopped",
1042        "FadeVolume" => "Fade a playing sound to a target volume over the given seconds",
1043        "Crossfade" => "Crossfade between two sounds over the given seconds",
1044        "SetBusVolume" => "Set an audio bus's volume in decibels, fading over seconds",
1045        "DuckVoice" => "Duck the music and ambient buses under the voice bus",
1046        "DirectionalLight" => {
1047            "Add a directional light shining along a direction, like a second sun"
1048        }
1049        "AreaLight" => "Add a rectangular area light, a glowing panel facing a target",
1050        "SetLightShadows" => "Turn shadow casting on or off for a light entity",
1051        "EmitSparks" => "Emit a continuous fountain of bright sparks at the given position",
1052        "EmitFirework" => "Launch a firework shell that arcs and bursts on its own",
1053        "EmitParticles" => "Spawn a configurable continuous emitter at the given position",
1054        "SetAlphaBlend" => "Turn alpha blending on or off for the entity",
1055        "SetAlphaCutoff" => "Switch the entity to alpha cutout, discarding texels below the cutoff",
1056        "SetDoubleSided" => "Render both faces of the entity's triangles",
1057        "SetIor" => "Set the index of refraction for the entity's surface",
1058        "SetTransmission" => "Set how much light passes through the entity",
1059        "SetClearcoat" => "Add a clearcoat layer over the entity with its own factor and roughness",
1060        "SetAnisotropy" => "Set anisotropic reflection stretching highlights along a rotation",
1061        "SetUvTransform" => "Transform the entity's base color texture coordinates",
1062        "SetSheen" => "Add a soft retroreflective sheen tint to the entity",
1063        "SetIridescence" => "Add a thin-film iridescence to the entity",
1064        "SetSpecular" => "Set the entity's specular reflectance factor and tint",
1065        "SetNormalScale" => "Scale the strength of the entity's normal map",
1066        "SetOcclusionStrength" => {
1067            "Scale how strongly the entity's ambient occlusion map darkens it"
1068        }
1069        "SetEmissiveStrength" => "Set the entity's emissive strength on its own",
1070        "SetThickness" => "Set the volume thickness of a transmissive entity",
1071        "SetTextOutline" => "Set a 3d text or label entity's outline width and color",
1072        "SetMorphWeight" => "Set one morph target's weight on an entity by index",
1073        "SetWindowTitle" => "Set the OS window title",
1074        "LockCursor" => "Lock the cursor to the window for mouse-look, or release it",
1075        "RequestExit" => "Ask the app to exit at the end of the frame",
1076        "SetRenderLayer" => "Put an entity on a render layer cameras can selectively show",
1077        "SetCameraLayers" => "Set which render layers a camera sees as a bitmask",
1078        "ThirdPersonCamera" => "Add a third-person camera trailing a target at a distance",
1079        "SpawnCloth" => "Spawn a cloth grid hanging from a position, pinned along its top edge",
1080        "ResetCloth" => "Reset a cloth back to its spawned shape, clearing all motion",
1081        "SetWind" => "Set a global wind force on all cloth",
1082        "PauseCutscene" => "Pause the running cutscene timeline",
1083        "ResumeCutscene" => "Resume a paused cutscene",
1084        "StopCutscene" => "Stop the cutscene and clear it",
1085        "SeekCutscene" => "Jump the cutscene to the given time along its timeline",
1086        "SetCutsceneCamera" => "Set the camera the cutscene drives",
1087        "BindCutsceneActor" => "Bind a named cutscene track to an entity it animates",
1088        "SpawnCylinderBody" => "Spawn a dynamic cylinder physics body at the given position",
1089        "SpawnCapsuleBody" => "Spawn a dynamic capsule physics body at the given position",
1090        "SetControllerSpeed" => "Set a character controller's move speed in units per second",
1091        "SetControllerJump" => "Set a character controller's jump impulse",
1092        "IsGrounded" => "Check whether a character controller is grounded this frame",
1093        "EnableTerrain" => "Turn on procedural terrain seeded by the given value",
1094        "DisableTerrain" => "Turn off procedural terrain",
1095        "SetTerrainHeightRange" => "Set the terrain's minimum and maximum height in world units",
1096        "SetTerrainSnowHeight" => "Set the elevation above which terrain turns to snow",
1097        "EnableGrass" => "Turn on procedural grass over the terrain",
1098        "DisableGrass" => "Turn off procedural grass",
1099        "LoadTexture" => "Register a texture from encoded png or jpeg bytes",
1100        "LoadTextureLinear" => {
1101            "Register a linear-space texture for normal, metallic-roughness, or occlusion data"
1102        }
1103        "RegisterTexture" => "Register a texture from raw RGBA8 pixels",
1104        "ListMaterials" => "Get every registered material name",
1105        "GetMaterial" => "Get a registered material's properties by name",
1106        "RegisterMaterial" => "Register a named material in the shared registry",
1107        "UpdateMaterial" => "Replace the material registered under a name and reupload it",
1108        "SetMaterialVariant" => "Activate a material variant by name across the scene",
1109        "SpawnObjects" => "Spawn one object at each position, all sharing one material",
1110        "SpawnInstanced" => {
1111            "Spawn one entity rendering many copies of a shape in a single draw call"
1112        }
1113        "SpawnInstancedWithMaterial" => {
1114            "Spawn an instanced mesh drawing every transform with a named material"
1115        }
1116        "SetInstances" => {
1117            "Replace an instanced batch's transforms and reupload its instance buffer"
1118        }
1119        "SpawnClothSheet" => "Spawn a simulated cloth sheet hanging from a position, pinned on top",
1120        "BlendToAnimationNamed" => "Cross-fade to a named clip over the given seconds",
1121        "AddAnimationLayer" => "Play an extra clip on top of the base animation at a weight",
1122        "NameEntity" => "Register an entity under a name so it can be looked up",
1123        "Name" => "Get the entity's name, or a stable fallback when it has none",
1124        "SetEntityName" => "Rename the entity",
1125        "Children" => "Get the entity's direct children in id order",
1126        "Descendants" => "Get the entity's whole subtree below it",
1127        "Roots" => "Get every parentless transform entity in id order",
1128        "SceneTree" => "Get the whole transform hierarchy flattened depth-first",
1129        "MaterialOf" => "Get the entity's resolved material as json",
1130        "GetColor" => "Get the entity's base color as linear RGBA",
1131        "GetMetallicRoughness" => "Get the entity's metallic and roughness factors",
1132        "GetEmissive" => "Get the entity's emissive color and strength",
1133        "GetUnlit" => "Check whether the entity renders unlit",
1134        "GetTexture" => "Get the entity's base color texture name, if any",
1135        "DescribeEntity" => "Get a summary of the entity as json",
1136        "SetTextAlignment" => "Set the horizontal alignment of a 3d text or label entity",
1137        "SetMorphWeights" => "Set every morph weight at once in target order",
1138        "MorphWeight" => "Get one morph target's weight by index",
1139        "MorphTargetCount" => "Get the number of morph targets the entity has",
1140        "SetFog" => "Enable distance fog between a start and end, or disable it",
1141        "SetDepthOfField" => "Set depth of field focus and blur parameters",
1142        "Screenshot" => "Save a screenshot of the next rendered frame to a path as png",
1143        "SetShadingMode" => "Set the active camera's shading mode",
1144        "AnimatePosition" => "Glide the entity to a target position over the given seconds",
1145        "AnimateScale" => "Grow or shrink the entity to a target scale over the given seconds",
1146        "AnimateColor" => "Fade the entity's base color to a target over the given seconds",
1147        "ShakeCamera" => "Shake the camera for a duration at the given strength",
1148        "ReachTo" => "Bend a three-joint chain so the tip reaches a world target",
1149        "Bounds" => "Get an entity's world-space bounding box as json",
1150        "BoundsOf" => "Get the combined world-space bounding box of several entities",
1151        "FrameEntities" => "Frame the given entities in view, moving the camera to fit them",
1152        "SpawnParticleEmitter" => "Spawn a fully configured particle emitter",
1153        "SetEmitter" => "Replace a spawned emitter's configuration",
1154        "SpawnDecal" => "Project a texture onto the surface at a position facing a normal",
1155        "SaveScene" => "Capture the world to a self-contained compressed binary scene",
1156        "LoadScene" => "Spawn a saved scene into the world and return its root entities",
1157        "WindowSize" => "Get the window's inner size in physical pixels",
1158        "CursorLocked" => "Check whether the cursor is currently locked",
1159        "FramesPerSecond" => "Get the current frames per second",
1160        "FrameCount" => "Get the number of frames rendered since startup",
1161        "UptimeMilliseconds" => "Get the milliseconds since the app started",
1162        "BakeNavmeshWith" => "Bake the navmesh with an explicit Recast configuration",
1163        "Raycast" => "Cast a ray against all physics colliders and return the closest hit",
1164        "AttachFixed" => "Weld two bodies together rigidly",
1165        "AttachHinge" => "Hinge two bodies around an axis",
1166        "AttachSpring" => "Connect two bodies with a spring",
1167        "AttachRope" => "Tether two bodies with a maximum separation",
1168        "ControllerVelocity" => "Get a character controller's current velocity",
1169        "MoveCharacter" => "Move a character controller this frame with input and a jump flag",
1170        "RequestSurfacePick" => "Request a precise GPU surface pick at a screen position",
1171        "TakeSurfacePick" => "Take the result of a requested surface pick, if ready",
1172        "WorldButtonHovered" => "Check whether the pointer is over a world-panel button this frame",
1173        "LoadSound" => "Register a sound from encoded audio file bytes",
1174        "PlaySound" => "Play a loaded sound once and return its voice entity",
1175        "PlaySoundLooping" => "Play a loaded sound on a loop and return its voice entity",
1176        "PlaySoundAt" => "Play a loaded sound once at a world position, attenuating with distance",
1177        "SetVolume" => "Set a playing sound's volume",
1178        "StopSound" => "Stop a sound and free its voice",
1179        "SetPitch" => "Set a sound's pitch and speed multiplier",
1180        "SetSpatialDistance" => "Set the distance range over which a spatial sound fades",
1181        "PanelCheckbox" => "Add a labeled checkbox to a panel",
1182        "CheckboxValue" => "Get the checkbox's current on or off value",
1183        "PanelSlider" => "Add a slider from min to max starting at an initial value to a panel",
1184        "SliderValue" => "Get the slider's current value",
1185        "SetSliderValue" => "Set the slider's value",
1186        "PanelTextInput" => "Add a single-line text input with placeholder text to a panel",
1187        "TextInputChanged" => "Get the input's new contents if it changed this frame",
1188        "PanelDropdown" => "Add a dropdown of options with an initial selection to a panel",
1189        "DropdownSelected" => "Get the newly chosen index if the dropdown changed this frame",
1190        "PanelProgressBar" => "Add a progress bar filled to an initial value to a panel",
1191        "SetProgress" => "Set a progress bar's fill from 0 to 1",
1192        "PanelToggle" => "Add an on or off toggle to a panel",
1193        "ToggleValue" => "Get the toggle's current on or off value",
1194        "PanelRadio" => "Add a radio button to a panel as one option of a group",
1195        "RadioSelected" => "Get the selected option index of a radio group, if any",
1196        "PanelRangeSlider" => "Add a dual-handle range slider to a panel",
1197        "SetRange" => "Set both handles of a range slider",
1198        "PanelTabs" => "Add a tab bar of labels to a panel with an initial selection",
1199        "SetTab" => "Select a tab bar's active tab by index",
1200        "PanelCollapsing" => "Add a collapsing section to a panel and return its content container",
1201        "PanelColorPicker" => "Add a color picker to a panel starting at a linear RGBA color",
1202        "ColorValue" => "Get the color picker's current color as linear RGBA",
1203        "PanelTextArea" => "Add a multi-line text area with placeholder text to a panel",
1204        "PanelTextAreaWithValue" => {
1205            "Add a multi-line text area pre-filled with an initial value to a panel"
1206        }
1207        "SetTextArea" => "Replace a text area's contents",
1208        "PanelMultiSelect" => "Add a multi-select chip list of options to a panel",
1209        "SetMultiSelect" => "Set a multi-select's chosen option indices",
1210        "PanelDatePicker" => "Add a date picker starting at a given date to a panel",
1211        "SetDate" => "Set a date picker's value",
1212        "PanelMenu" => "Add a dropdown menu listing items to a panel",
1213        "PanelColorPickerHsv" => {
1214            "Add an HSV color picker starting at a linear RGBA color to a panel"
1215        }
1216        "PanelSplitter" => "Add a draggable splitter dividing a panel into two resizable panes",
1217        "PanelBreadcrumb" => "Add a breadcrumb trail of segments to a panel",
1218        "PanelVirtualList" => "Add a virtual list that recycles a pool of rows to a panel",
1219        "PanelTable" => "Add a simple table with column headers and widths to a panel",
1220        "PanelDataGrid" => "Add a sortable, selectable data grid to a panel",
1221        "SetDataGridRows" => "Set a data grid's total row count",
1222        "SetDataGridCell" => "Set the text of a single data grid cell",
1223        "DataGridSelectionChanged" => {
1224            "Check whether the data grid's row selection changed this frame"
1225        }
1226        "PanelCommandPalette" => "Add a searchable command palette overlay to a panel",
1227        "PanelPropertyGrid" => "Add a two-column label and value property grid to a panel",
1228        "PanelPropertyRow" => "Add a labeled row to a property grid and return its value cell",
1229        "PanelTreeView" => "Add a tree view to a panel",
1230        "TreeContent" => "Get the root container of a tree view for adding top-level nodes",
1231        "TreeNode" => "Add a node at a depth under a container in a tree view",
1232        "TreeNodeChildren" => "Get the container a node's children go into for nesting",
1233        "SetTreeNodeExpanded" => "Expand or collapse a tree node",
1234        "TreeViewSelected" => "Get the entities of the currently selected tree nodes",
1235        "PanelDragValue" => "Add a numeric drag value field that scrubs as you drag to a panel",
1236        "DragValue" => "Get the drag value's current value",
1237        "PanelSelectable" => "Add a selectable label to a panel",
1238        "PanelModal" => "Add a centered modal dialog to a panel and return its content container",
1239        "PanelSpinner" => "Add an animated loading spinner to a panel",
1240        "PanelSeparator" => "Add a thin horizontal divider line to a panel",
1241        "PanelHeading" => "Add a larger heading-styled line of text to a panel",
1242        "SaveSceneToFile" => "Serialize the scene and write it to a file path (native only)",
1243        "LoadSceneFromFile" => {
1244            "Read a scene file from a path and spawn it, replying its root entities (native only)"
1245        }
1246        _ => "",
1247    }
1248}
1249
1250macro_rules! commands {
1251    (
1252        $(
1253            $(#[$meta:meta])*
1254            $variant:ident { $( $field:ident : $field_type:ty [$role:ident] ),* $(,)? }
1255                => $func:path , $reply:ident ;
1256        )*
1257    ) => {
1258        /// One API call as data. Field names and types match the free function
1259        /// it mirrors. Positions, axes, and colors are plain arrays so the wire
1260        /// form is clean json rather than a math library's internal layout.
1261        #[derive(Serialize, Deserialize, Clone, Debug, enum2schema::Schema)]
1262        pub enum Command {
1263            $(
1264                $(#[$meta])*
1265                $variant { $( $field : $field_type ),* },
1266            )*
1267        }
1268
1269        impl Command {
1270            /// This command's variant name, the key the manifest and a binding use,
1271            /// read straight off the value with no serialization.
1272            pub fn name(&self) -> &'static str {
1273                match self {
1274                    $(
1275                        $(#[$meta])*
1276                        Command::$variant { .. } => stringify!($variant),
1277                    )*
1278                }
1279            }
1280        }
1281
1282        fn dispatch(
1283            world: &mut World,
1284            command: &Command,
1285            produced: &[Option<Entity>],
1286        ) -> CommandReply {
1287            match command {
1288                $(
1289                    $(#[$meta])*
1290                    Command::$variant { $( $field ),* } => {
1291                        $( bind_argument!($field, $role, produced, world); )*
1292                        wrap_reply!($reply, $func(world $(, $field)*))
1293                    }
1294                )*
1295            }
1296        }
1297
1298        /// The command surface as data, generated from the same registry as
1299        /// [`Command`] and its dispatch. cfg-gated commands appear only when
1300        /// their feature is compiled, so the manifest matches the surface a
1301        /// binding can actually reach.
1302        pub fn command_manifest() -> Vec<CommandSpec> {
1303            let mut specs = Vec::new();
1304            $(
1305                $(#[$meta])*
1306                specs.extend([CommandSpec {
1307                    name: stringify!($variant),
1308                    fields: vec![
1309                        $( FieldSpec {
1310                            name: stringify!($field),
1311                            type_name: stringify!($field_type),
1312                            role: stringify!($role),
1313                        } ),*
1314                    ],
1315                    reply: stringify!($reply),
1316                    description: command_description(stringify!($variant)),
1317                }]);
1318            )*
1319            specs
1320        }
1321
1322        /// Registers every command as a method on a rhai array, so a script
1323        /// writes `commands.spawn_cube([x, y, z])` instead of pushing the
1324        /// command's map shape by hand. The arguments are the command's fields in
1325        /// order, each taken as a dynamic value and serialized exactly like the
1326        /// map form, so the two are equivalent. Generated from the same registry
1327        /// as the command enum, so the script surface never drifts from it.
1328        #[cfg(feature = "scripting")]
1329        pub fn register_command_methods(engine: &mut rhai::Engine) {
1330            $(
1331                $(#[$meta])*
1332                engine.register_fn(
1333                    command_method_name(stringify!($variant)),
1334                    |commands: &mut rhai::Array $(, $field: rhai::Dynamic )*| {
1335                        commands.push(command_method_map(
1336                            stringify!($variant),
1337                            vec![ $( (stringify!($field), $field) ),* ],
1338                        ));
1339                    },
1340                );
1341            )*
1342        }
1343    };
1344}
1345
1346commands! {
1347    SpawnCube { position: [f32; 3] [vec3] } => crate::scene::spawn_cube, entity;
1348    SpawnSphere { position: [f32; 3] [vec3] } => crate::scene::spawn_sphere, entity;
1349    SpawnCylinder { position: [f32; 3] [vec3] } => crate::scene::spawn_cylinder, entity;
1350    SpawnCone { position: [f32; 3] [vec3] } => crate::scene::spawn_cone, entity;
1351    SpawnPlane { position: [f32; 3] [vec3] } => crate::scene::spawn_plane, entity;
1352    SpawnTorus { position: [f32; 3] [vec3] } => crate::scene::spawn_torus, entity;
1353    SpawnFloor { half_extent: f32 [copy] } => crate::scene::spawn_floor, entity;
1354    SpawnGroup { position: [f32; 3] [vec3] } => crate::scene::spawn_group, entity;
1355    SpawnModel { glb: Vec<u8> [bytes], position: [f32; 3] [vec3] } => crate::scene::spawn_model, entity;
1356    SpawnObject { shape: Shape [copy], position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy], body: Body [copy] } => spawn_object_command, entity;
1357
1358    SetColor { entity: Ref [entity], color: [f32; 4] [copy] } => crate::appearance::set_color, none;
1359    SetMetallicRoughness { entity: Ref [entity], metallic: f32 [copy], roughness: f32 [copy] } => crate::appearance::set_metallic_roughness, none;
1360    SetEmissive { entity: Ref [entity], color: [f32; 3] [copy], strength: f32 [copy] } => crate::appearance::set_emissive, none;
1361    SetUnlit { entity: Ref [entity], unlit: bool [copy] } => crate::appearance::set_unlit, none;
1362    SetTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_texture, none;
1363    SetTextureTiling { entity: Ref [entity], repeats: f32 [copy] } => crate::appearance::set_texture_tiling, none;
1364    SetNormalTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_normal_texture, none;
1365    SetMetallicRoughnessTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_metallic_roughness_texture, none;
1366    SetEmissiveTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_emissive_texture, none;
1367    SetOcclusionTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_occlusion_texture, none;
1368
1369    SetPosition { entity: Ref [entity], position: [f32; 3] [vec3] } => crate::placement::set_position, none;
1370    SetScale { entity: Ref [entity], scale: [f32; 3] [vec3] } => crate::placement::set_scale, none;
1371    SetRotation { entity: Ref [entity], axis: [f32; 3] [vec3], radians: f32 [copy] } => crate::placement::set_rotation, none;
1372    Rotate { entity: Ref [entity], axis: [f32; 3] [vec3], radians: f32 [copy] } => crate::placement::rotate, none;
1373    Position { entity: Ref [entity] } => crate::placement::position, vector;
1374
1375    SetParent { child: Ref [entity], parent: Option<Ref> [opt_entity] } => crate::scene::set_parent, none;
1376    SetVisible { entity: Ref [entity], visible: bool [copy] } => crate::scene::set_visible, none;
1377    Despawn { entity: Ref [entity] } => crate::scene::despawn, none;
1378
1379    Tag { entity: Ref [entity], label: String [text] } => crate::groups::tag, none;
1380    Untag { entity: Ref [entity], label: String [text] } => crate::groups::untag, none;
1381    HasTag { entity: Ref [entity], label: String [text] } => crate::groups::has_tag, bool;
1382    QueryTagged { label: String [text] } => crate::groups::tagged, entities;
1383
1384    PointLight { position: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::point_light, entity;
1385    SpotLight { position: [f32; 3] [vec3], target: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::spot_light, entity;
1386    SetSun { color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::set_sun, none;
1387
1388    SetBackground { background: crate::environment::Background [owned] } => crate::environment::set_background, none;
1389    ShowGrid { enabled: bool [copy] } => crate::environment::show_grid, none;
1390    SetAmbient { color: [f32; 4] [copy] } => crate::environment::set_ambient, none;
1391    SetBloom { enabled: bool [copy] } => crate::environment::set_bloom, none;
1392    SetBloomIntensity { intensity: f32 [copy] } => crate::environment::set_bloom_intensity, none;
1393    SetSsao { enabled: bool [copy] } => crate::environment::set_ssao, none;
1394    SetSsr { enabled: bool [copy] } => crate::environment::set_ssr, none;
1395    SetSsgi { enabled: bool [copy] } => crate::environment::set_ssgi, none;
1396    SetFxaa { enabled: bool [copy] } => crate::environment::set_fxaa, none;
1397    SetExposure { exposure: f32 [copy] } => crate::environment::set_exposure, none;
1398    SetColorGrading { saturation: f32 [copy], contrast: f32 [copy], brightness: f32 [copy] } => crate::environment::set_color_grading, none;
1399    SetTimeOfDay { hour: f32 [copy] } => crate::environment::set_time_of_day, none;
1400    SetTitle { title: String [text] } => crate::environment::set_title, none;
1401
1402    EmitFire { position: [f32; 3] [vec3] } => crate::effects::emit_fire, entity;
1403    EmitSmoke { position: [f32; 3] [vec3] } => crate::effects::emit_smoke, entity;
1404    EmitBurst { position: [f32; 3] [vec3], color: [f32; 4] [copy], count: u32 [copy] } => crate::effects::emit_burst, entity;
1405
1406    DrawCube { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cube, none;
1407    DrawSphere { position: [f32; 3] [vec3], radius: f32 [copy], color: [f32; 4] [copy] } => crate::draw::draw_sphere, none;
1408    DrawCylinder { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cylinder, none;
1409    DrawCone { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cone, none;
1410    DrawTorus { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_torus, none;
1411    DrawLine { start: [f32; 3] [vec3], end: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_line, none;
1412    DrawText3d { text: String [text], position: [f32; 3] [vec3] } => crate::draw::draw_text_3d, none;
1413
1414    SpawnLabel { text: String [text], position: [f32; 3] [vec3] } => crate::text::spawn_label, entity;
1415    SpawnText { text: String [text], anchor: crate::text::ScreenAnchor [copy] } => crate::text::spawn_text, entity;
1416    SetText { entity: Ref [entity], text: String [text] } => crate::text::set_text, none;
1417    SetTextColor { entity: Ref [entity], color: [f32; 4] [copy] } => crate::text::set_text_color, none;
1418    SetTextSize { entity: Ref [entity], size: f32 [copy] } => crate::text::set_text_size, none;
1419
1420    SpawnPanel { anchor: crate::text::ScreenAnchor [copy], width: f32 [copy], height: f32 [copy] } => crate::ui::spawn_panel, entity;
1421    PanelLabel { panel: Ref [entity], text: String [text] } => crate::ui::panel_label, entity;
1422    PanelButton { panel: Ref [entity], text: String [text] } => crate::ui::panel_button, entity;
1423    ButtonClicked { button: Ref [entity] } => crate::ui::button_clicked, bool;
1424    ButtonHovered { button: Ref [entity] } => crate::ui::button_hovered, bool;
1425    DespawnPanel { panel: Ref [entity] } => crate::ui::despawn_panel, none;
1426    PanelRow { panel: Ref [entity], height: f32 [copy] } => crate::ui::panel_row, entity;
1427    PanelGrid { panel: Ref [entity], columns: usize [copy], row_height: f32 [copy], height: f32 [copy] } => crate::ui::panel_grid, entity;
1428    PanelScroll { panel: Ref [entity], height: f32 [copy] } => crate::ui::panel_scroll, entity;
1429    SetScrollOffset { scroll_area: Ref [entity], offset: f32 [copy] } => crate::ui::set_scroll_offset, none;
1430    SetFocusOrder { entity: Ref [entity], order: i32 [copy] } => crate::ui::set_focus_order, none;
1431    FocusWidget { entity: Ref [entity] } => crate::ui::focus_widget, none;
1432    SpawnPanelAt { anchor: crate::text::ScreenAnchor [copy], offset: [f32; 2] [vec2], size: [f32; 2] [vec2], color: [f32; 4] [copy] } => crate::ui::spawn_panel_at, entity;
1433    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;
1434    PanelBox { parent: Ref [entity], offset: [f32; 2] [vec2], size: [f32; 2] [vec2], color: [f32; 4] [copy] } => crate::ui::panel_box, entity;
1435    PanelButtonAt { parent: Ref [entity], label: String [text], offset: [f32; 2] [vec2], size: [f32; 2] [vec2] } => crate::ui::panel_button_at, entity;
1436    SetPanelRect { node: Ref [entity], offset: [f32; 2] [vec2], size: [f32; 2] [vec2] } => crate::ui::set_panel_rect, none;
1437    SetPanelColor { node: Ref [entity], color: [f32; 4] [copy] } => crate::ui::set_panel_color, none;
1438    SetPanelText { label: Ref [entity], text: String [text] } => crate::ui::set_panel_text, none;
1439    SetPanelTextColor { label: Ref [entity], color: [f32; 4] [copy] } => crate::ui::set_panel_text_color, none;
1440    SetPanelSelected { button: Ref [entity], selected: bool [copy], accent: [f32; 4] [copy] } => crate::ui::set_panel_selected, none;
1441    SetPanelVisible { node: Ref [entity], visible: bool [copy] } => crate::ui::set_panel_visible, none;
1442
1443    PlayAnimation { entity: Ref [entity], clip: usize [copy] } => crate::scene::play_animation, none;
1444    PlayAnimationNamed { entity: Ref [entity], name: String [text] } => crate::scene::play_animation_named, bool;
1445    SetAnimationLooping { entity: Ref [entity], looping: bool [copy] } => crate::scene::set_animation_looping, none;
1446    SetAnimationSpeed { entity: Ref [entity], speed: f32 [copy] } => crate::scene::set_animation_speed, none;
1447    BlendToAnimation { entity: Ref [entity], clip: usize [copy], seconds: f32 [copy] } => crate::scene::blend_to_animation, none;
1448    PauseAnimation { entity: Ref [entity] } => crate::scene::pause_animation, none;
1449    ResumeAnimation { entity: Ref [entity] } => crate::scene::resume_animation, none;
1450    StopAnimation { entity: Ref [entity] } => crate::scene::stop_animation, none;
1451    AnimationClips { entity: Ref [entity] } => crate::scene::animation_clips, strings;
1452    AddAnimationEvent { entity: Ref [entity], clip_index: usize [copy], time: f32 [copy], name: String [text] } => crate::scene::add_animation_event, bool;
1453    AddAnimationEventNamed { entity: Ref [entity], clip_name: String [text], time: f32 [copy], name: String [text] } => crate::scene::add_animation_event_named, bool;
1454    SetAnimationLayerWeight { entity: Ref [entity], layer_index: usize [copy], weight: f32 [copy] } => crate::scene::set_animation_layer_weight, none;
1455    ClearAnimationLayers { entity: Ref [entity] } => crate::scene::clear_animation_layers, none;
1456    AimAt { bone: Ref [entity], target: [f32; 3] [vec3], forward: [f32; 3] [vec3] } => crate::animate::aim_at, none;
1457
1458    OrbitCamera { focus: [f32; 3] [vec3], radius: f32 [copy] } => crate::camera::orbit_camera, entity;
1459    FlyCamera { position: [f32; 3] [vec3] } => crate::camera::fly_camera, entity;
1460    FixedCamera { eye: [f32; 3] [vec3], target: [f32; 3] [vec3] } => crate::camera::fixed_camera, entity;
1461    LookAt { eye: [f32; 3] [vec3], target: [f32; 3] [vec3] } => crate::camera::look_at, none;
1462    SetOrbitFocus { focus: [f32; 3] [vec3] } => crate::camera::set_orbit_focus, none;
1463    SetOrbitView { focus: [f32; 3] [vec3], radius: f32 [copy], yaw: f32 [copy], pitch: f32 [copy] } => crate::camera::set_orbit_view, none;
1464    SetOrbitZoom { enabled: bool [copy] } => crate::camera::set_orbit_zoom, none;
1465    SetOrbitModifier { modifier: String [text] } => crate::camera::set_orbit_modifier, none;
1466    SetFieldOfView { degrees: f32 [copy] } => crate::camera::set_field_of_view, none;
1467    SetOrthographic { half_height: f32 [copy] } => crate::camera::set_orthographic, none;
1468    SetPerspective { degrees: f32 [copy] } => crate::camera::set_perspective, none;
1469    CameraPosition {} => crate::camera::camera_position, vector;
1470    CameraForward {} => crate::camera::camera_forward, vector;
1471    #[cfg(feature = "physics")]
1472    FirstPerson { position: [f32; 3] [vec3] } => crate::camera::first_person, entity;
1473
1474    DeltaTime {} => crate::input::delta_time, float;
1475    ElapsedSeconds {} => crate::input::elapsed_seconds, float;
1476    KeyDown { key: String [text] } => key_down_command, bool;
1477    KeyPressed { key: String [text] } => key_pressed_command, bool;
1478    MouseDown { button: u8 [copy] } => mouse_down_command, bool;
1479    MouseClicked { button: u8 [copy] } => mouse_clicked_command, bool;
1480    Wasd {} => crate::input::wasd, vector;
1481    PointerOverUi {} => crate::input::pointer_over_ui, bool;
1482    MouseScroll {} => crate::input::mouse_scroll, float;
1483
1484    #[cfg(feature = "physics")]
1485    Push { entity: Ref [entity], impulse: [f32; 3] [vec3] } => crate::physics::push, none;
1486    #[cfg(feature = "physics")]
1487    SetVelocity { entity: Ref [entity], velocity: [f32; 3] [vec3] } => crate::physics::set_velocity, none;
1488    #[cfg(feature = "physics")]
1489    ApplyForce { entity: Ref [entity], force: [f32; 3] [vec3] } => crate::physics::apply_force, none;
1490    #[cfg(feature = "physics")]
1491    ApplyTorque { entity: Ref [entity], torque: [f32; 3] [vec3] } => crate::physics::apply_torque, none;
1492    #[cfg(feature = "physics")]
1493    SetAngularVelocity { entity: Ref [entity], velocity: [f32; 3] [vec3] } => crate::physics::set_angular_velocity, none;
1494    #[cfg(feature = "physics")]
1495    Velocity { entity: Ref [entity] } => crate::physics::velocity, opt_vector;
1496    #[cfg(feature = "physics")]
1497    AngularVelocity { entity: Ref [entity] } => crate::physics::angular_velocity, opt_vector;
1498    #[cfg(feature = "physics")]
1499    MakeSensor { entity: Ref [entity] } => crate::physics::make_sensor, none;
1500    #[cfg(feature = "physics")]
1501    OverlapSphere { center: [f32; 3] [vec3], radius: f32 [copy] } => crate::physics::overlap_sphere, entities;
1502    #[cfg(feature = "physics")]
1503    SetCollisionGroups { entity: Ref [entity], membership: u32 [copy], filter: u32 [copy] } => crate::physics::set_collision_groups, none;
1504    #[cfg(feature = "physics")]
1505    SetFriction { entity: Ref [entity], friction: f32 [copy] } => crate::physics::set_friction, none;
1506    #[cfg(feature = "physics")]
1507    SetRestitution { entity: Ref [entity], restitution: f32 [copy] } => crate::physics::set_restitution, none;
1508    #[cfg(feature = "physics")]
1509    SetLinearDamping { entity: Ref [entity], damping: f32 [copy] } => crate::physics::set_linear_damping, none;
1510    #[cfg(feature = "physics")]
1511    SetAngularDamping { entity: Ref [entity], damping: f32 [copy] } => crate::physics::set_angular_damping, none;
1512    #[cfg(feature = "physics")]
1513    SetMass { entity: Ref [entity], mass: f32 [copy] } => crate::physics::set_mass, none;
1514    #[cfg(feature = "physics")]
1515    SetGravityScale { entity: Ref [entity], scale: f32 [copy] } => crate::physics::set_gravity_scale, none;
1516
1517    #[cfg(feature = "navmesh")]
1518    BakeNavmesh {} => crate::navigation::bake_navmesh, none;
1519    #[cfg(feature = "navmesh")]
1520    SpawnWalker { position: [f32; 3] [vec3] } => crate::navigation::spawn_walker, entity;
1521    #[cfg(feature = "navmesh")]
1522    WalkTo { agent: Ref [entity], destination: [f32; 3] [vec3] } => crate::navigation::walk_to, none;
1523    #[cfg(feature = "navmesh")]
1524    SetWalkSpeed { agent: Ref [entity], speed: f32 [copy] } => crate::navigation::set_walk_speed, none;
1525    #[cfg(feature = "navmesh")]
1526    StopWalking { agent: Ref [entity] } => crate::navigation::stop_walking, none;
1527
1528    #[cfg(feature = "picking")]
1529    ClickedEntity {} => crate::picking::clicked_entity, opt_entity;
1530    #[cfg(feature = "picking")]
1531    EntityUnderCursor {} => crate::picking::entity_under_cursor, opt_entity;
1532    #[cfg(feature = "picking")]
1533    CursorOnGround {} => crate::picking::cursor_on_ground, opt_vector;
1534    #[cfg(feature = "picking")]
1535    SpawnWorldPanel { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy], color: [f32; 4] [copy] } => crate::world_ui::spawn_world_panel, entity;
1536    #[cfg(feature = "picking")]
1537    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;
1538    #[cfg(feature = "picking")]
1539    WorldPanelLabel { panel: Ref [entity], text: String [text], x: f32 [copy], y: f32 [copy] } => crate::world_ui::world_panel_label, entity;
1540    #[cfg(feature = "picking")]
1541    WorldButtonClicked { button: Ref [entity] } => crate::world_ui::world_button_clicked, bool;
1542
1543    #[cfg(feature = "audio")]
1544    PauseSound { entity: Ref [entity] } => crate::audio::pause_sound, none;
1545    #[cfg(feature = "audio")]
1546    ResumeSound { entity: Ref [entity] } => crate::audio::resume_sound, none;
1547    #[cfg(feature = "audio")]
1548    FadeVolume { entity: Ref [entity], volume: f32 [copy], seconds: f32 [copy] } => crate::audio::fade_volume, none;
1549    #[cfg(feature = "audio")]
1550    Crossfade { fade_out: Ref [entity], fade_in: Ref [entity], volume: f32 [copy], seconds: f32 [copy] } => crate::audio::crossfade, none;
1551    #[cfg(feature = "audio")]
1552    SetBusVolume { bus: nightshade::prelude::AudioBus [copy], decibels: f32 [copy], fade_seconds: f32 [copy] } => crate::audio::set_bus_volume, none;
1553    #[cfg(feature = "audio")]
1554    DuckVoice { amount: f32 [copy], fade_seconds: f32 [copy] } => crate::audio::duck_voice, none;
1555
1556    DirectionalLight { direction: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::directional_light, entity;
1557    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;
1558    SetLightShadows { light: Ref [entity], enabled: bool [copy] } => crate::lighting::set_light_shadows, none;
1559
1560    EmitSparks { position: [f32; 3] [vec3] } => crate::effects::emit_sparks, entity;
1561    EmitFirework { position: [f32; 3] [vec3], velocity: [f32; 3] [vec3] } => crate::effects::emit_firework, entity;
1562    EmitParticles { position: [f32; 3] [vec3], rate: f32 [copy], lifetime: f32 [copy], size: f32 [copy], gravity: [f32; 3] [vec3] } => crate::effects::emit_particles, entity;
1563
1564    SetAlphaBlend { entity: Ref [entity], enabled: bool [copy] } => crate::appearance::set_alpha_blend, none;
1565    SetAlphaCutoff { entity: Ref [entity], cutoff: f32 [copy] } => crate::appearance::set_alpha_cutoff, none;
1566    SetDoubleSided { entity: Ref [entity], double_sided: bool [copy] } => crate::appearance::set_double_sided, none;
1567    SetIor { entity: Ref [entity], ior: f32 [copy] } => crate::appearance::set_ior, none;
1568    SetTransmission { entity: Ref [entity], factor: f32 [copy] } => crate::appearance::set_transmission, none;
1569    SetClearcoat { entity: Ref [entity], factor: f32 [copy], roughness: f32 [copy] } => crate::appearance::set_clearcoat, none;
1570    SetAnisotropy { entity: Ref [entity], strength: f32 [copy], rotation: f32 [copy] } => crate::appearance::set_anisotropy, none;
1571    SetUvTransform { entity: Ref [entity], offset: [f32; 2] [copy], scale: [f32; 2] [copy], rotation: f32 [copy] } => crate::appearance::set_uv_transform, none;
1572    SetSheen { entity: Ref [entity], color: [f32; 3] [copy], roughness: f32 [copy] } => crate::appearance::set_sheen, none;
1573    SetIridescence { entity: Ref [entity], factor: f32 [copy], ior: f32 [copy] } => crate::appearance::set_iridescence, none;
1574    SetSpecular { entity: Ref [entity], factor: f32 [copy], color: [f32; 3] [copy] } => crate::appearance::set_specular, none;
1575    SetNormalScale { entity: Ref [entity], scale: f32 [copy] } => crate::appearance::set_normal_scale, none;
1576    SetOcclusionStrength { entity: Ref [entity], strength: f32 [copy] } => crate::appearance::set_occlusion_strength, none;
1577    SetEmissiveStrength { entity: Ref [entity], strength: f32 [copy] } => crate::appearance::set_emissive_strength, none;
1578    SetThickness { entity: Ref [entity], thickness: f32 [copy] } => crate::appearance::set_thickness, none;
1579
1580    SetTextOutline { entity: Ref [entity], width: f32 [copy], color: [f32; 4] [copy] } => crate::text::set_text_outline, none;
1581
1582    SetMorphWeight { entity: Ref [entity], index: u32 [copy], weight: f32 [copy] } => crate::morph::set_morph_weight, none;
1583
1584    SetWindowTitle { title: String [text] } => crate::window::set_window_title, none;
1585    LockCursor { locked: bool [copy] } => crate::window::lock_cursor, none;
1586    RequestExit {} => crate::window::request_exit, none;
1587
1588    SetRenderLayer { entity: Ref [entity], layer: u32 [copy] } => crate::render::set_render_layer, none;
1589    SetCameraLayers { camera: Ref [entity], mask: u32 [copy] } => crate::render::set_camera_layers, none;
1590
1591    ThirdPersonCamera { target: Ref [entity], distance: f32 [copy] } => crate::camera::third_person_camera, entity;
1592
1593    SpawnCloth { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy], columns: u32 [copy], rows: u32 [copy] } => crate::cloth::spawn_cloth, entity;
1594    ResetCloth { entity: Ref [entity] } => crate::cloth::reset_cloth, none;
1595    SetWind { direction: [f32; 3] [vec3], strength: f32 [copy] } => crate::cloth::set_wind, none;
1596
1597    PauseCutscene {} => crate::cutscene::pause_cutscene, none;
1598    ResumeCutscene {} => crate::cutscene::resume_cutscene, none;
1599    StopCutscene {} => crate::cutscene::stop_cutscene, none;
1600    SeekCutscene { seconds: f32 [copy] } => crate::cutscene::seek_cutscene, none;
1601    SetCutsceneCamera { camera: Ref [entity] } => crate::cutscene::set_cutscene_camera, none;
1602    BindCutsceneActor { name: String [text], entity: Ref [entity] } => crate::cutscene::bind_cutscene_actor, none;
1603
1604    #[cfg(feature = "physics")]
1605    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;
1606    #[cfg(feature = "physics")]
1607    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;
1608    #[cfg(feature = "physics")]
1609    SetControllerSpeed { entity: Ref [entity], speed: f32 [copy] } => crate::character::set_controller_speed, none;
1610    #[cfg(feature = "physics")]
1611    SetControllerJump { entity: Ref [entity], impulse: f32 [copy] } => crate::character::set_controller_jump, none;
1612    #[cfg(feature = "physics")]
1613    IsGrounded { entity: Ref [entity] } => crate::character::is_grounded, bool;
1614
1615    #[cfg(feature = "terrain")]
1616    EnableTerrain { seed: u32 [copy] } => crate::terrain::enable_terrain, none;
1617    #[cfg(feature = "terrain")]
1618    DisableTerrain {} => crate::terrain::disable_terrain, none;
1619    #[cfg(feature = "terrain")]
1620    SetTerrainHeightRange { min: f32 [copy], max: f32 [copy] } => crate::terrain::set_terrain_height_range, none;
1621    #[cfg(feature = "terrain")]
1622    SetTerrainSnowHeight { height: f32 [copy] } => crate::terrain::set_terrain_snow_height, none;
1623    #[cfg(feature = "grass")]
1624    EnableGrass {} => crate::terrain::enable_grass, none;
1625    #[cfg(feature = "grass")]
1626    DisableGrass {} => crate::terrain::disable_grass, none;
1627
1628    LoadTexture { name: String [text], image_bytes: Vec<u8> [bytes] } => crate::appearance::load_texture, none;
1629    LoadTextureLinear { name: String [text], image_bytes: Vec<u8> [bytes] } => crate::appearance::load_texture_linear, none;
1630    RegisterTexture { name: String [text], width: u32 [copy], height: u32 [copy], rgba: Vec<u8> [bytes] } => crate::appearance::register_texture, none;
1631
1632    ListMaterials {} => crate::materials::list_materials, json;
1633    GetMaterial { name: String [text] } => crate::materials::get_material, json;
1634    RegisterMaterial { name: String [text], base_color: [f32; 4] [copy], metallic: f32 [copy], roughness: f32 [copy], emissive: [f32; 3] [copy] } => register_material_command, text;
1635    UpdateMaterial { name: String [text], base_color: [f32; 4] [copy], metallic: f32 [copy], roughness: f32 [copy], emissive: [f32; 3] [copy] } => update_material_command, none;
1636    SetMaterialVariant { variant: String [text] } => set_material_variant_command, int;
1637
1638    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;
1639    SpawnInstanced { shape: Shape [copy], transforms: Vec<InstanceWire> [transforms], color: [f32; 4] [copy] } => crate::scene::spawn_instanced, entity;
1640    SpawnInstancedWithMaterial { shape: Shape [copy], transforms: Vec<InstanceWire> [transforms], material: String [text] } => crate::scene::spawn_instanced_with_material, entity;
1641    SetInstances { batch: Ref [entity], transforms: Vec<InstanceWire> [transforms] } => crate::scene::set_instances, none;
1642    SpawnClothSheet { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy] } => crate::scene::spawn_cloth_sheet, entity;
1643    BlendToAnimationNamed { entity: Ref [entity], name: String [text], seconds: f32 [copy] } => crate::scene::blend_to_animation_named, bool;
1644    AddAnimationLayer { entity: Ref [entity], clip_index: usize [copy], weight: f32 [copy] } => crate::scene::add_animation_layer, none;
1645    NameEntity { name: String [text], entity: Ref [entity] } => crate::scene::name_entity, none;
1646
1647    Name { entity: Ref [entity] } => crate::hierarchy::name, text;
1648    SetEntityName { entity: Ref [entity], name: String [text] } => crate::hierarchy::set_name, none;
1649    Children { entity: Ref [entity] } => crate::hierarchy::children, entities;
1650    Descendants { entity: Ref [entity] } => crate::hierarchy::descendants, entities;
1651    Roots {} => crate::hierarchy::roots, entities;
1652    SceneTree {} => crate::hierarchy::scene_tree, json;
1653
1654    MaterialOf { entity: Ref [entity] } => crate::inspect::material_of, json;
1655    GetColor { entity: Ref [entity] } => crate::inspect::get_color, json;
1656    GetMetallicRoughness { entity: Ref [entity] } => crate::inspect::get_metallic_roughness, json;
1657    GetEmissive { entity: Ref [entity] } => crate::inspect::get_emissive, json;
1658    GetUnlit { entity: Ref [entity] } => crate::inspect::get_unlit, json;
1659    GetTexture { entity: Ref [entity] } => crate::inspect::get_texture, json;
1660    DescribeEntity { entity: Ref [entity] } => crate::inspect::describe_entity, json;
1661
1662    SetTextAlignment { entity: Ref [entity], alignment: TextAlignment [copy] } => crate::text::set_text_alignment, none;
1663
1664    SetMorphWeights { entity: Ref [entity], weights: Vec<f32> [floats] } => crate::morph::set_morph_weights, none;
1665    MorphWeight { entity: Ref [entity], index: u32 [copy] } => crate::morph::morph_weight, float;
1666    MorphTargetCount { entity: Ref [entity] } => crate::morph::morph_target_count, int;
1667
1668    SetFog { enabled: bool [copy], color: [f32; 3] [copy], start: f32 [copy], end: f32 [copy] } => set_fog_command, none;
1669    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;
1670    Screenshot { path: String [text] } => screenshot_command, none;
1671
1672    SetShadingMode { mode: String [text] } => set_shading_mode_command, none;
1673
1674    AnimatePosition { entity: Ref [entity], to: [f32; 3] [vec3], seconds: f32 [copy], easing: String [text] } => animate_position_command, none;
1675    AnimateScale { entity: Ref [entity], to: [f32; 3] [vec3], seconds: f32 [copy], easing: String [text] } => animate_scale_command, none;
1676    AnimateColor { entity: Ref [entity], to: [f32; 4] [copy], seconds: f32 [copy], easing: String [text] } => animate_color_command, none;
1677    ShakeCamera { strength: f32 [copy], seconds: f32 [copy] } => crate::animate::shake_camera, none;
1678    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;
1679
1680    Bounds { entity: Ref [entity] } => crate::bounds::bounds, json;
1681    BoundsOf { entities: Vec<Ref> [refs] } => crate::bounds::bounds_of, json;
1682    FrameEntities { entities: Vec<Ref> [refs] } => crate::bounds::frame_entities, none;
1683
1684    SpawnParticleEmitter { emitter: EmitterWire [owned] } => spawn_particle_emitter_command, entity;
1685    SetEmitter { emitter_entity: Ref [entity], emitter: EmitterWire [owned] } => set_emitter_command, none;
1686
1687    SpawnDecal { texture: String [text], position: [f32; 3] [vec3], normal: [f32; 3] [vec3], size: f32 [copy] } => crate::decals::spawn_decal, entity;
1688
1689    SaveScene { name: String [text] } => save_scene_command, bytes;
1690    LoadScene { bytes: Vec<u8> [bytes] } => load_scene_command, entities;
1691
1692    WindowSize {} => crate::window::window_size, json;
1693    CursorLocked {} => crate::window::cursor_locked, bool;
1694    FramesPerSecond {} => crate::window::frames_per_second, float;
1695    FrameCount {} => crate::window::frame_count, int;
1696    UptimeMilliseconds {} => crate::window::uptime_milliseconds, int;
1697
1698    #[cfg(feature = "navmesh")]
1699    BakeNavmeshWith { config: RecastConfigWire [owned] } => bake_navmesh_with_command, none;
1700
1701    #[cfg(feature = "physics")]
1702    Raycast { origin: [f32; 3] [vec3], direction: [f32; 3] [vec3], max_distance: f32 [copy] } => raycast_command, json;
1703    #[cfg(feature = "physics")]
1704    AttachFixed { parent: Ref [entity], child: Ref [entity] } => attach_fixed_command, bool;
1705    #[cfg(feature = "physics")]
1706    AttachHinge { parent: Ref [entity], child: Ref [entity], axis: String [text] } => attach_hinge_command, bool;
1707    #[cfg(feature = "physics")]
1708    AttachSpring { parent: Ref [entity], child: Ref [entity], rest_length: f32 [copy], stiffness: f32 [copy], damping: f32 [copy] } => attach_spring_command, bool;
1709    #[cfg(feature = "physics")]
1710    AttachRope { parent: Ref [entity], child: Ref [entity], max_distance: f32 [copy] } => attach_rope_command, bool;
1711    #[cfg(feature = "physics")]
1712    ControllerVelocity { entity: Ref [entity] } => crate::character::controller_velocity, vector;
1713    #[cfg(feature = "physics")]
1714    MoveCharacter { entity: Ref [entity], movement: [f32; 2] [vec2], jump: bool [copy] } => crate::character::move_character, none;
1715
1716    #[cfg(feature = "picking")]
1717    RequestSurfacePick { screen_pos: [f32; 2] [vec2] } => crate::picking::request_surface_pick, none;
1718    #[cfg(feature = "picking")]
1719    TakeSurfacePick {} => take_surface_pick_command, json;
1720    #[cfg(feature = "picking")]
1721    WorldButtonHovered { button: Ref [entity] } => crate::world_ui::world_button_hovered, bool;
1722
1723    #[cfg(feature = "audio")]
1724    LoadSound { name: String [text], bytes: Vec<u8> [bytes] } => crate::audio::load_sound, none;
1725    #[cfg(feature = "audio")]
1726    PlaySound { name: String [text] } => crate::audio::play_sound, entity;
1727    #[cfg(feature = "audio")]
1728    PlaySoundLooping { name: String [text] } => crate::audio::play_sound_looping, entity;
1729    #[cfg(feature = "audio")]
1730    PlaySoundAt { name: String [text], position: [f32; 3] [vec3] } => crate::audio::play_sound_at, entity;
1731    #[cfg(feature = "audio")]
1732    SetVolume { entity: Ref [entity], volume: f32 [copy] } => crate::audio::set_volume, none;
1733    #[cfg(feature = "audio")]
1734    StopSound { entity: Ref [entity] } => crate::audio::stop_sound, none;
1735    #[cfg(feature = "audio")]
1736    SetPitch { entity: Ref [entity], rate: f32 [copy] } => crate::audio::set_pitch, none;
1737    #[cfg(feature = "audio")]
1738    SetSpatialDistance { entity: Ref [entity], min: f32 [copy], max: f32 [copy] } => crate::audio::set_spatial_distance, none;
1739
1740    PanelCheckbox { panel: Ref [entity], label: String [text], initial: bool [copy] } => crate::ui::panel_checkbox, entity;
1741    CheckboxValue { checkbox: Ref [entity] } => crate::ui::checkbox_value, bool;
1742    PanelSlider { panel: Ref [entity], min: f32 [copy], max: f32 [copy], initial: f32 [copy] } => crate::ui::panel_slider, entity;
1743    SliderValue { slider: Ref [entity] } => crate::ui::slider_value, float;
1744    SetSliderValue { slider: Ref [entity], value: f32 [copy] } => crate::ui::set_slider_value, none;
1745    PanelTextInput { panel: Ref [entity], placeholder: String [text] } => crate::ui::panel_text_input, entity;
1746    TextInputChanged { input: Ref [entity] } => crate::ui::text_input_changed, json;
1747    PanelDropdown { panel: Ref [entity], options: Vec<String> [strs], initial: usize [copy] } => crate::ui::panel_dropdown, entity;
1748    DropdownSelected { dropdown: Ref [entity] } => crate::ui::dropdown_selected, json;
1749    PanelProgressBar { panel: Ref [entity], initial: f32 [copy] } => crate::ui::panel_progress_bar, entity;
1750    SetProgress { bar: Ref [entity], value: f32 [copy] } => crate::ui::set_progress, none;
1751    PanelToggle { panel: Ref [entity], initial: bool [copy] } => crate::ui::panel_toggle, entity;
1752    ToggleValue { toggle: Ref [entity] } => crate::ui::toggle_value, bool;
1753    PanelRadio { panel: Ref [entity], label: String [text], group_id: u32 [copy], option_index: usize [copy] } => crate::ui::panel_radio, entity;
1754    RadioSelected { group_id: u32 [copy] } => crate::ui::radio_selected, json;
1755    PanelRangeSlider { panel: Ref [entity], min: f32 [copy], max: f32 [copy], low: f32 [copy], high: f32 [copy] } => crate::ui::panel_range_slider, entity;
1756    SetRange { slider: Ref [entity], low: f32 [copy], high: f32 [copy] } => crate::ui::set_range, none;
1757    PanelTabs { panel: Ref [entity], labels: Vec<String> [strs], initial: usize [copy] } => crate::ui::panel_tabs, entity;
1758    SetTab { tabs: Ref [entity], index: usize [copy] } => crate::ui::set_tab, none;
1759    PanelCollapsing { panel: Ref [entity], label: String [text], open: bool [copy] } => crate::ui::panel_collapsing, entity;
1760    PanelColorPicker { panel: Ref [entity], initial: [f32; 4] [copy] } => crate::ui::panel_color_picker, entity;
1761    ColorValue { picker: Ref [entity] } => crate::ui::color_value, json;
1762    PanelTextArea { panel: Ref [entity], placeholder: String [text], rows: usize [copy] } => crate::ui::panel_text_area, entity;
1763    PanelTextAreaWithValue { panel: Ref [entity], placeholder: String [text], rows: usize [copy], initial: String [text] } => crate::ui::panel_text_area_with_value, entity;
1764    SetTextArea { area: Ref [entity], text: String [text] } => crate::ui::set_text_area, none;
1765    PanelMultiSelect { panel: Ref [entity], options: Vec<String> [strs] } => crate::ui::panel_multi_select, entity;
1766    SetMultiSelect { widget: Ref [entity], indices: Vec<u32> [indices] } => crate::ui::set_multi_select, none;
1767    PanelDatePicker { panel: Ref [entity], year: i32 [copy], month: u32 [copy], day: u32 [copy] } => crate::ui::panel_date_picker, entity;
1768    SetDate { picker: Ref [entity], year: i32 [copy], month: u32 [copy], day: u32 [copy] } => crate::ui::set_date, none;
1769    PanelMenu { panel: Ref [entity], label: String [text], items: Vec<String> [strs] } => crate::ui::panel_menu, entity;
1770    PanelColorPickerHsv { panel: Ref [entity], initial: [f32; 4] [copy] } => crate::ui::panel_color_picker_hsv, entity;
1771    PanelSplitter { panel: Ref [entity], horizontal: bool [copy], ratio: f32 [copy] } => panel_splitter_command, entity;
1772    PanelBreadcrumb { panel: Ref [entity], segments: Vec<String> [strs] } => crate::ui::panel_breadcrumb, entity;
1773    PanelVirtualList { panel: Ref [entity], item_height: f32 [copy], pool_size: usize [copy] } => crate::ui::panel_virtual_list, entity;
1774    PanelTable { panel: Ref [entity], headers: Vec<String> [strs], widths: Vec<f32> [floats] } => crate::ui::panel_table, entity;
1775    PanelDataGrid { panel: Ref [entity], headers: Vec<String> [strs], widths: Vec<f32> [floats], pool_size: usize [copy] } => panel_data_grid_command, entity;
1776    SetDataGridRows { grid: Ref [entity], count: usize [copy] } => crate::ui::set_data_grid_rows, none;
1777    SetDataGridCell { grid: Ref [entity], row: usize [copy], column: usize [copy], text: String [text] } => crate::ui::set_data_grid_cell, none;
1778    DataGridSelectionChanged { grid: Ref [entity] } => crate::ui::data_grid_selection_changed, bool;
1779    PanelCommandPalette { panel: Ref [entity], pool_size: usize [copy] } => crate::ui::panel_command_palette, entity;
1780    PanelPropertyGrid { panel: Ref [entity], label_width: f32 [copy] } => crate::ui::panel_property_grid, entity;
1781    PanelPropertyRow { grid: Ref [entity], label: String [text] } => crate::ui::panel_property_row, entity;
1782    PanelTreeView { panel: Ref [entity], multi_select: bool [copy] } => crate::ui::panel_tree_view, entity;
1783    TreeContent { tree_view: Ref [entity] } => crate::ui::tree_content, entity;
1784    TreeNode { tree_view: Ref [entity], parent_container: Ref [entity], label: String [text], depth: usize [copy], user_data: u64 [copy] } => crate::ui::tree_node, entity;
1785    TreeNodeChildren { node: Ref [entity] } => crate::ui::tree_node_children, entity;
1786    SetTreeNodeExpanded { node: Ref [entity], expanded: bool [copy] } => crate::ui::set_tree_node_expanded, none;
1787    TreeViewSelected { tree_view: Ref [entity] } => crate::ui::tree_view_selected, entities;
1788    PanelDragValue { panel: Ref [entity], min: f32 [copy], max: f32 [copy], initial: f32 [copy] } => crate::ui::panel_drag_value, entity;
1789    DragValue { widget: Ref [entity] } => crate::ui::drag_value, float;
1790    PanelSelectable { panel: Ref [entity], text: String [text], group: u32 [copy], grouped: bool [copy] } => panel_selectable_command, entity;
1791    PanelModal { panel: Ref [entity], title: String [text], width: f32 [copy], height: f32 [copy] } => crate::ui::panel_modal, entity;
1792    PanelSpinner { panel: Ref [entity] } => crate::ui::panel_spinner, entity;
1793    PanelSeparator { panel: Ref [entity] } => crate::ui::panel_separator, entity;
1794    PanelHeading { panel: Ref [entity], text: String [text] } => crate::ui::panel_heading, entity;
1795
1796    SaveSceneToFile { path: String [text] } => crate::filesystem::save_scene_to_path, bool;
1797    LoadSceneFromFile { path: String [text] } => crate::filesystem::load_scene_from_path, entities;
1798}
1799
1800#[cfg(test)]
1801mod tests {
1802    use super::*;
1803
1804    #[test]
1805    fn command_schema_covers_the_surface() {
1806        let schema = command_schema();
1807        assert!(schema.get("oneOf").is_some());
1808        let text = enum2schema::serde_json::to_string(&schema).unwrap();
1809        for variant in [
1810            "SpawnCube",
1811            "SpawnObject",
1812            "SetColor",
1813            "Rotate",
1814            "QueryTagged",
1815        ] {
1816            assert!(text.contains(variant), "schema missing {variant}");
1817        }
1818    }
1819
1820    #[test]
1821    fn reply_schema_is_generated() {
1822        assert!(command_reply_schema().get("oneOf").is_some());
1823    }
1824
1825    #[test]
1826    fn manifest_covers_the_surface() {
1827        let manifest = command_manifest();
1828        assert!(!manifest.is_empty());
1829        let names: Vec<&str> = manifest.iter().map(|spec| spec.name).collect();
1830        for variant in [
1831            "SpawnCube",
1832            "SpawnObject",
1833            "SetColor",
1834            "Rotate",
1835            "QueryTagged",
1836        ] {
1837            assert!(names.contains(&variant), "manifest missing {variant}");
1838        }
1839        let replies = [
1840            "none",
1841            "entity",
1842            "opt_entity",
1843            "bool",
1844            "float",
1845            "int",
1846            "text",
1847            "vector",
1848            "opt_vector",
1849            "entities",
1850            "strings",
1851            "bytes",
1852            "json",
1853        ];
1854        for spec in &manifest {
1855            assert!(
1856                replies.contains(&spec.reply),
1857                "unknown reply {}",
1858                spec.reply
1859            );
1860        }
1861    }
1862
1863    #[test]
1864    fn every_command_has_a_description() {
1865        for spec in command_manifest() {
1866            assert!(
1867                !spec.description.is_empty(),
1868                "command {} has no description; add one to command_description",
1869                spec.name
1870            );
1871        }
1872    }
1873
1874    #[test]
1875    fn entity_reference_serializes_as_its_schema_claims() {
1876        let entity = Entity {
1877            id: 3,
1878            generation: 1,
1879        };
1880        let value = enum2schema::serde_json::to_value(Ref::Entity(entity)).unwrap();
1881        let inner = value
1882            .get("Entity")
1883            .and_then(|tagged| tagged.as_object())
1884            .expect("Ref::Entity serializes as an externally tagged object");
1885        assert!(inner.contains_key("id"));
1886        assert!(inner.contains_key("generation"));
1887        assert_eq!(inner.len(), 2);
1888    }
1889}