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