1use nightshade::ecs::world::CORE;
18use std::collections::{HashMap, HashSet};
19
20use rhai::{AST, Array, Dynamic, Engine, Map, Scope};
21
22use nightshade::ecs::event::{Event, events};
23use nightshade::ecs::script::components::ScriptSource;
24use nightshade::ecs::world::SCRIPT;
25use nightshade::prelude::{Entity, KeyCode, MouseState, Vec3, World, tracing};
26
27use crate::command::{Command, CommandReply, submit_commands};
28
29const TAG_ID_KEY: &str = "$id";
33const TAG_COMMAND_KEY: &str = "$cmd";
34
35#[derive(Clone, Default)]
40pub enum CommandPolicy {
41 #[default]
42 AllowAll,
43 Allow(HashSet<String>),
44}
45
46impl CommandPolicy {
47 fn allows(&self, name: &str) -> bool {
48 match self {
49 CommandPolicy::AllowAll => true,
50 CommandPolicy::Allow(allowed) => allowed.contains(name),
51 }
52 }
53}
54
55pub struct ScriptRuntime {
59 pub enabled: bool,
60 pub policy: CommandPolicy,
61 engine: Engine,
62 modules: ScriptModules,
63 compiled: HashMap<u64, AST>,
64 scopes: HashMap<Entity, Scope<'static>>,
65 global_scopes: HashMap<String, Scope<'static>>,
66 started_globals: HashSet<String>,
67 started_entities: HashSet<Entity>,
68 next_replies: Map,
69 assets: HashMap<String, Vec<u8>>,
70 random_state: u64,
71}
72
73const RANDOM_SEED: u64 = 0x853c_49e6_748f_ea9b;
76
77#[derive(Clone, Default)]
82struct ScriptModules {
83 table: std::sync::Arc<
84 std::sync::RwLock<std::collections::BTreeMap<String, rhai::Shared<rhai::Module>>>,
85 >,
86}
87
88impl ScriptModules {
89 fn insert(&self, name: &str, module: rhai::Module) {
90 if let Ok(mut table) = self.table.write() {
91 table.insert(name.to_string(), rhai::Shared::new(module));
92 }
93 }
94}
95
96impl rhai::ModuleResolver for ScriptModules {
97 fn resolve(
98 &self,
99 _engine: &Engine,
100 _source: Option<&str>,
101 path: &str,
102 position: rhai::Position,
103 ) -> Result<rhai::Shared<rhai::Module>, Box<rhai::EvalAltResult>> {
104 let not_found = || {
105 Box::new(rhai::EvalAltResult::ErrorModuleNotFound(
106 path.to_string(),
107 position,
108 ))
109 };
110 match self.table.read() {
111 Ok(table) => table.get(path).cloned().ok_or_else(not_found),
112 Err(_) => Err(not_found()),
113 }
114 }
115}
116
117const STD_MODULES: &[(&str, &str)] = &[
121 ("std/ease", include_str!("std_scripts/ease.rhai")),
122 ("std/math", include_str!("std_scripts/math.rhai")),
123 ("std/vec", include_str!("std_scripts/vec.rhai")),
124 ("std/timer", include_str!("std_scripts/timer.rhai")),
125];
126
127fn seed_std_modules(engine: &Engine, modules: &ScriptModules) {
129 for (name, source) in STD_MODULES {
130 match engine.compile(*source) {
131 Ok(ast) => match rhai::Module::eval_ast_as_new(Scope::new(), &ast, engine) {
132 Ok(module) => modules.insert(name, module),
133 Err(error) => tracing::error!("std module {name} failed to build: {error}"),
134 },
135 Err(error) => tracing::error!("std module {name} failed to parse: {error}"),
136 }
137 }
138}
139
140fn new_engine(modules: &ScriptModules) -> Engine {
145 let mut engine = Engine::new();
146 engine.set_max_operations(200_000);
147 engine.set_max_call_levels(64);
148 engine.set_max_string_size(8 * 1024);
149 engine.set_max_array_size(4096);
150 engine.set_max_expr_depths(0, 0);
151 register_api(&mut engine);
152 engine.set_module_resolver(modules.clone());
153 seed_std_modules(&engine, modules);
154 engine
155}
156
157impl Default for ScriptRuntime {
158 fn default() -> Self {
159 let modules = ScriptModules::default();
160 let engine = new_engine(&modules);
161 Self {
162 enabled: false,
163 policy: CommandPolicy::AllowAll,
164 engine,
165 modules,
166 compiled: HashMap::new(),
167 scopes: HashMap::new(),
168 global_scopes: HashMap::new(),
169 started_globals: HashSet::new(),
170 started_entities: HashSet::new(),
171 next_replies: Map::new(),
172 assets: HashMap::new(),
173 random_state: RANDOM_SEED,
174 }
175 }
176}
177
178#[derive(Default)]
183pub struct ScriptReport {
184 pub commands: Vec<Command>,
185 pub errors: Vec<String>,
186 pub logs: Vec<String>,
187}
188
189thread_local! {
190 static SCRIPT_RNG: std::cell::Cell<u64> = const { std::cell::Cell::new(RANDOM_SEED) };
196
197 static SCRIPT_LOGS: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
202}
203
204fn next_unit_random() -> f64 {
207 SCRIPT_RNG.with(|cell| {
208 let mut state = cell.get();
209 state ^= state << 13;
210 state ^= state >> 7;
211 state ^= state << 17;
212 cell.set(state);
213 ((state >> 11) as f64) / ((1u64 << 53) as f64)
214 })
215}
216
217fn register_api(engine: &mut Engine) {
224 engine.register_fn("log", |message: &str| {
225 tracing::info!("[script] {message}");
226 SCRIPT_LOGS.with(|logs| logs.borrow_mut().push(message.to_string()));
227 });
228 engine.register_fn("random", next_unit_random);
231 engine.register_fn("random_range", |low: f64, high: f64| {
232 low + next_unit_random() * (high - low)
233 });
234 engine.register_fn("random_int", |low: i64, high: i64| {
235 if high <= low {
236 low
237 } else {
238 low + (next_unit_random() * ((high - low + 1) as f64)) as i64
239 }
240 });
241 register_mixed_number_ops(engine);
242 register_color_helpers(engine);
243 register_vector_helpers(engine);
244 engine.register_fn("entity_ref", |packed: i64| {
245 let entity = script_to_entity(packed);
246 let mut map = Map::new();
247 map.insert("Existing".into(), Dynamic::from(entity.id as i64));
248 Dynamic::from_map(map)
249 });
250 engine.register_fn("result", |index: i64| {
254 let mut map = Map::new();
255 map.insert("Result".into(), Dynamic::from(index));
256 Dynamic::from_map(map)
257 });
258 engine.register_fn("last", |commands: &mut Array| {
262 let index = commands.len().saturating_sub(1) as i64;
263 let mut map = Map::new();
264 map.insert("Result".into(), Dynamic::from(index));
265 Dynamic::from_map(map)
266 });
267 engine.register_fn("tag", |commands: &mut Array, id: rhai::ImmutableString| {
272 if let Some(last) = commands.last_mut() {
273 let command = std::mem::take(last);
274 let mut request = Map::new();
275 request.insert(TAG_ID_KEY.into(), Dynamic::from(id));
276 request.insert(TAG_COMMAND_KEY.into(), command);
277 *last = Dynamic::from_map(request);
278 }
279 });
280 crate::command::register_command_methods(engine);
281}
282
283fn register_mixed_number_ops(engine: &mut Engine) {
289 engine.register_fn("+", |a: i64, b: f64| a as f64 + b);
290 engine.register_fn("+", |a: f64, b: i64| a + b as f64);
291 engine.register_fn("-", |a: i64, b: f64| a as f64 - b);
292 engine.register_fn("-", |a: f64, b: i64| a - b as f64);
293 engine.register_fn("*", |a: i64, b: f64| a as f64 * b);
294 engine.register_fn("*", |a: f64, b: i64| a * b as f64);
295 engine.register_fn("/", |a: i64, b: f64| a as f64 / b);
296 engine.register_fn("/", |a: f64, b: i64| a / b as f64);
297 engine.register_fn("%", |a: i64, b: f64| a as f64 % b);
298 engine.register_fn("%", |a: f64, b: i64| a % b as f64);
299 engine.register_fn("<", |a: i64, b: f64| (a as f64) < b);
300 engine.register_fn("<", |a: f64, b: i64| a < b as f64);
301 engine.register_fn(">", |a: i64, b: f64| a as f64 > b);
302 engine.register_fn(">", |a: f64, b: i64| a > b as f64);
303 engine.register_fn("<=", |a: i64, b: f64| a as f64 <= b);
304 engine.register_fn("<=", |a: f64, b: i64| a <= b as f64);
305 engine.register_fn(">=", |a: i64, b: f64| a as f64 >= b);
306 engine.register_fn(">=", |a: f64, b: i64| a >= b as f64);
307}
308
309fn register_vector_helpers(engine: &mut Engine) {
318 fn axis(value: &Dynamic) -> f64 {
319 value
320 .as_float()
321 .ok()
322 .or_else(|| value.as_int().ok().map(|integer| integer as f64))
323 .unwrap_or(0.0)
324 }
325 fn components(vector: &Array) -> (f64, f64, f64) {
326 (
327 vector.first().map(axis).unwrap_or(0.0),
328 vector.get(1).map(axis).unwrap_or(0.0),
329 vector.get(2).map(axis).unwrap_or(0.0),
330 )
331 }
332 fn vector3(x: f64, y: f64, z: f64) -> Array {
333 vec![Dynamic::from(x), Dynamic::from(y), Dynamic::from(z)]
334 }
335
336 engine.register_fn("vec3", |x: f64, y: f64, z: f64| vector3(x, y, z));
337 engine.register_fn("dot", |a: Array, b: Array| {
338 let (ax, ay, az) = components(&a);
339 let (bx, by, bz) = components(&b);
340 ax * bx + ay * by + az * bz
341 });
342 engine.register_fn("cross", |a: Array, b: Array| {
343 let (ax, ay, az) = components(&a);
344 let (bx, by, bz) = components(&b);
345 vector3(ay * bz - az * by, az * bx - ax * bz, ax * by - ay * bx)
346 });
347 engine.register_fn("length", |v: Array| {
348 let (x, y, z) = components(&v);
349 (x * x + y * y + z * z).sqrt()
350 });
351 engine.register_fn("normalize", |v: Array| {
352 let (x, y, z) = components(&v);
353 let len = (x * x + y * y + z * z).sqrt();
354 if len > 1e-9 {
355 vector3(x / len, y / len, z / len)
356 } else {
357 vector3(0.0, 0.0, 0.0)
358 }
359 });
360 engine.register_fn("distance", |a: Array, b: Array| {
361 let (ax, ay, az) = components(&a);
362 let (bx, by, bz) = components(&b);
363 ((ax - bx).powi(2) + (ay - by).powi(2) + (az - bz).powi(2)).sqrt()
364 });
365 engine.register_fn("scaled", |v: Array, s: f64| {
366 let (x, y, z) = components(&v);
367 vector3(x * s, y * s, z * s)
368 });
369 engine.register_fn("vadd", |a: Array, b: Array| {
370 let (ax, ay, az) = components(&a);
371 let (bx, by, bz) = components(&b);
372 vector3(ax + bx, ay + by, az + bz)
373 });
374 engine.register_fn("vsub", |a: Array, b: Array| {
375 let (ax, ay, az) = components(&a);
376 let (bx, by, bz) = components(&b);
377 vector3(ax - bx, ay - by, az - bz)
378 });
379 engine.register_fn("direction", |from: Array, to: Array| {
380 let (ax, ay, az) = components(&from);
381 let (bx, by, bz) = components(&to);
382 let (dx, dy, dz) = (bx - ax, by - ay, bz - az);
383 let len = (dx * dx + dy * dy + dz * dz).sqrt();
384 if len > 1e-9 {
385 vector3(dx / len, dy / len, dz / len)
386 } else {
387 vector3(0.0, 0.0, 0.0)
388 }
389 });
390 engine.register_fn("lerp", |a: f64, b: f64, t: f64| a + (b - a) * t);
391 engine.register_fn("lerp", |a: Array, b: Array, t: f64| {
392 let (ax, ay, az) = components(&a);
393 let (bx, by, bz) = components(&b);
394 vector3(ax + (bx - ax) * t, ay + (by - ay) * t, az + (bz - az) * t)
395 });
396 engine.register_fn("clamp", |x: f64, lo: f64, hi: f64| x.max(lo).min(hi));
397}
398
399fn register_color_helpers(engine: &mut Engine) {
400 engine.register_fn("rgb", |r: f64, g: f64, b: f64| -> Array {
401 vec![
402 Dynamic::from(r),
403 Dynamic::from(g),
404 Dynamic::from(b),
405 Dynamic::from(1.0_f64),
406 ]
407 });
408 engine.register_fn("rgba", |r: f64, g: f64, b: f64, a: f64| -> Array {
409 vec![
410 Dynamic::from(r),
411 Dynamic::from(g),
412 Dynamic::from(b),
413 Dynamic::from(a),
414 ]
415 });
416 engine.register_fn("hsv", |h: f64, s: f64, v: f64| -> Array {
417 let (r, g, b) = hsv_to_rgb(h, s, v);
418 vec![
419 Dynamic::from(r),
420 Dynamic::from(g),
421 Dynamic::from(b),
422 Dynamic::from(1.0_f64),
423 ]
424 });
425}
426
427fn hsv_to_rgb(hue: f64, saturation: f64, value: f64) -> (f64, f64, f64) {
430 let sector = hue.rem_euclid(1.0) * 6.0;
431 let chroma = value * saturation;
432 let secondary = chroma * (1.0 - (sector % 2.0 - 1.0).abs());
433 let base = value - chroma;
434 let (red, green, blue) = match sector as i64 {
435 0 => (chroma, secondary, 0.0),
436 1 => (secondary, chroma, 0.0),
437 2 => (0.0, chroma, secondary),
438 3 => (0.0, secondary, chroma),
439 4 => (secondary, 0.0, chroma),
440 _ => (chroma, 0.0, secondary),
441 };
442 (red + base, green + base, blue + base)
443}
444
445fn ast_defines_hook(ast: &AST, name: &str) -> bool {
448 ast.iter_functions()
449 .any(|function| function.name == name && function.params.is_empty())
450}
451
452fn ast_defines_hook_arg(ast: &AST, name: &str, params: usize) -> bool {
455 ast.iter_functions()
456 .any(|function| function.name == name && function.params.len() == params)
457}
458
459fn reply_to_dynamic(reply: &CommandReply) -> Dynamic {
464 match reply {
465 CommandReply::None => Dynamic::UNIT,
466 CommandReply::Entity(entity) => Dynamic::from(entity_to_script(*entity)),
467 CommandReply::Bool(value) => Dynamic::from(*value),
468 CommandReply::Float(value) => Dynamic::from(*value as f64),
469 CommandReply::Int(value) => Dynamic::from(*value),
470 CommandReply::Text(value) => Dynamic::from(value.clone()),
471 CommandReply::Vector(value) => {
472 Dynamic::from_array(array3(Vec3::new(value[0], value[1], value[2])))
473 }
474 CommandReply::Entities(entities) => Dynamic::from_array(
475 entities
476 .iter()
477 .map(|entity| Dynamic::from(entity_to_script(*entity)))
478 .collect(),
479 ),
480 CommandReply::Strings(strings) => Dynamic::from_array(
481 strings
482 .iter()
483 .map(|value| Dynamic::from(value.clone()))
484 .collect(),
485 ),
486 CommandReply::Bytes(bytes) => Dynamic::from_blob(bytes.clone()),
487 CommandReply::Json(value) => rhai::serde::to_dynamic(value).unwrap_or(Dynamic::UNIT),
488 CommandReply::Error(message) => {
489 let mut map = Map::new();
490 map.insert("error".into(), Dynamic::from(message.clone()));
491 Dynamic::from_map(map)
492 }
493 }
494}
495
496fn entity_to_script(entity: Entity) -> i64 {
499 (((entity.id as u64) << 32) | entity.generation as u64) as i64
500}
501
502fn script_to_entity(value: i64) -> Entity {
504 let bits = value as u64;
505 Entity {
506 id: (bits >> 32) as u32,
507 generation: bits as u32,
508 }
509}
510
511fn hash_source(source: &str) -> u64 {
512 use std::hash::{Hash, Hasher};
513 let mut hasher = std::collections::hash_map::DefaultHasher::new();
514 source.hash(&mut hasher);
515 hasher.finish()
516}
517
518fn compiled_ast<'a>(
519 runtime: &'a mut ScriptRuntime,
520 source: &str,
521 errors: &mut Vec<String>,
522) -> Option<&'a AST> {
523 let key = hash_source(source);
524 if !runtime.compiled.contains_key(&key) {
525 match runtime.engine.compile(source) {
526 Ok(ast) => {
527 runtime.compiled.insert(key, ast);
528 }
529 Err(error) => {
530 tracing::error!("script compile error: {error}");
531 errors.push(format!("compile error: {error}"));
532 return None;
533 }
534 }
535 }
536 runtime.compiled.get(&key)
537}
538
539pub fn script_runtime_forget_entity(runtime: &mut ScriptRuntime, entity: Entity) {
541 runtime.scopes.remove(&entity);
542 runtime.started_entities.remove(&entity);
543}
544
545pub fn script_runtime_register_module(
552 runtime: &mut ScriptRuntime,
553 name: &str,
554 source: &str,
555) -> Result<(), String> {
556 let ast = runtime
557 .engine
558 .compile(source)
559 .map_err(|error| error.to_string())?;
560 let module = rhai::Module::eval_ast_as_new(Scope::new(), &ast, &runtime.engine)
561 .map_err(|error| error.to_string())?;
562 runtime.modules.insert(name, module);
563 Ok(())
564}
565
566pub fn script_check(source: &str) -> Result<(), String> {
567 new_engine(&ScriptModules::default())
568 .compile(source)
569 .map(|_| ())
570 .map_err(|error| error.to_string())
571}
572
573pub fn script_runtime_set_max_operations(runtime: &mut ScriptRuntime, max: u64) {
577 runtime.engine.set_max_operations(max);
578}
579
580pub fn script_runtime_set_max_array_size(runtime: &mut ScriptRuntime, max: usize) {
583 runtime.engine.set_max_array_size(max);
584}
585
586pub fn script_runtime_set_asset(runtime: &mut ScriptRuntime, name: &str, bytes: Vec<u8>) {
590 runtime.assets.insert(name.to_string(), bytes);
591}
592
593pub fn script_runtime_reset(runtime: &mut ScriptRuntime) {
596 runtime.compiled.clear();
597 runtime.scopes.clear();
598 runtime.global_scopes.clear();
599 runtime.started_globals.clear();
600 runtime.started_entities.clear();
601 runtime.next_replies.clear();
602 runtime.random_state = RANDOM_SEED;
603}
604
605pub fn run_scripts(world: &mut World, runtime: &mut ScriptRuntime) -> ScriptReport {
610 SCRIPT_RNG.with(|cell| cell.set(runtime.random_state));
611 SCRIPT_LOGS.with(|logs| logs.borrow_mut().clear());
612
613 let frame_events: Vec<Event> = events(world).to_vec();
614
615 for event in &frame_events {
616 if let Event::Despawned { entity } = event {
617 script_runtime_forget_entity(runtime, *entity);
618 }
619 }
620
621 let policy = runtime.policy.clone();
622 let dt = world.resources.window.timing.delta_time as f64;
623 let time = world.resources.window.timing.uptime_milliseconds as f64 / 1000.0;
624
625 #[cfg(feature = "gamepad")]
628 let gamepad = {
629 let left = crate::input::gamepad_left_stick(world);
630 let right = crate::input::gamepad_right_stick(world);
631 let connected = crate::input::gamepad_connected(world);
632 let mut map = Map::new();
633 map.insert(
634 "left_stick".into(),
635 Dynamic::from_array(vec![
636 Dynamic::from(left.x as f64),
637 Dynamic::from(left.y as f64),
638 ]),
639 );
640 map.insert(
641 "right_stick".into(),
642 Dynamic::from_array(vec![
643 Dynamic::from(right.x as f64),
644 Dynamic::from(right.y as f64),
645 ]),
646 );
647 map.insert("connected".into(), Dynamic::from(connected));
648 map
649 };
650 #[cfg(not(feature = "gamepad"))]
651 let gamepad = Map::new();
652
653 let keyboard = &world.resources.input.keyboard;
654 let move_left =
655 keyboard.is_key_pressed(KeyCode::ArrowLeft) || keyboard.is_key_pressed(KeyCode::KeyA);
656 let move_right =
657 keyboard.is_key_pressed(KeyCode::ArrowRight) || keyboard.is_key_pressed(KeyCode::KeyD);
658 let launch = keyboard.is_key_pressed(KeyCode::Space);
659
660 let mut keys = Map::new();
665 for keycode in keyboard.keystates.keys() {
666 if keyboard.is_key_pressed(*keycode) {
667 keys.insert(format!("{keycode:?}").into(), Dynamic::from(true));
668 }
669 }
670 let mut pressed = Map::new();
671 for keycode in &keyboard.just_pressed_keys {
672 pressed.insert(format!("{keycode:?}").into(), Dynamic::from(true));
673 }
674 let mouse = &world.resources.input.mouse;
675 let mut mouse_map = Map::new();
676 mouse_map.insert("x".into(), Dynamic::from(mouse.position.x as f64));
677 mouse_map.insert("y".into(), Dynamic::from(mouse.position.y as f64));
678 mouse_map.insert(
679 "left".into(),
680 Dynamic::from(mouse.state.contains(MouseState::LEFT_CLICKED)),
681 );
682 mouse_map.insert(
683 "right".into(),
684 Dynamic::from(mouse.state.contains(MouseState::RIGHT_CLICKED)),
685 );
686 mouse_map.insert(
687 "middle".into(),
688 Dynamic::from(mouse.state.contains(MouseState::MIDDLE_CLICKED)),
689 );
690 mouse_map.insert(
691 "left_pressed".into(),
692 Dynamic::from(mouse.state.contains(MouseState::LEFT_JUST_PRESSED)),
693 );
694 mouse_map.insert(
695 "right_pressed".into(),
696 Dynamic::from(mouse.state.contains(MouseState::RIGHT_JUST_PRESSED)),
697 );
698 mouse_map.insert("scroll".into(), Dynamic::from(mouse.wheel_delta.y as f64));
699 mouse_map.insert("dx".into(), Dynamic::from(mouse.position_delta.x as f64));
700 mouse_map.insert("dy".into(), Dynamic::from(mouse.position_delta.y as f64));
701
702 let touch = &world.resources.input.touch;
705 let mut touch_list = Array::new();
706 for point in touch.touches.values() {
707 let mut entry = Map::new();
708 entry.insert("id".into(), Dynamic::from(point.id as i64));
709 entry.insert("x".into(), Dynamic::from(point.position.x as f64));
710 entry.insert("y".into(), Dynamic::from(point.position.y as f64));
711 touch_list.push(Dynamic::from_map(entry));
712 }
713 let touch_count = touch_list.len() as i64;
714
715 #[cfg(feature = "picking")]
718 let ground_hit = nightshade::prelude::get_ground_position_from_screen(
719 world,
720 world.resources.input.mouse.position,
721 0.0,
722 );
723 #[cfg(not(feature = "picking"))]
724 let ground_hit: Option<Vec3> = None;
725 let ground = match ground_hit {
726 Some(point) => array3(point),
727 None => Array::new(),
728 };
729 let pointer_over_ui = world
730 .resources
731 .retained_ui
732 .interaction
733 .hovered_entity
734 .is_some();
735
736 let mut named = Map::new();
737 let mut positions = Map::new();
738 let mut velocities = Map::new();
739 for (name, entity) in &world.resources.entities.names {
740 named.insert(
741 name.as_str().into(),
742 Dynamic::from(entity_to_script(*entity)),
743 );
744 let (position, velocity) = entity_pos_vel(world, *entity);
745 positions.insert(name.as_str().into(), Dynamic::from(position));
746 velocities.insert(name.as_str().into(), Dynamic::from(velocity));
747 }
748
749 let mut tag_lists: HashMap<String, Array> = HashMap::new();
752 for (entity, tags) in &world.resources.entities.tags {
753 let (position, velocity) = entity_pos_vel(world, *entity);
754 let mut record = Map::new();
755 record.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
756 record.insert("pos".into(), Dynamic::from(position));
757 record.insert("vel".into(), Dynamic::from(velocity));
758 let record = Dynamic::from_map(record);
759 for tag in tags {
760 tag_lists
761 .entry(tag.clone())
762 .or_default()
763 .push(record.clone());
764 }
765 }
766 let mut tagged = Map::new();
767 for (tag, list) in tag_lists {
768 tagged.insert(tag.as_str().into(), Dynamic::from(list));
769 }
770
771 let mut widgets = Map::new();
775 for (entity, tags) in &world.resources.entities.tags {
776 if let Some(value) = widget_value(world, *entity) {
777 for tag in tags {
778 widgets.insert(tag.as_str().into(), value.clone());
779 }
780 }
781 }
782 #[cfg(feature = "picking")]
783 let picked = match crate::picking::clicked_entity(world) {
784 Some(entity) => Dynamic::from(entity_to_script(entity)),
785 None => Dynamic::UNIT,
786 };
787 #[cfg(not(feature = "picking"))]
788 let picked = Dynamic::UNIT;
789 let mut camera = Map::new();
790 camera.insert(
791 "pos".into(),
792 Dynamic::from_array(array3(crate::camera::camera_position(world))),
793 );
794 camera.insert(
795 "forward".into(),
796 Dynamic::from_array(array3(crate::camera::camera_forward(world))),
797 );
798
799 let mut produced: Vec<Command> = Vec::new();
800 let mut result_tags: Vec<(usize, String)> = Vec::new();
801 let mut errors: Vec<String> = Vec::new();
802 let replies = std::mem::take(&mut runtime.next_replies);
803
804 let global_scripts: Vec<(String, String)> = world
805 .resources
806 .global_scripts
807 .entries
808 .iter()
809 .filter(|script| script.enabled && !script.source.trim().is_empty())
810 .map(|script| (script.name.clone(), script.source.clone()))
811 .collect();
812
813 for (name, source) in &global_scripts {
814 let Some(ast) = compiled_ast(runtime, source, &mut errors).cloned() else {
815 continue;
816 };
817 let run_start =
818 !runtime.started_globals.contains(name) && ast_defines_hook(&ast, "on_start");
819 runtime.started_globals.insert(name.clone());
820 let scope = runtime.global_scopes.entry(name.clone()).or_default();
821 if !scope.contains("state") {
822 scope.set_value("state", Map::new());
823 }
824 if !scope.contains("assets") {
825 let mut assets = Map::new();
826 for (name, bytes) in &runtime.assets {
827 assets.insert(name.as_str().into(), Dynamic::from_blob(bytes.clone()));
828 }
829 scope.set_value("assets", assets);
830 }
831 scope.set_value("dt", dt);
832 scope.set_value("time", time);
833 scope.set_value("tau", std::f64::consts::TAU);
834 scope.set_value("pi", std::f64::consts::PI);
835 scope.set_value("replies", replies.clone());
836 scope.set_value("move_left", move_left);
837 scope.set_value("move_right", move_right);
838 scope.set_value("launch", launch);
839 scope.set_value("keys", keys.clone());
840 scope.set_value("pressed", pressed.clone());
841 scope.set_value("mouse", mouse_map.clone());
842 scope.set_value("gamepad", gamepad.clone());
843 scope.set_value("touches", touch_list.clone());
844 scope.set_value("touch_count", touch_count);
845 scope.set_value("named", named.clone());
846 scope.set_value("positions", positions.clone());
847 scope.set_value("velocities", velocities.clone());
848 scope.set_value("tagged", tagged.clone());
849 scope.set_value("widgets", widgets.clone());
850 scope.set_value("picked", picked.clone());
851 scope.set_value("camera", camera.clone());
852 scope.set_value("ground", ground.clone());
853 scope.set_value("pointer_over_ui", pointer_over_ui);
854 scope.set_value("events", events_array(&frame_events));
855 scope.set_value("commands", Array::new());
856
857 if run_start {
858 if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_start", ()) {
859 tracing::warn!("global script '{name}' on_start error: {error}");
860 errors.push(format!("on_start error: {error}"));
861 } else {
862 collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
863 }
864 scope.set_value("commands", Array::new());
865 }
866
867 if ast_defines_hook(&ast, "on_tick") {
868 if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_tick", ()) {
869 tracing::warn!("global script '{name}' on_tick error: {error}");
870 errors.push(format!("on_tick error: {error}"));
871 continue;
872 }
873 collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
874 }
875
876 if ast_defines_hook_arg(&ast, "on_collision", 1) {
877 for event in &frame_events {
878 if !matches!(event, Event::Collision { .. }) {
879 continue;
880 }
881 scope.set_value("commands", Array::new());
882 if let Err(error) = runtime.engine.call_fn::<()>(
883 scope,
884 &ast,
885 "on_collision",
886 (event_to_map(event),),
887 ) {
888 errors.push(format!("on_collision error: {error}"));
889 } else {
890 collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
891 }
892 }
893 }
894 }
895
896 let mut scripted: Vec<(Entity, String)> = Vec::new();
897 for entity in world.ecs.worlds[CORE]
898 .query_entities(SCRIPT)
899 .collect::<Vec<_>>()
900 {
901 if let Some(script) = world.get::<nightshade::ecs::script::components::Script>(entity)
902 && script.enabled
903 && let ScriptSource::Embedded { source } = &script.source
904 {
905 scripted.push((entity, source.clone()));
906 }
907 }
908
909 for (entity, source) in &scripted {
910 let position = world
911 .get::<nightshade::ecs::transform::components::LocalTransform>(*entity)
912 .map(|transform| transform.translation)
913 .unwrap_or_else(Vec3::zeros);
914 let velocity = nightshade::ecs::physics::resources::physics_world_linear_velocity(
915 &world.resources.physics,
916 *entity,
917 )
918 .unwrap_or_else(Vec3::zeros);
919 let entity_events = relevant_events(&frame_events, *entity);
920
921 let Some(ast) = compiled_ast(runtime, source, &mut errors).cloned() else {
922 continue;
923 };
924 let run_start =
925 !runtime.started_entities.contains(entity) && ast_defines_hook(&ast, "on_start");
926 runtime.started_entities.insert(*entity);
927 let scope = runtime.scopes.entry(*entity).or_default();
928 if !scope.contains("state") {
929 scope.set_value("state", Map::new());
930 }
931 if !scope.contains("assets") {
932 let mut assets = Map::new();
933 for (name, bytes) in &runtime.assets {
934 assets.insert(name.as_str().into(), Dynamic::from_blob(bytes.clone()));
935 }
936 scope.set_value("assets", assets);
937 }
938 scope.set_value("self", entity_to_script(*entity));
939 scope.set_value("dt", dt);
940 scope.set_value("time", time);
941 scope.set_value("tau", std::f64::consts::TAU);
942 scope.set_value("pi", std::f64::consts::PI);
943 scope.set_value("replies", replies.clone());
944 scope.set_value("move_left", move_left);
945 scope.set_value("move_right", move_right);
946 scope.set_value("launch", launch);
947 scope.set_value("keys", keys.clone());
948 scope.set_value("pressed", pressed.clone());
949 scope.set_value("mouse", mouse_map.clone());
950 scope.set_value("gamepad", gamepad.clone());
951 scope.set_value("touches", touch_list.clone());
952 scope.set_value("touch_count", touch_count);
953 scope.set_value("pos", array3(position));
954 scope.set_value("vel", array3(velocity));
955 scope.set_value("named", named.clone());
956 scope.set_value("positions", positions.clone());
957 scope.set_value("velocities", velocities.clone());
958 scope.set_value("tagged", tagged.clone());
959 scope.set_value("widgets", widgets.clone());
960 scope.set_value("picked", picked.clone());
961 scope.set_value("camera", camera.clone());
962 scope.set_value("ground", ground.clone());
963 scope.set_value("pointer_over_ui", pointer_over_ui);
964 scope.set_value("events", events_array(&entity_events));
965 scope.set_value("commands", Array::new());
966
967 if run_start {
968 if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_start", ()) {
969 tracing::warn!("script on_start error: {error}");
970 errors.push(format!("on_start error: {error}"));
971 } else {
972 collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
973 }
974 scope.set_value("commands", Array::new());
975 }
976
977 if ast_defines_hook(&ast, "on_tick") {
978 if let Err(error) = runtime.engine.call_fn::<()>(scope, &ast, "on_tick", ()) {
979 tracing::warn!("script on_tick error: {error}");
980 errors.push(format!("on_tick error: {error}"));
981 continue;
982 }
983 collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
984 }
985
986 if ast_defines_hook_arg(&ast, "on_collision", 1) {
987 for event in &entity_events {
988 if !matches!(event, Event::Collision { .. }) {
989 continue;
990 }
991 scope.set_value("commands", Array::new());
992 if let Err(error) = runtime.engine.call_fn::<()>(
993 scope,
994 &ast,
995 "on_collision",
996 (event_to_map(event),),
997 ) {
998 errors.push(format!("on_collision error: {error}"));
999 } else {
1000 collect_commands(scope, &mut produced, &mut result_tags, &mut errors, &policy);
1001 }
1002 }
1003 }
1004 }
1005
1006 let replies_out = submit_commands(world, &produced);
1007 let mut next_replies = Map::new();
1008 for (index, id) in &result_tags {
1009 if let Some(reply) = replies_out.get(*index) {
1010 next_replies.insert(id.as_str().into(), reply_to_dynamic(reply));
1011 }
1012 }
1013 runtime.next_replies = next_replies;
1014 runtime.random_state = SCRIPT_RNG.with(|cell| cell.get());
1015 let logs = SCRIPT_LOGS.with(|logs| std::mem::take(&mut *logs.borrow_mut()));
1016 ScriptReport {
1017 commands: produced,
1018 errors,
1019 logs,
1020 }
1021}
1022
1023fn collect_commands(
1028 scope: &Scope<'static>,
1029 produced: &mut Vec<Command>,
1030 tags: &mut Vec<(usize, String)>,
1031 errors: &mut Vec<String>,
1032 policy: &CommandPolicy,
1033) {
1034 let Some(commands) = scope
1035 .get("commands")
1036 .and_then(|value| value.read_lock::<Array>())
1037 else {
1038 return;
1039 };
1040 for entry in commands.iter() {
1041 let is_tagged = entry
1042 .read_lock::<Map>()
1043 .is_some_and(|map| map.contains_key(TAG_COMMAND_KEY));
1044 let parsed = if is_tagged {
1045 let request = entry.read_lock::<Map>().unwrap();
1046 let id = request
1047 .get(TAG_ID_KEY)
1048 .and_then(|value| value.clone().into_string().ok());
1049 match request.get(TAG_COMMAND_KEY) {
1050 Some(command) => {
1051 crate::dynamic_de::from_dynamic::<Command>(command).map(|c| (c, id))
1052 }
1053 None => continue,
1054 }
1055 } else {
1056 crate::dynamic_de::from_dynamic::<Command>(entry).map(|c| (c, None))
1057 };
1058 match parsed {
1059 Ok((command, id)) if policy.allows(command.name()) => {
1060 if let Some(id) = id {
1061 tags.push((produced.len(), id));
1062 }
1063 produced.push(command);
1064 }
1065 Ok((command, _)) => {
1066 tracing::warn!(
1067 "script command '{}' blocked by write policy",
1068 command.name()
1069 );
1070 errors.push(format!(
1071 "command '{}' blocked by write policy",
1072 command.name()
1073 ));
1074 }
1075 Err(error) => {
1076 tracing::warn!("dropped malformed script command: {error}");
1077 errors.push(format!("dropped command: {error}"));
1078 }
1079 }
1080 }
1081}
1082
1083fn entity_pos_vel(world: &World, entity: Entity) -> (Array, Array) {
1086 let position = world
1087 .get::<nightshade::ecs::transform::components::LocalTransform>(entity)
1088 .map(|transform| transform.translation)
1089 .unwrap_or_else(Vec3::zeros);
1090 let velocity = nightshade::ecs::physics::resources::physics_world_linear_velocity(
1091 &world.resources.physics,
1092 entity,
1093 )
1094 .unwrap_or_else(Vec3::zeros);
1095 (array3(position), array3(velocity))
1096}
1097
1098fn array3(value: Vec3) -> Array {
1099 vec![
1100 Dynamic::from(value.x as f64),
1101 Dynamic::from(value.y as f64),
1102 Dynamic::from(value.z as f64),
1103 ]
1104}
1105
1106fn array4(value: nightshade::prelude::Vec4) -> Array {
1107 vec![
1108 Dynamic::from(value.x as f64),
1109 Dynamic::from(value.y as f64),
1110 Dynamic::from(value.z as f64),
1111 Dynamic::from(value.w as f64),
1112 ]
1113}
1114
1115fn widget_value(world: &World, entity: Entity) -> Option<Dynamic> {
1121 use nightshade::prelude::{
1122 ui_checkbox_value, ui_collapsing_header_open, ui_color_picker_color, ui_drag_value,
1123 ui_dropdown_selected, ui_slider_value, ui_tab_bar_selected, ui_text_input_text,
1124 ui_toggle_value,
1125 };
1126 if let Some(value) = ui_slider_value(world, entity) {
1127 return Some(Dynamic::from(value as f64));
1128 }
1129 if let Some(value) = ui_drag_value(world, entity) {
1130 return Some(Dynamic::from(value as f64));
1131 }
1132 if let Some(value) = ui_toggle_value(world, entity) {
1133 return Some(Dynamic::from(value));
1134 }
1135 if let Some(value) = ui_checkbox_value(world, entity) {
1136 return Some(Dynamic::from(value));
1137 }
1138 if let Some(value) = ui_dropdown_selected(world, entity) {
1139 return Some(Dynamic::from(value as i64));
1140 }
1141 if let Some(value) = ui_tab_bar_selected(world, entity) {
1142 return Some(Dynamic::from(value as i64));
1143 }
1144 if let Some(value) = ui_collapsing_header_open(world, entity) {
1145 return Some(Dynamic::from(value));
1146 }
1147 if let Some(value) = ui_color_picker_color(world, entity) {
1148 return Some(Dynamic::from_array(array4(value)));
1149 }
1150 if let Some(value) = ui_text_input_text(world, entity) {
1151 return Some(Dynamic::from(value.to_string()));
1152 }
1153 None
1154}
1155
1156fn relevant_events(frame_events: &[Event], entity: Entity) -> Vec<Event> {
1159 frame_events
1160 .iter()
1161 .filter(|event| match event {
1162 Event::Collision { a, b, .. } => *a == entity || *b == entity,
1163 Event::Despawned { entity: target }
1164 | Event::AnimationFinished { entity: target }
1165 | Event::AnimationEvent { entity: target, .. }
1166 | Event::NavigationArrived { entity: target } => *target == entity,
1167 })
1168 .cloned()
1169 .collect()
1170}
1171
1172fn events_array(frame_events: &[Event]) -> Array {
1173 frame_events.iter().map(event_to_map).collect()
1174}
1175
1176fn event_to_map(event: &Event) -> Dynamic {
1177 let mut map = Map::new();
1178 match event {
1179 Event::Collision {
1180 a,
1181 b,
1182 sensor,
1183 started,
1184 } => {
1185 map.insert("kind".into(), Dynamic::from("collision".to_string()));
1186 map.insert("a".into(), Dynamic::from(entity_to_script(*a)));
1187 map.insert("b".into(), Dynamic::from(entity_to_script(*b)));
1188 map.insert("sensor".into(), Dynamic::from(*sensor));
1189 map.insert("started".into(), Dynamic::from(*started));
1190 }
1191 Event::Despawned { entity } => {
1192 map.insert("kind".into(), Dynamic::from("despawned".to_string()));
1193 map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
1194 }
1195 Event::AnimationFinished { entity } => {
1196 map.insert(
1197 "kind".into(),
1198 Dynamic::from("animation_finished".to_string()),
1199 );
1200 map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
1201 }
1202 Event::AnimationEvent { entity, name } => {
1203 map.insert("kind".into(), Dynamic::from("animation_event".to_string()));
1204 map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
1205 map.insert("name".into(), Dynamic::from(name.clone()));
1206 }
1207 Event::NavigationArrived { entity } => {
1208 map.insert(
1209 "kind".into(),
1210 Dynamic::from("navigation_arrived".to_string()),
1211 );
1212 map.insert("entity".into(), Dynamic::from(entity_to_script(*entity)));
1213 }
1214 }
1215 Dynamic::from_map(map)
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use super::{ScriptModules, new_engine};
1221
1222 #[test]
1223 fn mixed_integer_and_float_arithmetic_works() {
1224 let engine = new_engine(&ScriptModules::default());
1225 assert_eq!(engine.eval::<f64>("3 * 1.5").unwrap(), 4.5);
1226 assert_eq!(engine.eval::<f64>("2.0 + 3").unwrap(), 5.0);
1227 assert_eq!(engine.eval::<f64>("7 / 2.0").unwrap(), 3.5);
1228 assert!(engine.eval::<bool>("2 < 2.5").unwrap());
1229 assert!(engine.eval::<bool>("3.0 >= 3").unwrap());
1230 }
1231
1232 #[test]
1233 fn same_type_integer_division_stays_integer() {
1234 let engine = new_engine(&ScriptModules::default());
1235 assert_eq!(engine.eval::<i64>("7 / 2").unwrap(), 3);
1236 }
1237
1238 #[test]
1239 fn closures_capture_and_mutate_outer_state() {
1240 let engine = new_engine(&ScriptModules::default());
1241 let result = engine
1242 .eval::<i64>(
1243 r#"
1244 let state = #{ n: 0 };
1245 let bump = || { state.n += 1; };
1246 bump.call();
1247 bump.call();
1248 state.n
1249 "#,
1250 )
1251 .unwrap();
1252 assert_eq!(result, 2);
1253 }
1254
1255 #[test]
1256 fn color_helpers_build_four_channel_arrays() {
1257 let engine = new_engine(&ScriptModules::default());
1258 assert_eq!(
1259 engine
1260 .eval::<rhai::Array>("rgb(0.5, 0.25, 0.1)")
1261 .unwrap()
1262 .len(),
1263 4
1264 );
1265 assert_eq!(
1266 engine
1267 .eval::<rhai::Array>("rgba(0.1, 0.2, 0.3, 0.4)")
1268 .unwrap()
1269 .len(),
1270 4
1271 );
1272 assert_eq!(
1273 engine
1274 .eval::<rhai::Array>("hsv(0.0, 1.0, 1.0)")
1275 .unwrap()
1276 .len(),
1277 4
1278 );
1279 }
1280}