Skip to main content

nightshade_api/
command.rs

1//! A serializable command form of the API.
2//!
3//! Every call here also exists as a free function. The command layer mirrors the
4//! API as a single [`Command`] enum so the same work can be driven three ways:
5//! call the free function directly, [`submit_command`] one [`Command`], or
6//! [`submit_commands`] a whole batch. The enum derives serde, so it doubles as
7//! the wire format a binding (FFI, network, scripting) targets: a binding only
8//! builds `Command` values and reads [`CommandReply`] back.
9//!
10//! Entities cross the boundary as the engine [`Entity`] itself, which is
11//! serializable and stable, so a reply hands one back and a binding stores it.
12//! Within a [`submit_commands`] batch a later command can also reference an
13//! entity an earlier command produced through [`Ref::Result`], with no round
14//! trip, which is what makes a single batch able to build and wire up a scene.
15//!
16//! The free functions stay the real implementations. The whole enum and its
17//! dispatch are generated from one registry by the [`commands`] macro, so the
18//! command surface cannot drift from the functions, and reference resolution and
19//! reply wrapping are uniform across every command rather than retyped per arm.
20
21use crate::scene::{Body, Object, Shape};
22use nightshade::prelude::{Entity, KeyCode, MouseButton, TextAlignment, Vec3, World, vec3};
23use serde::{Deserialize, Serialize};
24
25/// How a command names an entity: an existing [`Entity`] a reply handed back, or
26/// the entity produced by an earlier command in the same [`submit_commands`]
27/// batch. `Entity` is itself serializable and stable, so a binding stores it and
28/// hands it back verbatim, no separate handle type needed.
29#[derive(Serialize, Deserialize, Clone, Copy, Debug, enum2schema::Schema)]
30pub enum Ref {
31    Entity(#[schema(with = entity_schema)] Entity),
32    Result(u32),
33    /// A live entity named by its id alone, resolved against the world at
34    /// dispatch. Lets a caller reference an existing entity without tracking
35    /// its generation, which is what a picked editor selection or a script's
36    /// entity handle has.
37    Existing(u32),
38}
39
40/// What a [`Command`] returns. Setters reply [`CommandReply::None`], spawns reply
41/// the [`Entity`] they made, queries reply their value, and a failed reference
42/// resolution replies [`CommandReply::Error`].
43#[derive(Serialize, Deserialize, Clone, Debug, enum2schema::Schema)]
44pub enum CommandReply {
45    None,
46    Entity(#[schema(with = entity_schema)] Entity),
47    Bool(bool),
48    Float(f32),
49    Vector([f32; 3]),
50    Entities(#[schema(with = entities_schema)] Vec<Entity>),
51    Strings(Vec<String>),
52    Error(String),
53}
54
55/// One field of a [`Command`] as data: its name, its Rust type as written, and
56/// the dispatch role that says how the value is bound. Emitted by the same
57/// registry that defines the commands, so a binding generator reads the surface
58/// from a compiled artifact instead of parsing source.
59#[derive(Serialize, Clone, Debug)]
60pub struct FieldSpec {
61    pub name: &'static str,
62    pub type_name: &'static str,
63    pub role: &'static str,
64}
65
66/// One [`Command`] as data: its variant name, fields, and reply kind.
67#[derive(Serialize, Clone, Debug)]
68pub struct CommandSpec {
69    pub name: &'static str,
70    pub fields: Vec<FieldSpec>,
71    pub reply: &'static str,
72}
73
74/// [`command_manifest`] as a json string, the input a binding code generator
75/// reads alongside [`command_schema`].
76pub fn command_manifest_json() -> String {
77    enum2schema::serde_json::to_string(&command_manifest()).unwrap_or_default()
78}
79
80fn entity_schema() -> enum2schema::serde_json::Value {
81    enum2schema::serde_json::json!({
82        "type": "object",
83        "properties": {
84            "id": { "type": "integer" },
85            "generation": { "type": "integer" }
86        },
87        "required": ["id", "generation"]
88    })
89}
90
91fn entities_schema() -> enum2schema::serde_json::Value {
92    enum2schema::serde_json::json!({ "type": "array", "items": entity_schema() })
93}
94
95/// The json schema for [`Command`], the wire form a binding builds. Derived from
96/// the command enum by the same registry that defines it, so it always matches
97/// the surface. Pair with [`command_reply_schema`] for the output shape.
98pub fn command_schema() -> enum2schema::serde_json::Value {
99    <Command as enum2schema::Schema>::schema()
100}
101
102/// The json schema for [`CommandReply`], what a binding reads back.
103pub fn command_reply_schema() -> enum2schema::serde_json::Value {
104    <CommandReply as enum2schema::Schema>::schema()
105}
106
107/// Runs one command and returns its reply.
108pub fn submit_command(world: &mut World, command: &Command) -> CommandReply {
109    dispatch(world, command, &[])
110}
111
112/// Runs a batch in order and returns one reply per command. A command may name
113/// an entity an earlier command in the same batch produced with [`Ref::Result`],
114/// so a batch can spawn entities and then configure and parent them in one call.
115pub fn submit_commands(world: &mut World, commands: &[Command]) -> Vec<CommandReply> {
116    let mut produced: Vec<Option<Entity>> = Vec::with_capacity(commands.len());
117    let mut replies = Vec::with_capacity(commands.len());
118    for command in commands {
119        let reply = dispatch(world, command, &produced);
120        produced.push(match &reply {
121            CommandReply::Entity(entity) => Some(*entity),
122            _ => None,
123        });
124        replies.push(reply);
125    }
126    replies
127}
128
129fn resolve(world: &World, reference: Ref, produced: &[Option<Entity>]) -> Option<Entity> {
130    match reference {
131        Ref::Entity(entity) => Some(entity),
132        Ref::Result(index) => produced.get(index as usize).copied().flatten(),
133        Ref::Existing(id) => world
134            .core
135            .entity_locations
136            .get(id)
137            .filter(|location| location.allocated)
138            .map(|location| Entity {
139                id,
140                generation: location.generation,
141            }),
142    }
143}
144
145fn array_to_vec3(values: [f32; 3]) -> Vec3 {
146    vec3(values[0], values[1], values[2])
147}
148
149fn array_to_vec2(values: [f32; 2]) -> nightshade::prelude::Vec2 {
150    nightshade::prelude::vec2(values[0], values[1])
151}
152
153/// Adapts the flat command fields to [`spawn_object`](crate::scene::spawn_object),
154/// which takes an [`Object`] struct. The dispatch macro calls functions with
155/// positional arguments, so the one command that builds a struct goes through
156/// this rather than special casing the macro.
157fn spawn_object_command(
158    world: &mut World,
159    shape: Shape,
160    position: Vec3,
161    scale: Vec3,
162    color: [f32; 4],
163    body: Body,
164) -> Entity {
165    crate::scene::spawn_object(
166        world,
167        Object {
168            shape,
169            position,
170            scale,
171            color,
172            body,
173        },
174    )
175}
176
177/// Maps a plain key name to a [`KeyCode`] for the input-query commands, so the
178/// wire form names keys as strings like `"a"`, `"space"`, or `"left"` rather
179/// than carrying the engine's key enum.
180fn key_from_name(name: &str) -> Option<KeyCode> {
181    let lower = name.to_ascii_lowercase();
182    Some(match lower.as_str() {
183        "a" => KeyCode::KeyA,
184        "b" => KeyCode::KeyB,
185        "c" => KeyCode::KeyC,
186        "d" => KeyCode::KeyD,
187        "e" => KeyCode::KeyE,
188        "f" => KeyCode::KeyF,
189        "g" => KeyCode::KeyG,
190        "h" => KeyCode::KeyH,
191        "i" => KeyCode::KeyI,
192        "j" => KeyCode::KeyJ,
193        "k" => KeyCode::KeyK,
194        "l" => KeyCode::KeyL,
195        "m" => KeyCode::KeyM,
196        "n" => KeyCode::KeyN,
197        "o" => KeyCode::KeyO,
198        "p" => KeyCode::KeyP,
199        "q" => KeyCode::KeyQ,
200        "r" => KeyCode::KeyR,
201        "s" => KeyCode::KeyS,
202        "t" => KeyCode::KeyT,
203        "u" => KeyCode::KeyU,
204        "v" => KeyCode::KeyV,
205        "w" => KeyCode::KeyW,
206        "x" => KeyCode::KeyX,
207        "y" => KeyCode::KeyY,
208        "z" => KeyCode::KeyZ,
209        "0" => KeyCode::Digit0,
210        "1" => KeyCode::Digit1,
211        "2" => KeyCode::Digit2,
212        "3" => KeyCode::Digit3,
213        "4" => KeyCode::Digit4,
214        "5" => KeyCode::Digit5,
215        "6" => KeyCode::Digit6,
216        "7" => KeyCode::Digit7,
217        "8" => KeyCode::Digit8,
218        "9" => KeyCode::Digit9,
219        "space" => KeyCode::Space,
220        "enter" | "return" => KeyCode::Enter,
221        "escape" | "esc" => KeyCode::Escape,
222        "tab" => KeyCode::Tab,
223        "backspace" => KeyCode::Backspace,
224        "delete" => KeyCode::Delete,
225        "left" => KeyCode::ArrowLeft,
226        "right" => KeyCode::ArrowRight,
227        "up" => KeyCode::ArrowUp,
228        "down" => KeyCode::ArrowDown,
229        "shift" | "lshift" => KeyCode::ShiftLeft,
230        "rshift" => KeyCode::ShiftRight,
231        "ctrl" | "control" | "lctrl" => KeyCode::ControlLeft,
232        "rctrl" => KeyCode::ControlRight,
233        "alt" | "lalt" => KeyCode::AltLeft,
234        "ralt" => KeyCode::AltRight,
235        _ => return None,
236    })
237}
238
239/// Maps a button index to a [`MouseButton`]: 0 left, 1 middle, 2 right.
240fn mouse_button_from_index(index: u8) -> MouseButton {
241    match index {
242        1 => MouseButton::Middle,
243        2 => MouseButton::Right,
244        _ => MouseButton::Left,
245    }
246}
247
248fn key_down_command(world: &World, key: &str) -> bool {
249    key_from_name(key)
250        .map(|key| crate::input::key_down(world, key))
251        .unwrap_or(false)
252}
253
254fn key_pressed_command(world: &World, key: &str) -> bool {
255    key_from_name(key)
256        .map(|key| crate::input::key_pressed(world, key))
257        .unwrap_or(false)
258}
259
260fn mouse_down_command(world: &World, button: u8) -> bool {
261    crate::input::mouse_down(world, mouse_button_from_index(button))
262}
263
264fn mouse_clicked_command(world: &World, button: u8) -> bool {
265    crate::input::mouse_clicked(world, mouse_button_from_index(button))
266}
267
268macro_rules! bind_argument {
269    ($field:ident, entity, $produced:ident, $world:ident) => {
270        let $field = match resolve($world, *$field, $produced) {
271            Some(entity) => entity,
272            None => {
273                return CommandReply::Error(
274                    concat!(stringify!($field), ": unresolved entity reference").to_string(),
275                );
276            }
277        };
278    };
279    ($field:ident, opt_entity, $produced:ident, $world:ident) => {
280        let $field = match $field {
281            Some(reference) => match resolve($world, *reference, $produced) {
282                Some(entity) => Some(entity),
283                None => {
284                    return CommandReply::Error(
285                        concat!(stringify!($field), ": unresolved entity reference").to_string(),
286                    );
287                }
288            },
289            None => None,
290        };
291    };
292    ($field:ident, vec3, $produced:ident, $world:ident) => {
293        let $field = array_to_vec3(*$field);
294    };
295    ($field:ident, vec2, $produced:ident, $world:ident) => {
296        let $field = array_to_vec2(*$field);
297    };
298    ($field:ident, copy, $produced:ident, $world:ident) => {
299        let $field = *$field;
300    };
301    ($field:ident, owned, $produced:ident, $world:ident) => {
302        let $field = $field.clone();
303    };
304    ($field:ident, text, $produced:ident, $world:ident) => {
305        let $field = $field.as_str();
306    };
307    ($field:ident, bytes, $produced:ident, $world:ident) => {
308        let $field = $field.as_slice();
309    };
310}
311
312macro_rules! wrap_reply {
313    (none, $call:expr) => {{
314        $call;
315        CommandReply::None
316    }};
317    (entity, $call:expr) => {
318        CommandReply::Entity($call)
319    };
320    (opt_entity, $call:expr) => {
321        match $call {
322            Some(entity) => CommandReply::Entity(entity),
323            None => CommandReply::None,
324        }
325    };
326    (bool, $call:expr) => {
327        CommandReply::Bool($call)
328    };
329    (float, $call:expr) => {
330        CommandReply::Float($call)
331    };
332    (vector, $call:expr) => {{
333        let value = $call;
334        CommandReply::Vector([value.x, value.y, value.z])
335    }};
336    (opt_vector, $call:expr) => {
337        match $call {
338            Some(value) => CommandReply::Vector([value.x, value.y, value.z]),
339            None => CommandReply::None,
340        }
341    };
342    (entities, $call:expr) => {
343        CommandReply::Entities($call)
344    };
345    (strings, $call:expr) => {
346        CommandReply::Strings($call)
347    };
348}
349
350macro_rules! commands {
351    (
352        $(
353            $(#[$meta:meta])*
354            $variant:ident { $( $field:ident : $field_type:ty [$role:ident] ),* $(,)? }
355                => $func:path , $reply:ident ;
356        )*
357    ) => {
358        /// One API call as data. Field names and types match the free function
359        /// it mirrors. Positions, axes, and colors are plain arrays so the wire
360        /// form is clean json rather than a math library's internal layout.
361        #[derive(Serialize, Deserialize, Clone, Debug, enum2schema::Schema)]
362        pub enum Command {
363            $(
364                $(#[$meta])*
365                $variant { $( $field : $field_type ),* },
366            )*
367        }
368
369        impl Command {
370            /// This command's variant name, the key the manifest and a binding use,
371            /// read straight off the value with no serialization.
372            pub fn name(&self) -> &'static str {
373                match self {
374                    $(
375                        $(#[$meta])*
376                        Command::$variant { .. } => stringify!($variant),
377                    )*
378                }
379            }
380        }
381
382        fn dispatch(
383            world: &mut World,
384            command: &Command,
385            produced: &[Option<Entity>],
386        ) -> CommandReply {
387            match command {
388                $(
389                    $(#[$meta])*
390                    Command::$variant { $( $field ),* } => {
391                        $( bind_argument!($field, $role, produced, world); )*
392                        wrap_reply!($reply, $func(world $(, $field)*))
393                    }
394                )*
395            }
396        }
397
398        /// The command surface as data, generated from the same registry as
399        /// [`Command`] and its dispatch. cfg-gated commands appear only when
400        /// their feature is compiled, so the manifest matches the surface a
401        /// binding can actually reach.
402        pub fn command_manifest() -> Vec<CommandSpec> {
403            let mut specs = Vec::new();
404            $(
405                $(#[$meta])*
406                specs.extend([CommandSpec {
407                    name: stringify!($variant),
408                    fields: vec![
409                        $( FieldSpec {
410                            name: stringify!($field),
411                            type_name: stringify!($field_type),
412                            role: stringify!($role),
413                        } ),*
414                    ],
415                    reply: stringify!($reply),
416                }]);
417            )*
418            specs
419        }
420    };
421}
422
423commands! {
424    SpawnCube { position: [f32; 3] [vec3] } => crate::scene::spawn_cube, entity;
425    SpawnSphere { position: [f32; 3] [vec3] } => crate::scene::spawn_sphere, entity;
426    SpawnCylinder { position: [f32; 3] [vec3] } => crate::scene::spawn_cylinder, entity;
427    SpawnCone { position: [f32; 3] [vec3] } => crate::scene::spawn_cone, entity;
428    SpawnPlane { position: [f32; 3] [vec3] } => crate::scene::spawn_plane, entity;
429    SpawnTorus { position: [f32; 3] [vec3] } => crate::scene::spawn_torus, entity;
430    SpawnFloor { half_extent: f32 [copy] } => crate::scene::spawn_floor, entity;
431    SpawnGroup { position: [f32; 3] [vec3] } => crate::scene::spawn_group, entity;
432    SpawnModel { glb: Vec<u8> [bytes], position: [f32; 3] [vec3] } => crate::scene::spawn_model, entity;
433    SpawnObject { shape: Shape [copy], position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy], body: Body [copy] } => spawn_object_command, entity;
434
435    SetColor { entity: Ref [entity], color: [f32; 4] [copy] } => crate::appearance::set_color, none;
436    SetMetallicRoughness { entity: Ref [entity], metallic: f32 [copy], roughness: f32 [copy] } => crate::appearance::set_metallic_roughness, none;
437    SetEmissive { entity: Ref [entity], color: [f32; 3] [copy], strength: f32 [copy] } => crate::appearance::set_emissive, none;
438    SetUnlit { entity: Ref [entity], unlit: bool [copy] } => crate::appearance::set_unlit, none;
439    SetTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_texture, none;
440    SetTextureTiling { entity: Ref [entity], repeats: f32 [copy] } => crate::appearance::set_texture_tiling, none;
441    SetNormalTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_normal_texture, none;
442    SetMetallicRoughnessTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_metallic_roughness_texture, none;
443    SetEmissiveTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_emissive_texture, none;
444    SetOcclusionTexture { entity: Ref [entity], texture: String [text] } => crate::appearance::set_occlusion_texture, none;
445
446    SetPosition { entity: Ref [entity], position: [f32; 3] [vec3] } => crate::placement::set_position, none;
447    SetScale { entity: Ref [entity], scale: [f32; 3] [vec3] } => crate::placement::set_scale, none;
448    SetRotation { entity: Ref [entity], axis: [f32; 3] [vec3], radians: f32 [copy] } => crate::placement::set_rotation, none;
449    Rotate { entity: Ref [entity], axis: [f32; 3] [vec3], radians: f32 [copy] } => crate::placement::rotate, none;
450    Position { entity: Ref [entity] } => crate::placement::position, vector;
451
452    SetParent { child: Ref [entity], parent: Option<Ref> [opt_entity] } => crate::scene::set_parent, none;
453    SetVisible { entity: Ref [entity], visible: bool [copy] } => crate::scene::set_visible, none;
454    Despawn { entity: Ref [entity] } => crate::scene::despawn, none;
455
456    Tag { entity: Ref [entity], label: String [text] } => crate::groups::tag, none;
457    Untag { entity: Ref [entity], label: String [text] } => crate::groups::untag, none;
458    HasTag { entity: Ref [entity], label: String [text] } => crate::groups::has_tag, bool;
459    QueryTagged { label: String [text] } => crate::groups::tagged, entities;
460
461    PointLight { position: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::point_light, entity;
462    SpotLight { position: [f32; 3] [vec3], target: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::spot_light, entity;
463    SetSun { color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::set_sun, none;
464
465    SetBackground { background: crate::environment::Background [owned] } => crate::environment::set_background, none;
466    ShowGrid { enabled: bool [copy] } => crate::environment::show_grid, none;
467    SetAmbient { color: [f32; 4] [copy] } => crate::environment::set_ambient, none;
468    SetBloom { enabled: bool [copy] } => crate::environment::set_bloom, none;
469    SetBloomIntensity { intensity: f32 [copy] } => crate::environment::set_bloom_intensity, none;
470    SetSsao { enabled: bool [copy] } => crate::environment::set_ssao, none;
471    SetSsr { enabled: bool [copy] } => crate::environment::set_ssr, none;
472    SetSsgi { enabled: bool [copy] } => crate::environment::set_ssgi, none;
473    SetFxaa { enabled: bool [copy] } => crate::environment::set_fxaa, none;
474    SetExposure { exposure: f32 [copy] } => crate::environment::set_exposure, none;
475    SetColorGrading { saturation: f32 [copy], contrast: f32 [copy], brightness: f32 [copy] } => crate::environment::set_color_grading, none;
476    SetTimeOfDay { hour: f32 [copy] } => crate::environment::set_time_of_day, none;
477    SetTitle { title: String [text] } => crate::environment::set_title, none;
478
479    EmitFire { position: [f32; 3] [vec3] } => crate::effects::emit_fire, entity;
480    EmitSmoke { position: [f32; 3] [vec3] } => crate::effects::emit_smoke, entity;
481    EmitBurst { position: [f32; 3] [vec3], color: [f32; 4] [copy], count: u32 [copy] } => crate::effects::emit_burst, entity;
482
483    DrawCube { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cube, none;
484    DrawSphere { position: [f32; 3] [vec3], radius: f32 [copy], color: [f32; 4] [copy] } => crate::draw::draw_sphere, none;
485    DrawCylinder { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cylinder, none;
486    DrawCone { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_cone, none;
487    DrawTorus { position: [f32; 3] [vec3], scale: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_torus, none;
488    DrawLine { start: [f32; 3] [vec3], end: [f32; 3] [vec3], color: [f32; 4] [copy] } => crate::draw::draw_line, none;
489    DrawText3d { text: String [text], position: [f32; 3] [vec3] } => crate::draw::draw_text_3d, none;
490
491    SpawnLabel { text: String [text], position: [f32; 3] [vec3] } => crate::text::spawn_label, entity;
492    SpawnText { text: String [text], anchor: crate::text::ScreenAnchor [copy] } => crate::text::spawn_text, entity;
493    SetText { entity: Ref [entity], text: String [text] } => crate::text::set_text, none;
494    SetTextColor { entity: Ref [entity], color: [f32; 4] [copy] } => crate::text::set_text_color, none;
495    SetTextSize { entity: Ref [entity], size: f32 [copy] } => crate::text::set_text_size, none;
496
497    SpawnPanel { anchor: crate::text::ScreenAnchor [copy], width: f32 [copy], height: f32 [copy] } => crate::ui::spawn_panel, entity;
498    PanelLabel { panel: Ref [entity], text: String [text] } => crate::ui::panel_label, entity;
499    PanelButton { panel: Ref [entity], text: String [text] } => crate::ui::panel_button, entity;
500    ButtonClicked { button: Ref [entity] } => crate::ui::button_clicked, bool;
501    ButtonHovered { button: Ref [entity] } => crate::ui::button_hovered, bool;
502    DespawnPanel { panel: Ref [entity] } => crate::ui::despawn_panel, none;
503    PanelRow { panel: Ref [entity], height: f32 [copy] } => crate::ui::panel_row, entity;
504    PanelGrid { panel: Ref [entity], columns: usize [copy], row_height: f32 [copy], height: f32 [copy] } => crate::ui::panel_grid, entity;
505    PanelScroll { panel: Ref [entity], height: f32 [copy] } => crate::ui::panel_scroll, entity;
506    SetScrollOffset { scroll_area: Ref [entity], offset: f32 [copy] } => crate::ui::set_scroll_offset, none;
507    SetFocusOrder { entity: Ref [entity], order: i32 [copy] } => crate::ui::set_focus_order, none;
508    FocusWidget { entity: Ref [entity] } => crate::ui::focus_widget, none;
509    SpawnPanelAt { anchor: crate::text::ScreenAnchor [copy], offset: [f32; 2] [vec2], size: [f32; 2] [vec2], color: [f32; 4] [copy] } => crate::ui::spawn_panel_at, entity;
510    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;
511    PanelBox { parent: Ref [entity], offset: [f32; 2] [vec2], size: [f32; 2] [vec2], color: [f32; 4] [copy] } => crate::ui::panel_box, entity;
512    PanelButtonAt { parent: Ref [entity], label: String [text], offset: [f32; 2] [vec2], size: [f32; 2] [vec2] } => crate::ui::panel_button_at, entity;
513    SetPanelRect { node: Ref [entity], offset: [f32; 2] [vec2], size: [f32; 2] [vec2] } => crate::ui::set_panel_rect, none;
514    SetPanelColor { node: Ref [entity], color: [f32; 4] [copy] } => crate::ui::set_panel_color, none;
515    SetPanelText { label: Ref [entity], text: String [text] } => crate::ui::set_panel_text, none;
516    SetPanelTextColor { label: Ref [entity], color: [f32; 4] [copy] } => crate::ui::set_panel_text_color, none;
517    SetPanelSelected { button: Ref [entity], selected: bool [copy], accent: [f32; 4] [copy] } => crate::ui::set_panel_selected, none;
518    SetPanelVisible { node: Ref [entity], visible: bool [copy] } => crate::ui::set_panel_visible, none;
519
520    PlayAnimation { entity: Ref [entity], clip: usize [copy] } => crate::scene::play_animation, none;
521    PlayAnimationNamed { entity: Ref [entity], name: String [text] } => crate::scene::play_animation_named, bool;
522    SetAnimationLooping { entity: Ref [entity], looping: bool [copy] } => crate::scene::set_animation_looping, none;
523    SetAnimationSpeed { entity: Ref [entity], speed: f32 [copy] } => crate::scene::set_animation_speed, none;
524    BlendToAnimation { entity: Ref [entity], clip: usize [copy], seconds: f32 [copy] } => crate::scene::blend_to_animation, none;
525    PauseAnimation { entity: Ref [entity] } => crate::scene::pause_animation, none;
526    ResumeAnimation { entity: Ref [entity] } => crate::scene::resume_animation, none;
527    StopAnimation { entity: Ref [entity] } => crate::scene::stop_animation, none;
528    AnimationClips { entity: Ref [entity] } => crate::scene::animation_clips, strings;
529    AddAnimationEvent { entity: Ref [entity], clip_index: usize [copy], time: f32 [copy], name: String [text] } => crate::scene::add_animation_event, bool;
530    AddAnimationEventNamed { entity: Ref [entity], clip_name: String [text], time: f32 [copy], name: String [text] } => crate::scene::add_animation_event_named, bool;
531    SetAnimationLayerWeight { entity: Ref [entity], layer_index: usize [copy], weight: f32 [copy] } => crate::scene::set_animation_layer_weight, none;
532    ClearAnimationLayers { entity: Ref [entity] } => crate::scene::clear_animation_layers, none;
533    AimAt { bone: Ref [entity], target: [f32; 3] [vec3], forward: [f32; 3] [vec3] } => crate::animate::aim_at, none;
534
535    OrbitCamera { focus: [f32; 3] [vec3], radius: f32 [copy] } => crate::camera::orbit_camera, entity;
536    FlyCamera { position: [f32; 3] [vec3] } => crate::camera::fly_camera, entity;
537    FixedCamera { eye: [f32; 3] [vec3], target: [f32; 3] [vec3] } => crate::camera::fixed_camera, entity;
538    LookAt { eye: [f32; 3] [vec3], target: [f32; 3] [vec3] } => crate::camera::look_at, none;
539    SetOrbitFocus { focus: [f32; 3] [vec3] } => crate::camera::set_orbit_focus, none;
540    SetOrbitView { focus: [f32; 3] [vec3], radius: f32 [copy], yaw: f32 [copy], pitch: f32 [copy] } => crate::camera::set_orbit_view, none;
541    SetOrbitZoom { enabled: bool [copy] } => crate::camera::set_orbit_zoom, none;
542    SetFieldOfView { degrees: f32 [copy] } => crate::camera::set_field_of_view, none;
543    SetOrthographic { half_height: f32 [copy] } => crate::camera::set_orthographic, none;
544    SetPerspective { degrees: f32 [copy] } => crate::camera::set_perspective, none;
545    CameraPosition {} => crate::camera::camera_position, vector;
546    CameraForward {} => crate::camera::camera_forward, vector;
547    #[cfg(feature = "physics")]
548    FirstPerson { position: [f32; 3] [vec3] } => crate::camera::first_person, entity;
549
550    DeltaTime {} => crate::input::delta_time, float;
551    ElapsedSeconds {} => crate::input::elapsed_seconds, float;
552    KeyDown { key: String [text] } => key_down_command, bool;
553    KeyPressed { key: String [text] } => key_pressed_command, bool;
554    MouseDown { button: u8 [copy] } => mouse_down_command, bool;
555    MouseClicked { button: u8 [copy] } => mouse_clicked_command, bool;
556    Wasd {} => crate::input::wasd, vector;
557    PointerOverUi {} => crate::input::pointer_over_ui, bool;
558    MouseScroll {} => crate::input::mouse_scroll, float;
559
560    #[cfg(feature = "physics")]
561    Push { entity: Ref [entity], impulse: [f32; 3] [vec3] } => crate::physics::push, none;
562    #[cfg(feature = "physics")]
563    SetVelocity { entity: Ref [entity], velocity: [f32; 3] [vec3] } => crate::physics::set_velocity, none;
564    #[cfg(feature = "physics")]
565    ApplyForce { entity: Ref [entity], force: [f32; 3] [vec3] } => crate::physics::apply_force, none;
566    #[cfg(feature = "physics")]
567    ApplyTorque { entity: Ref [entity], torque: [f32; 3] [vec3] } => crate::physics::apply_torque, none;
568    #[cfg(feature = "physics")]
569    SetAngularVelocity { entity: Ref [entity], velocity: [f32; 3] [vec3] } => crate::physics::set_angular_velocity, none;
570    #[cfg(feature = "physics")]
571    Velocity { entity: Ref [entity] } => crate::physics::velocity, opt_vector;
572    #[cfg(feature = "physics")]
573    AngularVelocity { entity: Ref [entity] } => crate::physics::angular_velocity, opt_vector;
574    #[cfg(feature = "physics")]
575    MakeSensor { entity: Ref [entity] } => crate::physics::make_sensor, none;
576    #[cfg(feature = "physics")]
577    OverlapSphere { center: [f32; 3] [vec3], radius: f32 [copy] } => crate::physics::overlap_sphere, entities;
578    #[cfg(feature = "physics")]
579    SetCollisionGroups { entity: Ref [entity], membership: u32 [copy], filter: u32 [copy] } => crate::physics::set_collision_groups, none;
580    #[cfg(feature = "physics")]
581    SetFriction { entity: Ref [entity], friction: f32 [copy] } => crate::physics::set_friction, none;
582    #[cfg(feature = "physics")]
583    SetRestitution { entity: Ref [entity], restitution: f32 [copy] } => crate::physics::set_restitution, none;
584    #[cfg(feature = "physics")]
585    SetLinearDamping { entity: Ref [entity], damping: f32 [copy] } => crate::physics::set_linear_damping, none;
586    #[cfg(feature = "physics")]
587    SetAngularDamping { entity: Ref [entity], damping: f32 [copy] } => crate::physics::set_angular_damping, none;
588    #[cfg(feature = "physics")]
589    SetMass { entity: Ref [entity], mass: f32 [copy] } => crate::physics::set_mass, none;
590    #[cfg(feature = "physics")]
591    SetGravityScale { entity: Ref [entity], scale: f32 [copy] } => crate::physics::set_gravity_scale, none;
592
593    #[cfg(feature = "navmesh")]
594    BakeNavmesh {} => crate::navigation::bake_navmesh, none;
595    #[cfg(feature = "navmesh")]
596    SpawnWalker { position: [f32; 3] [vec3] } => crate::navigation::spawn_walker, entity;
597    #[cfg(feature = "navmesh")]
598    WalkTo { agent: Ref [entity], destination: [f32; 3] [vec3] } => crate::navigation::walk_to, none;
599    #[cfg(feature = "navmesh")]
600    SetWalkSpeed { agent: Ref [entity], speed: f32 [copy] } => crate::navigation::set_walk_speed, none;
601    #[cfg(feature = "navmesh")]
602    StopWalking { agent: Ref [entity] } => crate::navigation::stop_walking, none;
603
604    #[cfg(feature = "picking")]
605    ClickedEntity {} => crate::picking::clicked_entity, opt_entity;
606    #[cfg(feature = "picking")]
607    EntityUnderCursor {} => crate::picking::entity_under_cursor, opt_entity;
608    #[cfg(feature = "picking")]
609    CursorOnGround {} => crate::picking::cursor_on_ground, opt_vector;
610    #[cfg(feature = "picking")]
611    SpawnWorldPanel { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy], color: [f32; 4] [copy] } => crate::world_ui::spawn_world_panel, entity;
612    #[cfg(feature = "picking")]
613    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;
614    #[cfg(feature = "picking")]
615    WorldPanelLabel { panel: Ref [entity], text: String [text], x: f32 [copy], y: f32 [copy] } => crate::world_ui::world_panel_label, entity;
616    #[cfg(feature = "picking")]
617    WorldButtonClicked { button: Ref [entity] } => crate::world_ui::world_button_clicked, bool;
618
619    #[cfg(feature = "audio")]
620    PauseSound { entity: Ref [entity] } => crate::audio::pause_sound, none;
621    #[cfg(feature = "audio")]
622    ResumeSound { entity: Ref [entity] } => crate::audio::resume_sound, none;
623    #[cfg(feature = "audio")]
624    FadeVolume { entity: Ref [entity], volume: f32 [copy], seconds: f32 [copy] } => crate::audio::fade_volume, none;
625    #[cfg(feature = "audio")]
626    Crossfade { fade_out: Ref [entity], fade_in: Ref [entity], volume: f32 [copy], seconds: f32 [copy] } => crate::audio::crossfade, none;
627    #[cfg(feature = "audio")]
628    SetBusVolume { bus: nightshade::prelude::AudioBus [copy], decibels: f32 [copy], fade_seconds: f32 [copy] } => crate::audio::set_bus_volume, none;
629    #[cfg(feature = "audio")]
630    DuckVoice { amount: f32 [copy], fade_seconds: f32 [copy] } => crate::audio::duck_voice, none;
631
632    DirectionalLight { direction: [f32; 3] [vec3], color: [f32; 3] [copy], intensity: f32 [copy] } => crate::lighting::directional_light, entity;
633    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;
634    SetLightShadows { light: Ref [entity], enabled: bool [copy] } => crate::lighting::set_light_shadows, none;
635
636    EmitSparks { position: [f32; 3] [vec3] } => crate::effects::emit_sparks, entity;
637    EmitFirework { position: [f32; 3] [vec3], velocity: [f32; 3] [vec3] } => crate::effects::emit_firework, entity;
638    EmitParticles { position: [f32; 3] [vec3], rate: f32 [copy], lifetime: f32 [copy], size: f32 [copy], gravity: [f32; 3] [vec3] } => crate::effects::emit_particles, entity;
639
640    SetAlphaBlend { entity: Ref [entity], enabled: bool [copy] } => crate::appearance::set_alpha_blend, none;
641    SetAlphaCutoff { entity: Ref [entity], cutoff: f32 [copy] } => crate::appearance::set_alpha_cutoff, none;
642    SetDoubleSided { entity: Ref [entity], double_sided: bool [copy] } => crate::appearance::set_double_sided, none;
643    SetIor { entity: Ref [entity], ior: f32 [copy] } => crate::appearance::set_ior, none;
644    SetTransmission { entity: Ref [entity], factor: f32 [copy] } => crate::appearance::set_transmission, none;
645    SetClearcoat { entity: Ref [entity], factor: f32 [copy], roughness: f32 [copy] } => crate::appearance::set_clearcoat, none;
646    SetAnisotropy { entity: Ref [entity], strength: f32 [copy], rotation: f32 [copy] } => crate::appearance::set_anisotropy, none;
647    SetUvTransform { entity: Ref [entity], offset: [f32; 2] [copy], scale: [f32; 2] [copy], rotation: f32 [copy] } => crate::appearance::set_uv_transform, none;
648    SetSheen { entity: Ref [entity], color: [f32; 3] [copy], roughness: f32 [copy] } => crate::appearance::set_sheen, none;
649    SetIridescence { entity: Ref [entity], factor: f32 [copy], ior: f32 [copy] } => crate::appearance::set_iridescence, none;
650    SetSpecular { entity: Ref [entity], factor: f32 [copy], color: [f32; 3] [copy] } => crate::appearance::set_specular, none;
651    SetNormalScale { entity: Ref [entity], scale: f32 [copy] } => crate::appearance::set_normal_scale, none;
652    SetOcclusionStrength { entity: Ref [entity], strength: f32 [copy] } => crate::appearance::set_occlusion_strength, none;
653    SetEmissiveStrength { entity: Ref [entity], strength: f32 [copy] } => crate::appearance::set_emissive_strength, none;
654    SetThickness { entity: Ref [entity], thickness: f32 [copy] } => crate::appearance::set_thickness, none;
655
656    SetTextOutline { entity: Ref [entity], width: f32 [copy], color: [f32; 4] [copy] } => crate::text::set_text_outline, none;
657
658    SetMorphWeight { entity: Ref [entity], index: u32 [copy], weight: f32 [copy] } => crate::morph::set_morph_weight, none;
659
660    SetWindowTitle { title: String [text] } => crate::window::set_window_title, none;
661    LockCursor { locked: bool [copy] } => crate::window::lock_cursor, none;
662    RequestExit {} => crate::window::request_exit, none;
663
664    SetRenderLayer { entity: Ref [entity], layer: u32 [copy] } => crate::render::set_render_layer, none;
665    SetCameraLayers { camera: Ref [entity], mask: u32 [copy] } => crate::render::set_camera_layers, none;
666
667    ThirdPersonCamera { target: Ref [entity], distance: f32 [copy] } => crate::camera::third_person_camera, entity;
668
669    SpawnCloth { position: [f32; 3] [vec3], width: f32 [copy], height: f32 [copy], columns: u32 [copy], rows: u32 [copy] } => crate::cloth::spawn_cloth, entity;
670    ResetCloth { entity: Ref [entity] } => crate::cloth::reset_cloth, none;
671    SetWind { direction: [f32; 3] [vec3], strength: f32 [copy] } => crate::cloth::set_wind, none;
672
673    PauseCutscene {} => crate::cutscene::pause_cutscene, none;
674    ResumeCutscene {} => crate::cutscene::resume_cutscene, none;
675    StopCutscene {} => crate::cutscene::stop_cutscene, none;
676    SeekCutscene { seconds: f32 [copy] } => crate::cutscene::seek_cutscene, none;
677    SetCutsceneCamera { camera: Ref [entity] } => crate::cutscene::set_cutscene_camera, none;
678    BindCutsceneActor { name: String [text], entity: Ref [entity] } => crate::cutscene::bind_cutscene_actor, none;
679
680    #[cfg(feature = "physics")]
681    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;
682    #[cfg(feature = "physics")]
683    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;
684    #[cfg(feature = "physics")]
685    SetControllerSpeed { entity: Ref [entity], speed: f32 [copy] } => crate::character::set_controller_speed, none;
686    #[cfg(feature = "physics")]
687    SetControllerJump { entity: Ref [entity], impulse: f32 [copy] } => crate::character::set_controller_jump, none;
688    #[cfg(feature = "physics")]
689    IsGrounded { entity: Ref [entity] } => crate::character::is_grounded, bool;
690
691    #[cfg(feature = "terrain")]
692    EnableTerrain { seed: u32 [copy] } => crate::terrain::enable_terrain, none;
693    #[cfg(feature = "terrain")]
694    DisableTerrain {} => crate::terrain::disable_terrain, none;
695    #[cfg(feature = "terrain")]
696    SetTerrainHeightRange { min: f32 [copy], max: f32 [copy] } => crate::terrain::set_terrain_height_range, none;
697    #[cfg(feature = "terrain")]
698    SetTerrainSnowHeight { height: f32 [copy] } => crate::terrain::set_terrain_snow_height, none;
699    #[cfg(feature = "grass")]
700    EnableGrass {} => crate::terrain::enable_grass, none;
701    #[cfg(feature = "grass")]
702    DisableGrass {} => crate::terrain::disable_grass, none;
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    #[test]
710    fn command_schema_covers_the_surface() {
711        let schema = command_schema();
712        assert!(schema.get("oneOf").is_some());
713        let text = enum2schema::serde_json::to_string(&schema).unwrap();
714        for variant in [
715            "SpawnCube",
716            "SpawnObject",
717            "SetColor",
718            "Rotate",
719            "QueryTagged",
720        ] {
721            assert!(text.contains(variant), "schema missing {variant}");
722        }
723    }
724
725    #[test]
726    fn reply_schema_is_generated() {
727        assert!(command_reply_schema().get("oneOf").is_some());
728    }
729
730    #[test]
731    fn manifest_covers_the_surface() {
732        let manifest = command_manifest();
733        assert!(!manifest.is_empty());
734        let names: Vec<&str> = manifest.iter().map(|spec| spec.name).collect();
735        for variant in [
736            "SpawnCube",
737            "SpawnObject",
738            "SetColor",
739            "Rotate",
740            "QueryTagged",
741        ] {
742            assert!(names.contains(&variant), "manifest missing {variant}");
743        }
744        let replies = [
745            "none",
746            "entity",
747            "opt_entity",
748            "bool",
749            "float",
750            "vector",
751            "opt_vector",
752            "entities",
753            "strings",
754        ];
755        for spec in &manifest {
756            assert!(
757                replies.contains(&spec.reply),
758                "unknown reply {}",
759                spec.reply
760            );
761        }
762    }
763
764    #[test]
765    fn entity_reference_serializes_as_its_schema_claims() {
766        let entity = Entity {
767            id: 3,
768            generation: 1,
769        };
770        let value = enum2schema::serde_json::to_value(Ref::Entity(entity)).unwrap();
771        let inner = value
772            .get("Entity")
773            .and_then(|tagged| tagged.as_object())
774            .expect("Ref::Entity serializes as an externally tagged object");
775        assert!(inner.contains_key("id"));
776        assert!(inner.contains_key("generation"));
777        assert_eq!(inner.len(), 2);
778    }
779}