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