1use crate::components::*;
9use crate::hierarchy;
10use crate::input::{Input, Key, MouseButton};
11use crate::physics::{Physics, QueryOptions, RayHit};
12use crate::reflect::{apply_props, apply_props_checked, read_props, Field};
13use crate::scene::Scene;
14use glam::Vec3;
15use kvas_ecs::{Component as EcsComponent, Entity, World};
16use std::collections::HashSet;
17use std::panic::{catch_unwind, AssertUnwindSafe};
18use std::path::{Path, PathBuf};
19use std::sync::atomic::{AtomicUsize, Ordering};
20use std::sync::Arc;
21
22#[derive(Clone, Copy, Default)]
24pub struct Time {
25 pub dt: f32,
27 pub elapsed: f32,
29 pub frame: u64,
31}
32
33pub trait Script {
36 fn start(&mut self, _ctx: &mut Ctx) {}
39 fn update(&mut self, _ctx: &mut Ctx) {}
41 fn fixed_update(&mut self, _ctx: &mut Ctx) {}
43 fn fields(&mut self) -> Vec<Field<'_>> {
45 Vec::new()
46 }
47}
48
49pub struct ScriptDef {
51 pub name: &'static str,
53 pub create: fn() -> Box<dyn Script>,
55 pub update: bool,
57 pub fixed: bool,
59}
60
61impl ScriptDef {
62 pub fn of<T: Script + Default + 'static>(name: &'static str) -> Self {
66 Self { name, create: || Box::new(T::default()), update: true, fixed: true }
67 }
68}
69
70pub struct ScriptLib {
72 pub abi: AbiInfo,
74 pub scripts: Vec<ScriptDef>,
76}
77
78const ABI_VERSION: u32 = 3;
79
80fn fnv(text: &str) -> u64 {
81 text.bytes().fold(0xcbf2_9ce4_8422_2325u64, |h, b| (h ^ b as u64).wrapping_mul(0x0100_0000_01b3))
82}
83
84#[derive(Clone, Copy, PartialEq, Eq, Debug)]
87#[repr(C)]
88pub struct AbiInfo {
89 version: u32,
90 rustc: u64,
91 sources: u64,
92 layouts: [usize; 8],
93}
94
95impl AbiInfo {
96 pub fn current() -> Self {
99 use std::mem::{align_of, size_of};
100 Self {
101 version: ABI_VERSION,
102 rustc: fnv(env!("KVAS_RUSTC_VERSION")),
103 sources: fnv(&format!("{}|{}", env!("KVAS_SRC_HASH"), env!("KVAS_BUILD_CONFIG"))),
104 layouts: [
105 size_of::<World>(),
106 size_of::<Scene>(),
107 size_of::<Ctx>(),
108 size_of::<Transform>(),
109 size_of::<glam::Mat4>() * 16 + align_of::<glam::Mat4>(),
110 size_of::<glam::Quat>() * 16 + align_of::<glam::Quat>(),
111 size_of::<RigidBody>(),
112 [
115 size_of::<Scripts>(),
116 align_of::<Scripts>(),
117 size_of::<Input>(),
118 align_of::<Input>(),
119 size_of::<serde_json::Value>(),
120 align_of::<serde_json::Value>(),
121 size_of::<crate::reflect::FieldRef<'static>>(),
122 size_of::<crate::mesh::MeshData>(),
123 align_of::<crate::mesh::MeshData>(),
124 size_of::<QueryOptions>(),
125 ]
126 .into_iter()
127 .fold(0xcbf2_9ce4_8422_2325u64, |hash, value| (hash ^ value as u64).wrapping_mul(0x0100_0000_01b3))
128 as usize,
129 ],
130 }
131 }
132}
133
134pub fn short_name(path: &'static str) -> &'static str {
136 path.rsplit("::").next().unwrap_or(path).trim()
137}
138
139#[macro_export]
141macro_rules! register_scripts {
142 (@flags $($t:ty => $u:expr, $f:expr);* $(;)?) => {
144 #[no_mangle]
146 pub extern "C" fn kvas_script_abi() -> $crate::script::AbiInfo {
147 $crate::script::AbiInfo::current()
148 }
149 #[no_mangle]
151 pub fn kvas_script_lib() -> $crate::script::ScriptLib {
152 $crate::script::ScriptLib {
153 abi: $crate::script::AbiInfo::current(),
154 scripts: vec![$($crate::script::ScriptDef {
155 name: $crate::script::short_name(stringify!($t)),
156 create: || Box::new(<$t as Default>::default()),
157 update: $u,
158 fixed: $f,
159 }),*],
160 }
161 }
162 };
163 ($($t:ty),* $(,)?) => {
164 #[no_mangle]
166 pub extern "C" fn kvas_script_abi() -> $crate::script::AbiInfo {
167 $crate::script::AbiInfo::current()
168 }
169 #[no_mangle]
171 pub fn kvas_script_lib() -> $crate::script::ScriptLib {
172 $crate::script::ScriptLib {
173 abi: $crate::script::AbiInfo::current(),
174 scripts: vec![$($crate::script::ScriptDef {
175 name: $crate::script::short_name(stringify!($t)),
176 create: || Box::new(<$t as Default>::default()),
177 update: true,
178 fixed: true,
179 }),*],
180 }
181 }
182 };
183}
184
185pub struct Ctx<'a> {
187 pub entity: Entity,
189 pub scene: &'a mut Scene,
191 pub input: &'a Input,
193 pub time: Time,
195 physics: &'a Physics,
196 raycast_fn: fn(&Physics, Vec3, Vec3, f32, QueryOptions) -> Option<RayHit>,
198 overlap_fn: fn(&Physics, Vec3, f32) -> Vec<Entity>,
199 touching_fn: fn(&Physics, Entity) -> Vec<Entity>,
200 overlap_filtered_fn: fn(&Physics, Vec3, f32, QueryOptions) -> Vec<Entity>,
201 log: &'a mut Vec<String>,
202}
203
204impl Ctx<'_> {
205 pub fn dt(&self) -> f32 {
207 self.time.dt
208 }
209 pub fn elapsed(&self) -> f32 {
211 self.time.elapsed
212 }
213 pub fn frame(&self) -> u64 {
215 self.time.frame
216 }
217 pub fn fps(&self) -> f32 {
219 if self.time.dt > 0.0 { 1.0 / self.time.dt } else { 0.0 }
220 }
221 pub fn every(&self, seconds: f32) -> bool {
223 seconds > 0.0 && (self.time.elapsed % seconds) < self.time.dt
224 }
225
226 pub fn time_of_day(&self) -> f32 {
230 self.scene.settings.atmosphere.time
231 }
232 pub fn set_time_of_day(&mut self, hour: f32) {
234 let sky = &mut self.scene.settings.atmosphere;
235 sky.procedural = true;
236 sky.time = hour.rem_euclid(24.0);
237 }
238 pub fn set_day_speed(&mut self, hours_per_second: f32) {
240 self.scene.settings.atmosphere.speed = hours_per_second.max(0.0);
241 }
242 pub fn sky(&mut self) -> &mut crate::sky::SkySettings {
244 &mut self.scene.settings.atmosphere
245 }
246 pub fn set_clouds(&mut self, amount: f32) {
248 self.scene.settings.atmosphere.clouds = amount.clamp(0.0, 1.0);
249 }
250 pub fn is_night(&self) -> bool {
252 let h = self.scene.settings.atmosphere.time;
253 !(5.0..20.0).contains(&h)
254 }
255
256 pub fn post(&mut self) -> &mut crate::fx::PostSettings {
260 &mut self.scene.settings.post
261 }
262 pub fn shadows(&mut self) -> &mut crate::fx::ShadowSettings {
264 &mut self.scene.settings.shadows
265 }
266 pub fn post_preset(&mut self, name: &str) {
268 self.scene.settings.post = crate::fx::PostSettings::preset(name);
269 }
270 pub fn set_exposure(&mut self, exposure: f32) {
272 self.scene.settings.exposure = exposure.clamp(0.05, 8.0);
273 }
274
275 pub fn add_post_effect(&mut self, shader: &str) -> usize {
277 let list = &mut self.scene.settings.post.effects;
278 list.push(crate::fx::PostEffect { shader: shader.to_string(), ..Default::default() });
279 list.len() - 1
280 }
281 pub fn set_post_effect_params(&mut self, index: usize, params: [f32; 4]) {
283 if let Some(effect) = self.scene.settings.post.effects.get_mut(index) {
284 effect.params = params;
285 }
286 }
287 pub fn enable_post_effect(&mut self, index: usize, on: bool) {
289 if let Some(effect) = self.scene.settings.post.effects.get_mut(index) {
290 effect.enabled = on;
291 }
292 }
293 pub fn clear_post_effects(&mut self) {
295 self.scene.settings.post.effects.clear();
296 }
297
298 pub fn play_sound(&mut self, clip: &str, volume: f32) -> crate::audio::SoundId {
302 self.scene.audio.play(clip, volume)
303 }
304 pub fn play_music(&mut self, clip: &str, volume: f32) -> crate::audio::SoundId {
306 self.scene.audio.play_loop(clip, volume)
307 }
308 pub fn play_sound_at(&mut self, clip: &str, volume: f32, position: Vec3) -> crate::audio::SoundId {
310 self.scene.audio.play_at(clip, volume, position)
311 }
312 pub fn play_sound_panned(&mut self, clip: &str, volume: f32, pan: f32) -> crate::audio::SoundId {
314 self.scene.audio.play_panned(clip, volume, pan)
315 }
316 pub fn play_sound_pitched(&mut self, clip: &str, volume: f32, pitch: f32) -> crate::audio::SoundId {
318 self.scene.audio.play_pitched(clip, volume, pitch)
319 }
320 pub fn stop_sound(&mut self, id: crate::audio::SoundId) {
322 self.scene.audio.stop(id);
323 }
324 pub fn set_sound_volume(&mut self, id: crate::audio::SoundId, volume: f32) {
326 self.scene.audio.set_volume(id, volume);
327 }
328 pub fn stop_all_sounds(&mut self) {
330 self.scene.audio.stop_all();
331 }
332 pub fn set_master_volume(&mut self, volume: f32) {
334 self.scene.audio.master_volume = volume.clamp(0.0, 1.0);
335 }
336
337 pub fn world(&mut self) -> &mut World {
339 &mut self.scene.world
340 }
341 pub fn log(&mut self, msg: impl Into<String>) {
343 self.log.push(msg.into());
344 }
345
346 pub fn get<T: EcsComponent>(&self) -> Option<&T> {
348 self.scene.world.get::<T>(self.entity)
349 }
350 pub fn get_mut<T: EcsComponent>(&mut self) -> Option<&mut T> {
352 self.scene.world.get_mut::<T>(self.entity)
353 }
354 pub fn get_of<T: EcsComponent>(&self, e: Entity) -> Option<&T> {
356 self.scene.world.get::<T>(e)
357 }
358 pub fn get_mut_of<T: EcsComponent>(&mut self, e: Entity) -> Option<&mut T> {
360 self.scene.world.get_mut::<T>(e)
361 }
362
363 pub fn transform(&self) -> &Transform {
365 self.get::<Transform>().expect("entity has no Transform")
366 }
367 pub fn transform_mut(&mut self) -> &mut Transform {
371 self.get_mut::<Transform>().expect("entity has no Transform")
372 }
373 pub fn world_position(&self) -> Vec3 {
375 hierarchy::global_matrix(&self.scene.world, self.entity).w_axis.truncate()
376 }
377
378 pub fn set_color(&mut self, e: Entity, rgba: [f32; 4]) {
380 if let Some(m) = self.scene.world.get_mut::<MeshRenderer>(e) {
381 m.color = rgba;
382 }
383 }
384 pub fn set_texture(&mut self, e: Entity, texture: &str) {
386 if let Some(m) = self.scene.world.get_mut::<MeshRenderer>(e) {
387 m.texture = texture.to_string();
388 }
389 }
390 pub fn set_shape(&mut self, e: Entity, shape: Shape) {
392 self.scene.world.insert(e, shape);
393 if !self.scene.world.has::<MeshRenderer>(e) {
394 self.scene.world.insert(e, MeshRenderer::default());
395 }
396 }
397 pub fn children(&self, e: Entity) -> Vec<Entity> {
399 hierarchy::children(&self.scene.world, e)
400 }
401 pub fn find(&self, name: &str) -> Option<Entity> {
403 hierarchy::find_by_name(&self.scene.world, name)
404 }
405 pub fn find_all(&self, name: &str) -> Vec<Entity> {
407 self.scene.world.entities().into_iter().filter(|&e| hierarchy::name_of(&self.scene.world, e) == name).collect()
408 }
409 pub fn find_with<T: EcsComponent>(&self) -> Vec<Entity> {
411 self.scene.world.iter::<T>().map(|(e, _)| e).collect()
412 }
413 pub fn var(&self, entity: Entity, script: &str, name: &str) -> Option<serde_json::Value> {
416 self.scene.world.get::<Scripts>(entity)?.0.iter().find(|s| s.name == script)?.props.get(name).cloned()
417 }
418 pub fn set_var(&mut self, entity: Entity, script: &str, name: &str, value: serde_json::Value) -> bool {
421 match self.scene.world.get_mut::<Scripts>(entity).and_then(|s| s.0.iter_mut().find(|s| s.name == script)) {
422 Some(slot) => {
423 slot.props.insert(name.to_string(), value);
424 true
425 }
426 None => false,
427 }
428 }
429 pub fn var_f32(&self, entity: Entity, script: &str, name: &str) -> Option<f32> {
431 self.var(entity, script, name)?.as_f64().map(|v| v as f32)
432 }
433 pub fn var_bool(&self, entity: Entity, script: &str, name: &str) -> Option<bool> {
435 self.var(entity, script, name)?.as_bool()
436 }
437
438 pub fn spawn(&mut self, name: &str) -> Entity {
440 hierarchy::spawn_named(&mut self.scene.world, name)
441 }
442 pub fn spawn_primitive(&mut self, name: &str, primitive: &str, position: Vec3) -> Entity {
445 let e = self.spawn(name);
446 self.scene.world.insert(e, Transform::at(position));
447 self.scene.world.insert(e, MeshRenderer { mesh: format!("primitive:{primitive}"), ..Default::default() });
448 e
449 }
450 pub fn despawn(&mut self, e: Entity) {
452 hierarchy::despawn_recursive(&mut self.scene.world, e);
453 }
454
455 pub fn add_impulse(&mut self, impulse: Vec3) {
458 if let Some(rb) = self.get_mut::<RigidBody>() {
459 rb.impulse += impulse;
460 }
461 }
462 pub fn set_velocity(&mut self, v: Vec3) {
464 if let Some(rb) = self.get_mut::<RigidBody>() {
465 rb.set_velocity = Some(v);
466 }
467 }
468 pub fn set_angular_velocity_of(&mut self, e: Entity, w: Vec3) {
470 if let Some(rb) = self.get_mut_of::<RigidBody>(e) {
471 rb.set_angular_velocity = Some(w);
472 }
473 }
474 pub fn set_velocity_of(&mut self, e: Entity, v: Vec3) {
476 if let Some(rb) = self.get_mut_of::<RigidBody>(e) {
477 rb.set_velocity = Some(v);
478 }
479 }
480 pub fn velocity(&self) -> Vec3 {
482 self.get::<RigidBody>().map_or(Vec3::ZERO, |rb| rb.velocity)
483 }
484 pub fn raycast(&self, origin: Vec3, dir: Vec3, max_distance: f32) -> Option<RayHit> {
486 (self.raycast_fn)(self.physics, origin, dir, max_distance, QueryOptions { exclude: Some(self.entity), ..Default::default() })
487 }
488
489 pub fn raycast_filtered(&self, origin: Vec3, direction: Vec3, max_distance: f32, mut options: QueryOptions) -> Option<RayHit> {
492 options.exclude.get_or_insert(self.entity);
493 (self.raycast_fn)(self.physics, origin, direction, max_distance, options)
494 }
495
496 pub fn overlap_sphere_filtered(&self, center: Vec3, radius: f32, options: QueryOptions) -> Vec<Entity> {
498 (self.overlap_filtered_fn)(self.physics, center, radius, options)
499 }
500
501 pub fn overlap_sphere(&self, center: Vec3, radius: f32) -> Vec<Entity> {
504 (self.overlap_fn)(self.physics, center, radius)
505 }
506
507 pub fn touching(&self, entity: Entity) -> Vec<Entity> {
509 (self.touching_fn)(self.physics, entity)
510 }
511
512 pub fn is_alive(&self, entity: Entity) -> bool {
514 self.scene.world.is_alive(entity)
515 }
516
517 pub fn world_matrix_of(&self, entity: Entity) -> glam::Mat4 {
519 hierarchy::global_matrix(&self.scene.world, entity)
520 }
521
522 pub fn set_parent(&mut self, entity: Entity, parent: Option<Entity>) -> bool {
524 hierarchy::set_parent(&mut self.scene.world, entity, parent)
525 }
526
527 pub fn set_parent_local(&mut self, entity: Entity, parent: Option<Entity>) -> bool {
529 hierarchy::set_parent_local(&mut self.scene.world, entity, parent)
530 }
531
532 pub fn insert<T: EcsComponent>(&mut self, entity: Entity, component: T) {
536 self.scene.world.insert(entity, component);
537 }
538
539 pub fn remove<T: EcsComponent>(&mut self, entity: Entity) -> Option<T> {
541 self.scene.world.remove::<T>(entity)
542 }
543
544 pub fn has<T: EcsComponent>(&self) -> bool {
546 self.scene.world.has::<T>(self.entity)
547 }
548
549 pub fn attach_script(&mut self, entity: Entity, name: &str) -> bool {
552 let Some(scripts) = self.scene.world.get_or_insert_with::<Scripts>(entity, Scripts::default) else {
553 return false;
554 };
555 scripts.0.push(ScriptSlot { name: name.to_owned(), ..Default::default() });
556 true
557 }
558
559 pub fn detach_script(&mut self, entity: Entity, name: &str) -> usize {
561 let Some(scripts) = self.scene.world.get_mut::<Scripts>(entity) else {
562 return 0;
563 };
564 let before = scripts.0.len();
565 scripts.0.retain(|s| s.name != name);
566 before - scripts.0.len()
567 }
568
569 pub fn tagged(&self, tag: &str) -> Vec<Entity> {
571 crate::gameplay::tagged(&self.scene.world, tag)
572 }
573
574 pub fn has_tag(&self, entity: Entity, tag: &str) -> bool {
576 self.get_of::<Tags>(entity).is_some_and(|tags| tags.contains(tag))
577 }
578
579 pub fn add_tag(&mut self, entity: Entity, tag: &str) -> bool {
581 if tag.is_empty() || tag.chars().any(char::is_whitespace) {
582 return false;
583 }
584 self.scene.world.get_or_insert_with::<Tags>(entity, Tags::default).is_some_and(|tags| tags.insert(tag))
585 }
586
587 pub fn remove_tag(&mut self, entity: Entity, tag: &str) -> bool {
589 self.get_mut_of::<Tags>(entity).is_some_and(|tags| tags.remove(tag))
590 }
591
592 pub fn despawn_after(&mut self, entity: Entity, seconds: f32) -> bool {
596 let Some(lifetime) = Lifetime::new(seconds) else {
597 return false;
598 };
599 if !self.is_alive(entity) {
600 return false;
601 }
602 self.scene.world.insert(entity, lifetime);
603 true
604 }
605
606 pub fn set_visible(&mut self, entity: Entity, visible: bool) -> bool {
609 if !self.is_alive(entity) {
610 return false;
611 }
612 self.scene.world.insert(entity, Visibility { visible });
613 true
614 }
615
616 pub fn is_visible(&self, entity: Entity) -> bool {
618 hierarchy::is_visible(&self.scene.world, entity)
619 }
620
621 pub fn move_to(&mut self, entity: Entity, target: Vec3, seconds: f32, ease: crate::tween::Ease) -> bool {
624 let Some(mut tween) = crate::tween::MoveTween::to(target, seconds) else {
625 return false;
626 };
627 if !self.scene.world.has::<Transform>(entity) {
628 return false;
629 }
630 tween.ease = ease;
631 self.scene.world.insert(entity, tween);
632 true
633 }
634
635 pub fn teleport_of(&mut self, entity: Entity, position: Vec3, reset_velocity: bool) -> bool {
640 if !hierarchy::set_world_position(&mut self.scene.world, entity, position) {
641 return false;
642 }
643 if let Some(body) = self.get_mut_of::<RigidBody>(entity) {
644 body.teleport = true;
645 if reset_velocity {
646 body.set_velocity = Some(Vec3::ZERO);
647 body.set_angular_velocity = Some(Vec3::ZERO);
648 body.impulse = Vec3::ZERO;
649 }
650 }
651 true
652 }
653
654 pub fn teleport(&mut self, position: Vec3, reset_velocity: bool) -> bool {
656 self.teleport_of(self.entity, position, reset_velocity)
657 }
658
659 pub fn emit(&mut self, channel: &str, payload: serde_json::Value) -> Result<u64, crate::signals::SignalError> {
662 self.scene.signals.send(channel, payload, Some(self.entity), None)
663 }
664
665 pub fn send_to(&mut self, target: Entity, channel: &str, payload: serde_json::Value) -> Result<u64, crate::signals::SignalError> {
667 if !self.is_alive(target) {
668 return Err(crate::signals::SignalError::TargetNotAlive);
669 }
670 self.scene.signals.send(channel, payload, Some(self.entity), Some(target))
671 }
672
673 pub fn signals(&self, channel: &str) -> Vec<crate::signals::Signal> {
677 self.scene.signals.for_entity(self.entity).filter(|signal| signal.channel == channel).cloned().collect()
678 }
679
680 pub fn key_down(&self, k: Key) -> bool {
682 self.input.key_down(k)
683 }
684 pub fn key_pressed(&self, k: Key) -> bool {
686 self.input.key_pressed(k)
687 }
688 pub fn key_released(&self, key: Key) -> bool {
690 self.input.key_released(key)
691 }
692
693 pub fn mouse_down(&self, b: MouseButton) -> bool {
695 self.input.mouse_down(b)
696 }
697
698 pub fn mouse_pressed(&self, button: MouseButton) -> bool {
700 self.input.mouse_pressed(button)
701 }
702 pub fn mouse_released(&self, button: MouseButton) -> bool {
704 self.input.mouse_released(button)
705 }
706
707 pub fn lock_cursor(&self, lock: bool) {
709 self.input.lock_cursor(lock);
710 }
711 pub fn load_scene(&self, path: &str) {
714 self.input.request_scene(crate::input::SceneRequest::Load(path.to_string()));
715 }
716 pub fn add_scene(&self, path: &str) {
719 self.input.request_scene(crate::input::SceneRequest::Add(path.to_string()));
720 }
721 pub fn reload_scene(&self) {
723 self.input.request_scene(crate::input::SceneRequest::Reload);
724 }
725 pub fn scene_path(&self) -> String {
727 self.input.scene_path()
728 }
729 pub fn hud_prompt(&self, text: &str) {
731 self.input.hud().prompt = text.to_string();
732 }
733 pub fn hud_message(&self, text: &str, seconds: f32) {
735 let mut h = self.input.hud();
736 h.message = text.to_string();
737 h.message_time = if seconds.is_finite() { seconds.max(0.0) } else { 0.0 };
738 }
739 pub fn hud_objective(&self, text: &str) {
741 self.input.hud().objective = text.to_string();
742 }
743 pub fn hud_crosshair(&self, on: bool) {
745 self.input.hud().crosshair = on;
746 }
747
748 pub fn action_down(&self, name: &str) -> bool {
750 self.input.action_down(name)
751 }
752 pub fn action_pressed(&self, name: &str) -> bool {
754 self.input.action_pressed(name)
755 }
756 pub fn action_released(&self, name: &str) -> bool {
758 self.input.action_released(name)
759 }
760 pub fn action_vector(&self, left: &str, right: &str, down: &str, up: &str) -> glam::Vec2 {
762 self.input.action_vector(left, right, down, up)
763 }
764 pub fn action_axis(&self, negative: &str, positive: &str) -> f32 {
766 self.input.action_axis(negative, positive)
767 }
768 pub fn rebind(&self, name: &str, inputs: &[&str]) {
770 self.input.rebind(name, inputs);
771 }
772}
773
774pub struct ScriptHost {
777 defs: Vec<ScriptDef>,
778 library: Option<Arc<LoadedLibrary>>,
779}
780
781struct LoadedLibrary {
782 library: Option<libloading::Library>,
783 copy: PathBuf,
784}
785
786impl Drop for LoadedLibrary {
787 fn drop(&mut self) {
788 self.library.take();
790 let _ = std::fs::remove_file(&self.copy);
791 }
792}
793
794struct RetainedScript {
795 script: Box<dyn Script>,
797 _library: Option<Arc<LoadedLibrary>>,
798}
799
800impl Script for RetainedScript {
801 fn start(&mut self, ctx: &mut Ctx) {
802 self.script.start(ctx);
803 }
804 fn update(&mut self, ctx: &mut Ctx) {
805 self.script.update(ctx);
806 }
807 fn fixed_update(&mut self, ctx: &mut Ctx) {
808 self.script.fixed_update(ctx);
809 }
810 fn fields(&mut self) -> Vec<Field<'_>> {
811 self.script.fields()
812 }
813}
814
815static LOAD_COUNTER: AtomicUsize = AtomicUsize::new(0);
816
817impl ScriptHost {
818 pub fn from_defs(defs: Vec<ScriptDef>) -> Result<Self, String> {
830 validate_defs(&defs)?;
831 Ok(Self { defs, library: None })
832 }
833
834 pub fn load(path: &Path) -> Result<ScriptHost, String> {
836 let ext = path.extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();
837 let copy = std::env::temp_dir().join(format!(
838 "kvas_scripts_{}_{}.{ext}",
839 std::process::id(),
840 LOAD_COUNTER.fetch_add(1, Ordering::Relaxed)
841 ));
842 std::fs::copy(path, ©).map_err(|e| format!("{}: {e}", path.display()))?;
843 let lib = match unsafe { libloading::Library::new(©) } {
845 Ok(lib) => lib,
846 Err(e) => {
847 let _ = std::fs::remove_file(©);
848 return Err(format!("cannot load scripts: {e}"));
849 }
850 };
851 let library = Arc::new(LoadedLibrary { library: Some(lib), copy });
852 let lib = library.library.as_ref().expect("loaded library");
853 let probe: libloading::Symbol<extern "C" fn() -> AbiInfo> = unsafe { lib.get(b"kvas_script_abi") }
854 .map_err(|_| "scripts library has no ABI probe; rebuild it with the current engine".to_string())?;
855 if probe() != AbiInfo::current() {
856 return Err("scripts were built against a different engine/compiler build; rebuild them".into());
857 }
858 let entry: libloading::Symbol<fn() -> ScriptLib> =
859 unsafe { lib.get(b"kvas_script_lib") }.map_err(|_| "scripts library does not call register_scripts!".to_string())?;
860 let lib_fn: fn() -> ScriptLib = *entry;
862 let script_lib = lib_fn();
863 if script_lib.abi != AbiInfo::current() {
864 return Err("scripts were built against a different engine/compiler build; rebuild them".into());
865 }
866 validate_defs(&script_lib.scripts)?;
867 Ok(ScriptHost { defs: script_lib.scripts, library: Some(library) })
868 }
869
870 pub fn names(&self) -> Vec<&str> {
873 self.defs.iter().map(|d| d.name).collect()
874 }
875
876 pub fn names_owned(&self) -> Vec<String> {
878 self.names().into_iter().map(str::to_owned).collect()
879 }
880
881 pub fn callbacks(&self, name: &str) -> (bool, bool) {
883 self.defs.iter().find(|d| d.name == name).map_or((true, true), |d| (d.update, d.fixed))
884 }
885
886 pub fn create(&self, name: &str) -> Option<Box<dyn Script>> {
890 self.defs
891 .iter()
892 .find(|d| d.name == name)
893 .map(|d| Box::new(RetainedScript { script: (d.create)(), _library: self.library.clone() }) as Box<dyn Script>)
894 }
895}
896
897#[derive(Clone, Copy, PartialEq, Eq)]
899pub enum Phase {
900 Update,
902 Fixed,
904}
905
906struct Instance {
907 entity: Entity,
908 slot: usize,
909 name: String,
910 script: Box<dyn Script>,
911 started: bool,
912 failed: bool,
913 update: bool,
915 fixed: bool,
916 synced: serde_json::Map<String, serde_json::Value>,
918}
919
920#[derive(Default)]
923pub struct ScriptRuntime {
924 instances: Vec<Instance>,
926 host: Option<ScriptHost>,
927 failed_creations: HashSet<(Entity, usize, String)>,
928}
929
930fn panic_text(p: Box<dyn std::any::Any + Send>) -> String {
931 p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
932}
933
934impl ScriptRuntime {
935 pub fn host(&self) -> Option<&ScriptHost> {
937 self.host.as_ref()
938 }
939
940 pub fn instance_count(&self) -> usize {
942 self.instances.len()
943 }
944
945 pub fn failed_count(&self) -> usize {
947 self.instances.iter().filter(|i| i.failed).count() + self.failed_creations.len()
948 }
949
950 pub fn restart_instances(&mut self) {
953 self.instances.clear();
954 self.failed_creations.clear();
955 }
956
957 pub fn set_host(&mut self, host: Option<ScriptHost>) {
959 self.instances.clear();
960 self.failed_creations.clear();
961 self.host = host;
962 }
963
964 pub fn sync(&mut self, scene: &Scene, log: &mut Vec<String>) {
966 let attached = |entity: Entity, slot: usize, name: &str| {
967 scene.world.get::<Scripts>(entity).and_then(|s| s.0.get(slot)).is_some_and(|s| s.name == name)
968 };
969 self.instances.retain(|i| attached(i.entity, i.slot, &i.name));
970 self.failed_creations.retain(|(entity, slot, name)| attached(*entity, *slot, name));
971 let have: HashSet<(Entity, usize)> = self.instances.iter().map(|i| (i.entity, i.slot)).collect();
972 for (entity, scripts) in scene.world.iter::<Scripts>() {
973 for (slot, s) in scripts.0.iter().enumerate() {
974 if have.contains(&(entity, slot)) {
975 continue;
976 }
977 let Some(host) = &self.host else { continue };
978 let key = (entity, slot, s.name.clone());
979 if self.failed_creations.contains(&key) {
980 continue;
981 }
982 let created = catch_unwind(AssertUnwindSafe(|| {
983 let mut script = host.create(&s.name)?;
984 let mut fields = script.fields();
985 log.extend(
986 apply_props_checked(&mut fields, &s.props).into_iter().map(|warning| format!("script '{}': {warning}", s.name)),
987 );
988 drop(fields);
989 Some(script)
990 }));
991 match created {
992 Ok(Some(script)) => {
993 let (update, fixed) = host.callbacks(&s.name);
994 self.instances.push(Instance {
995 entity,
996 slot,
997 name: s.name.clone(),
998 script,
999 started: false,
1000 failed: false,
1001 update,
1002 fixed,
1003 synced: s.props.clone(),
1004 });
1005 }
1006 Ok(None) => {
1007 self.failed_creations.insert(key);
1008 log.push(format!("script '{}' not found (is the scripts library built?)", s.name));
1009 }
1010 Err(p) => {
1011 self.failed_creations.insert(key);
1012 log.push(format!("script '{}' initialization panicked and was disabled: {}", s.name, panic_text(p)));
1013 }
1014 }
1015 }
1016 }
1017 }
1018
1019 pub fn run(&mut self, phase: Phase, scene: &mut Scene, physics: &Physics, input: &Input, time: Time, log: &mut Vec<String>) {
1022 for inst in &mut self.instances {
1023 let wanted = match phase {
1024 Phase::Update => inst.update,
1025 Phase::Fixed => inst.fixed,
1026 };
1027 if inst.failed || (inst.started && !wanted) {
1029 continue;
1030 }
1031 let Some(slot) = scene.world.get::<Scripts>(inst.entity).and_then(|s| s.0.get(inst.slot)).filter(|s| s.name == inst.name) else {
1032 continue;
1033 };
1034 if slot.props != inst.synced {
1036 let props = slot.props.clone();
1037 let mut fields = inst.script.fields();
1038 apply_props(&mut fields, &props);
1039 drop(fields);
1040 inst.synced = props;
1041 }
1042 let mut ctx = Ctx {
1043 entity: inst.entity,
1044 scene,
1045 input,
1046 time,
1047 physics,
1048 raycast_fn: Physics::raycast_with_options,
1049 overlap_fn: Physics::overlap_sphere,
1050 touching_fn: Physics::touching,
1051 overlap_filtered_fn: Physics::overlap_sphere_with_options,
1052 log,
1053 };
1054 let result = catch_unwind(AssertUnwindSafe(|| {
1055 if !inst.started {
1056 inst.started = true;
1057 inst.script.start(&mut ctx);
1058 }
1059 if !ctx.scene.world.get::<Scripts>(inst.entity).and_then(|s| s.0.get(inst.slot)).is_some_and(|s| s.name == inst.name) {
1060 return;
1061 }
1062 match phase {
1063 Phase::Update if wanted => inst.script.update(&mut ctx),
1064 Phase::Fixed if wanted => inst.script.fixed_update(&mut ctx),
1065 _ => {}
1066 }
1067 }));
1068 if let Err(p) = result {
1069 inst.failed = true;
1070 log.push(format!("script panicked and was disabled: {}", panic_text(p)));
1071 continue;
1072 }
1073 let current = read_props(&inst.script.fields());
1075 if current != inst.synced {
1076 if let Some(slot) = scene.world.get_mut::<Scripts>(inst.entity).and_then(|s| s.0.get_mut(inst.slot)) {
1077 if slot.name == inst.name {
1078 slot.props = current.clone();
1079 }
1080 }
1081 inst.synced = current;
1082 }
1083 }
1084 }
1085}
1086
1087fn validate_defs(defs: &[ScriptDef]) -> Result<(), String> {
1088 let mut names = HashSet::new();
1089 for def in defs {
1090 if def.name.trim().is_empty() {
1091 return Err("script name cannot be empty".into());
1092 }
1093 if !names.insert(def.name) {
1094 return Err(format!("duplicate script name '{}'", def.name));
1095 }
1096 }
1097 Ok(())
1098}