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