1use crate::scene::{Body, Object, Shape};
22use nightshade::prelude::{Entity, KeyCode, MouseButton, TextAlignment, Vec3, World, vec3};
23use serde::{Deserialize, Serialize};
24
25#[derive(Serialize, Deserialize, Clone, Copy, Debug, enum2schema::Schema)]
30pub enum Ref {
31 Entity(#[schema(with = entity_schema)] Entity),
32 Result(u32),
33 Existing(u32),
38}
39
40#[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#[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#[derive(Serialize, Clone, Debug)]
68pub struct CommandSpec {
69 pub name: &'static str,
70 pub fields: Vec<FieldSpec>,
71 pub reply: &'static str,
72}
73
74pub 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
95pub fn command_schema() -> enum2schema::serde_json::Value {
99 <Command as enum2schema::Schema>::schema()
100}
101
102pub fn command_reply_schema() -> enum2schema::serde_json::Value {
104 <CommandReply as enum2schema::Schema>::schema()
105}
106
107pub fn submit_command(world: &mut World, command: &Command) -> CommandReply {
109 dispatch(world, command, &[])
110}
111
112pub 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
153fn 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
177fn 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
239fn 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 #[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 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 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}