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