1use gizmo_core::input::Input;
2use gizmo_core::World;
3use mlua::prelude::*;
4use mlua::RegistryKey;
5use std::collections::BTreeMap;
6use std::sync::Arc;
7use tracing::{debug, error, info, trace, warn};
8
9
10fn wake_after_velocity_write(world: &mut World, id: u32) {
21 let mut rbs = world.borrow_mut::<gizmo_physics_rigid::components::RigidBody>();
22 if let Some(mut rb) = rbs.get_mut(id) {
23 rb.wake_up();
24 }
25}
26
27use crate::api_ai;
28use crate::api_audio;
29use crate::api_entity;
30use crate::api_fighter;
31use crate::api_input;
32use crate::api_physics;
33use crate::api_scene;
34use crate::api_time;
35use crate::api_vehicle;
36use crate::commands::{CommandQueue, ScriptCommand};
37
38pub struct ScriptEngine {
40 lua: Lua,
41 loaded_scripts: BTreeMap<String, (String, RegistryKey)>,
49 command_queue: Arc<CommandQueue>,
50 budget: Arc<std::sync::atomic::AtomicU32>,
52 budget_ticks: u32,
54 elapsed_time: f32,
55 pub log_queue: Arc<std::sync::Mutex<Vec<(String, String)>>>, }
58
59unsafe impl Sync for ScriptEngine {}
86
87impl std::fmt::Debug for ScriptEngine {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("ScriptEngine")
92 .field("lua", &"<Lua VM>")
93 .field("loaded_scripts", &self.loaded_scripts.keys())
94 .field("elapsed_time", &self.elapsed_time)
95 .field(
96 "queued_commands",
97 &self.command_queue.len(),
98 )
99 .field(
100 "queued_logs",
101 &self.log_queue.lock().map(|q| q.len()).unwrap_or(0),
102 )
103 .finish()
104 }
105}
106
107#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
113pub enum ScriptValue {
114 Num(f64),
115 Bool(bool),
116 Text(String),
117}
118
119impl ScriptValue {
120 pub fn kind(&self) -> &'static str {
132 match self {
133 Self::Num(_) => "number",
134 Self::Bool(_) => "bool",
135 Self::Text(_) => "text",
136 }
137 }
138}
139
140#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
142pub struct Script {
143 pub file_path: String,
144 #[serde(default, skip)]
145 pub initialized: bool, #[serde(default)]
155 pub properties: std::collections::BTreeMap<String, ScriptValue>,
156}
157
158impl Script {
159 pub fn new(path: &str) -> Self {
160 Self {
161 file_path: path.to_string(),
162 initialized: false,
163 properties: std::collections::BTreeMap::new(),
164 }
165 }
166}
167
168#[derive(Clone, Debug, Default)]
170#[non_exhaustive]
171pub struct ScriptContext {
172 pub entity_id: u32,
173 pub dt: f32,
174 pub position: [f32; 3],
175 pub velocity: [f32; 3],
176 pub key_w: bool,
177 pub key_a: bool,
178 pub key_s: bool,
179 pub key_d: bool,
180 pub key_space: bool,
181 pub key_up: bool,
182 pub key_down: bool,
183 pub key_left: bool,
184 pub key_right: bool,
185}
186
187#[derive(Clone, Debug, Default)]
189pub struct ScriptResult {
190 pub new_position: Option<[f32; 3]>,
191 pub new_velocity: Option<[f32; 3]>,
192}
193
194impl ScriptEngine {
195 const HOOK_INSTRUCTION_STEP: u32 = 10_000;
199
200 pub const DEFAULT_INSTRUCTION_BUDGET: u32 = 2_000_000;
209
210 pub const DEFAULT_MEMORY_LIMIT: usize = 64 * 1024 * 1024;
214
215 pub fn new() -> Result<Self, LuaError> {
216 let lua = Lua::new();
217 let command_queue = Arc::new(CommandQueue::new());
218 let log_queue = Arc::new(std::sync::Mutex::new(Vec::new()));
219
220 lua.globals().set("os", LuaNil)?;
222 lua.globals().set("io", LuaNil)?;
223 lua.globals().set("loadfile", LuaNil)?;
224 lua.globals().set("dofile", LuaNil)?;
225 lua.globals().set("require", LuaNil)?;
226 lua.globals().set("package", LuaNil)?;
227 lua.globals().set("debug", LuaNil)?;
228 lua.globals().set("loadstring", LuaNil)?;
229 lua.globals().set("load", LuaNil)?;
230
231 let budget = Arc::new(std::sync::atomic::AtomicU32::new(0));
238 let hook_budget = budget.clone();
239 lua.set_hook(
240 mlua::HookTriggers::new().every_nth_instruction(Self::HOOK_INSTRUCTION_STEP),
241 move |_lua, _debug| {
242 let left = hook_budget.load(std::sync::atomic::Ordering::Relaxed);
246 if left == 0 {
247 return Err(LuaError::RuntimeError(
248 "script exceeded its instruction budget for this call (infinite loop?)"
249 .to_string(),
250 ));
251 }
252 hook_budget.store(left - 1, std::sync::atomic::Ordering::Relaxed);
253 Ok(())
254 },
255 );
256 lua.set_memory_limit(Self::DEFAULT_MEMORY_LIMIT)?;
257
258 let lq_clone1 = log_queue.clone();
260 lua.globals().set(
261 "print_engine",
262 lua.create_function(move |_, msg: String| {
263 if let Ok(mut q) = lq_clone1.lock() {
264 q.push(("info".to_string(), msg));
265 }
266 Ok(())
267 })?,
268 )?;
269
270 let lq_clone2 = log_queue.clone();
272 lua.globals().set(
273 "print",
274 lua.create_function(move |_, values: LuaMultiValue| {
275 let parts: Vec<String> = values
276 .iter()
277 .map(|v| {
278 if let mlua::Value::String(s) = v {
279 s.to_str().unwrap_or("").to_string()
280 } else if let mlua::Value::Number(n) = v {
281 n.to_string()
282 } else if let mlua::Value::Integer(i) = v {
283 i.to_string()
284 } else if let mlua::Value::Boolean(b) = v {
285 b.to_string()
286 } else {
287 format!("{:?}", v)
288 }
289 })
290 .collect();
291 if let Ok(mut q) = lq_clone2.lock() {
292 q.push(("info".to_string(), parts.join("\t")));
293 }
294 Ok(())
295 })?,
296 )?;
297
298 lua.load(
300 r#"
301 function vec3(x, y, z)
302 return { x = x or 0, y = y or 0, z = z or 0 }
303 end
304
305 function vec3_add(a, b)
306 return vec3(a.x + b.x, a.y + b.y, a.z + b.z)
307 end
308
309 function vec3_sub(a, b)
310 return vec3(a.x - b.x, a.y - b.y, a.z - b.z)
311 end
312
313 function vec3_scale(v, s)
314 return vec3(v.x * s, v.y * s, v.z * s)
315 end
316
317 function vec3_length(v)
318 return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
319 end
320
321 function vec3_normalize(v)
322 local len = vec3_length(v)
323 if len > 0.0001 then
324 return vec3(v.x / len, v.y / len, v.z / len)
325 end
326 return vec3(0, 0, 0)
327 end
328
329 function vec3_dot(a, b)
330 return a.x * b.x + a.y * b.y + a.z * b.z
331 end
332
333 function vec3_cross(a, b)
334 return vec3(
335 a.y * b.z - a.z * b.y,
336 a.z * b.x - a.x * b.z,
337 a.x * b.y - a.y * b.x
338 )
339 end
340
341 function vec3_lerp(a, b, t)
342 return vec3(
343 a.x + (b.x - a.x) * t,
344 a.y + (b.y - a.y) * t,
345 a.z + (b.z - a.z) * t
346 )
347 end
348
349 function vec3_distance(a, b)
350 return vec3_length(vec3_sub(a, b))
351 end
352
353 -- Clamp utility
354 function clamp(value, min, max)
355 return math.max(min, math.min(max, value))
356 end
357
358 -- Lerp utility
359 function lerp(a, b, t)
360 return a + (b - a) * t
361 end
362 "#,
363 )
364 .exec()?;
365
366 api_entity::register_entity_api(&lua, command_queue.clone())?;
368 api_fighter::register_fighter_api(&lua, command_queue.clone())?;
369 api_input::register_input_api(&lua)?;
370 api_physics::register_physics_api(&lua, command_queue.clone())?;
371 api_scene::register_scene_api(&lua, command_queue.clone())?;
372 api_audio::register_audio_api(&lua, command_queue.clone())?;
373 api_time::register_time_api(&lua)?;
374 api_vehicle::register_vehicle_api(&lua, command_queue.clone())?;
375 api_ai::register_ai_api(&lua, command_queue.clone())?;
376
377 info!("[Scripting] ScriptEngine başlatıldı — Lua 5.4 sandbox aktif, API modülleri kayıtlı");
378 Ok(Self {
379 lua,
380 loaded_scripts: BTreeMap::new(),
381 command_queue,
382 budget,
383 budget_ticks: Self::DEFAULT_INSTRUCTION_BUDGET / Self::HOOK_INSTRUCTION_STEP,
384 elapsed_time: 0.0,
385 log_queue,
386 })
387 }
388
389 fn arm_budget(&self) {
395 self.budget
396 .store(self.budget_ticks, std::sync::atomic::Ordering::Relaxed);
397 }
398
399 pub fn set_instruction_budget(&mut self, instructions: u32) {
402 self.budget_ticks = (instructions / Self::HOOK_INSTRUCTION_STEP).max(1);
403 }
404
405 pub fn set_memory_limit(&mut self, bytes: usize) -> Result<usize, LuaError> {
407 self.lua.set_memory_limit(bytes)
408 }
409
410 #[tracing::instrument(skip_all, name = "script_load", fields(path = %path))]
411 pub fn load_script(&mut self, path: &str) -> Result<(), String> {
412 let content = std::fs::read_to_string(path).map_err(|e| {
413 error!(path, error = %e, "[Scripting] Script dosyası okunamadı");
414 format!("Script okunamadı {}: {}", path, e)
415 })?;
416 let byte_len = content.len();
417
418 let env = self.lua.create_table().map_err(|e| e.to_string())?;
419
420 let meta = self.lua.create_table().map_err(|e| e.to_string())?;
423 meta.set("__index", self.lua.globals())
424 .map_err(|e| e.to_string())?;
425 env.set_metatable(Some(meta));
426
427 env.set("_G", env.clone()).map_err(|e| e.to_string())?;
439
440 self.arm_budget();
442 self.lua
443 .load(&content)
444 .set_environment(env.clone())
445 .exec()
446 .map_err(|e| {
447 error!(path, bytes = byte_len, error = %e, "[Scripting] Lua derleme/çalıştırma hatası");
448 format!("Lua hata {}: {}", path, e)
449 })?;
450
451 let key = self
452 .lua
453 .create_registry_value(env)
454 .map_err(|e| e.to_string())?;
455
456 if let Some((_, old_key)) = self.loaded_scripts.insert(path.to_string(), (content, key)) {
458 debug!(path, "[Scripting] Var olan script değiştirildi (hot-reload), eski sürüm boşaltılıyor");
459 if let Err(e) = self.lua.remove_registry_value(old_key) {
462 warn!(path, error = %e, "[Scripting] Eski script registry değeri boşaltılamadı (olası Lua bellek sızıntısı)");
463 }
464 }
465
466 info!(path, bytes = byte_len, "🔧 [Scripting] Script yüklendi ve izole edildi");
467 Ok(())
468 }
469
470 #[tracing::instrument(skip_all, name = "script_update")]
472 pub fn update(&mut self, world: &World, input: &Input, dt: f32) -> Result<(), String> {
473 self.elapsed_time += dt;
474
475 api_entity::update_entity_read_api(&self.lua, world)
477 .map_err(|e| format!("Entity API güncelleme hatası: {}", e))?;
478 api_fighter::update_fighter_read_api(&self.lua, world)
479 .map_err(|e| format!("Fighter API güncelleme hatası: {}", e))?;
480 api_input::update_input_api(&self.lua, input)
481 .map_err(|e| format!("Input API güncelleme hatası: {}", e))?;
482 api_scene::update_scene_api(&self.lua, world)
483 .map_err(|e| format!("Scene API güncelleme hatası: {}", e))?;
484 api_time::update_time_api(&self.lua, dt, self.elapsed_time, 1.0 / dt.max(0.0001))
485 .map_err(|e| format!("Time API güncelleme hatası: {}", e))?;
486 api_physics::update_physics_api(&self.lua, world)
487 .map_err(|e| format!("Physics API güncelleme hatası: {}", e))?;
488
489 let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
494 ctx_table.set("dt", dt).map_err(|e| e.to_string())?;
495 ctx_table
496 .set("elapsed", self.elapsed_time)
497 .map_err(|e| e.to_string())?;
498
499 let lua = &self.lua;
508 let scripts = &self.loaded_scripts;
509 let budget = &self.budget;
510 let budget_ticks = self.budget_ticks;
511 let mut failures = Vec::new();
512 api_physics::with_call_time_queries(lua, world, || {
513 for (path, (_, key)) in scripts {
514 let env: mlua::Table = match lua.registry_value(key) {
515 Ok(env) => env,
516 Err(e) => {
517 failures.push(format!("{path}: env okunamadı: {e}"));
518 continue;
519 }
520 };
521 if let Ok(func) = env.get::<_, LuaFunction>("on_update") {
522 budget.store(budget_ticks, std::sync::atomic::Ordering::Relaxed);
523 if let Err(e) = func.call::<_, ()>(ctx_table.clone()) {
524 warn!(path = %path, error = %e, "[Scripting] on_update çalışma-zamanı hatası");
525 failures.push(format!("Lua on_update hatası ({path}): {e}"));
526 }
527 }
528 }
529 Ok(())
530 })
531 .map_err(|e| format!("script scope hatası: {e}"))?;
532
533 if failures.is_empty() {
534 Ok(())
535 } else {
536 Err(failures.join(" | "))
539 }
540 }
541
542 pub fn declared_properties(
558 &self,
559 script_path: &str,
560 ) -> std::collections::BTreeMap<String, ScriptValue> {
561 let mut out = std::collections::BTreeMap::new();
562 let Some((_, key)) = self.loaded_scripts.get(script_path) else {
563 return out;
564 };
565 let Ok(env) = self.lua.registry_value::<mlua::Table>(key) else {
566 return out;
567 };
568 let Ok(table) = env.get::<_, mlua::Table>("properties") else {
569 return out;
570 };
571 for pair in table.pairs::<String, mlua::Value>() {
572 let Ok((name, value)) = pair else { continue };
573 let converted = match value {
574 mlua::Value::Number(n) => Some(ScriptValue::Num(n)),
575 mlua::Value::Integer(i) => Some(ScriptValue::Num(i as f64)),
576 mlua::Value::Boolean(b) => Some(ScriptValue::Bool(b)),
577 mlua::Value::String(s) => s.to_str().ok().map(|t| ScriptValue::Text(t.to_string())),
578 _ => None,
579 };
580 if let Some(v) = converted {
581 out.insert(name, v);
582 }
583 }
584 out
585 }
586
587
588 #[cfg(test)]
590 pub fn eval_number(&self, script_path: &str, expr: &str) -> Option<f64> {
591 let (_, key) = self.loaded_scripts.get(script_path)?;
592 let env: mlua::Table = self.lua.registry_value(key).ok()?;
593 self.lua
594 .load(format!("return {expr}"))
595 .set_environment(env)
596 .eval::<f64>()
597 .ok()
598 }
599
600 pub fn update_entity(
607 &mut self,
608 entity_id: u32,
609 script_path: &str,
610 dt: f32,
611 properties: &std::collections::BTreeMap<String, ScriptValue>,
612 ) -> Result<(), String> {
613 if let Some((_, key)) = self.loaded_scripts.get(script_path) {
614 let env: mlua::Table = self.lua.registry_value(key).map_err(|e| e.to_string())?;
615
616 if let Ok(func) = env.get::<_, LuaFunction>("on_entity_update") {
618 let props = self.lua.create_table().map_err(|e| e.to_string())?;
619 for (name, value) in properties {
620 let set = match value {
621 ScriptValue::Num(n) => props.set(name.as_str(), *n),
622 ScriptValue::Bool(b) => props.set(name.as_str(), *b),
623 ScriptValue::Text(t) => props.set(name.as_str(), t.as_str()),
624 };
625 set.map_err(|e| e.to_string())?;
626 }
627 self.arm_budget();
628 func.call::<_, ()>((entity_id, dt, props)).map_err(|e| {
629 warn!(entity_id, script_path, error = %e, "[Scripting] on_entity_update çalışma-zamanı hatası");
630 format!(
631 "Lua on_entity_update hatası (entity {} mod {}): {}",
632 entity_id, script_path, e
633 )
634 })?;
635 }
636 } else {
637 trace!(entity_id, script_path, "[Scripting] update_entity: script yüklü değil, atlandı");
638 }
639 Ok(())
640 }
641
642 #[tracing::instrument(skip_all, name = "script_flush_commands")]
644 pub fn flush_commands(&self, world: &mut World, dt: f32) -> Vec<ScriptCommand> {
645 let commands = self.command_queue.drain();
646 let total = commands.len();
647 let mut unhandled = Vec::new();
648
649 for cmd in commands {
650 match cmd {
651 ScriptCommand::SetPosition(id, pos) => {
652 let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
653 if let Some(mut t) = transforms.get_mut(id) {
654 t.position = pos;
655 } else {
656 trace!(entity = id, "[Scripting] SetPosition: hedefte Transform yok, komut atlandı");
657 }
658 }
659 ScriptCommand::SetRotation(id, rot) => {
660 let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
661 if let Some(mut t) = transforms.get_mut(id) {
662 t.rotation = rot;
663 } else {
664 trace!(entity = id, "[Scripting] SetRotation: hedefte Transform yok, komut atlandı");
665 }
666 }
667 ScriptCommand::SetScale(id, scale) => {
668 let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
669 if let Some(mut t) = transforms.get_mut(id) {
670 t.scale = scale;
671 } else {
672 trace!(entity = id, "[Scripting] SetScale: hedefte Transform yok, komut atlandı");
673 }
674 }
675 ScriptCommand::SetVelocity(id, vel) => {
676 let mut written = false;
677 {
678 let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
679 if let Some(mut v) = velocities.get_mut(id) {
680 v.linear = vel;
681 written = true;
682 } else {
683 trace!(entity = id, "[Scripting] SetVelocity: hedefte Velocity yok, komut atlandı");
684 }
685 }
686 if written {
687 wake_after_velocity_write(world, id);
688 }
689 }
690 ScriptCommand::SetAngularVelocity(id, ang_vel) => {
691 let mut written = false;
692 {
693 let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
694 if let Some(mut v) = velocities.get_mut(id) {
695 v.angular = ang_vel;
696 written = true;
697 } else {
698 trace!(entity = id, "[Scripting] SetAngularVelocity: hedefte Velocity yok, komut atlandı");
699 }
700 }
701 if written {
702 wake_after_velocity_write(world, id);
703 }
704 }
705 ScriptCommand::ApplyForce(id, force) => {
706 let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
707 if let Some(rb) = rbs.get(id) {
708 if rb.mass > 0.0 {
709 let accel = force * (1.0 / rb.mass);
710 drop(rbs);
711 if world
714 .borrow::<gizmo_physics_rigid::components::Velocity>()
715 .get(id)
716 .is_none()
717 {
718 if let Some(e) = world.entity(id) {
719 world.add_component(
720 e,
721 gizmo_physics_rigid::components::Velocity::new(
722 gizmo_math::Vec3::ZERO,
723 ),
724 );
725 }
726 }
727 {
728 let mut vels =
729 world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
730 if let Some(mut v) = vels.get_mut(id) {
731 v.linear += accel * dt;
732 }
733 }
734 wake_after_velocity_write(world, id);
735 }
736 } else {
737 trace!(entity = id, "[Scripting] ApplyForce: hedefte RigidBody yok, kuvvet yok sayıldı");
738 }
739 }
740 ScriptCommand::ApplyImpulse(id, impulse) => {
741 let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
742 if let Some(rb) = rbs.get(id) {
743 if rb.mass > 0.0 {
744 let delta_v = impulse * (1.0 / rb.mass);
745 drop(rbs);
746 if world
749 .borrow::<gizmo_physics_rigid::components::Velocity>()
750 .get(id)
751 .is_none()
752 {
753 if let Some(e) = world.entity(id) {
754 world.add_component(
755 e,
756 gizmo_physics_rigid::components::Velocity::new(
757 gizmo_math::Vec3::ZERO,
758 ),
759 );
760 }
761 }
762 {
763 let mut vels =
764 world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
765 if let Some(mut v) = vels.get_mut(id) {
766 v.linear += delta_v;
767 }
768 }
769 wake_after_velocity_write(world, id);
770 }
771 } else {
772 trace!(entity = id, "[Scripting] ApplyImpulse: hedefte RigidBody yok, impuls yok sayıldı");
773 }
774 }
775 ScriptCommand::AddRigidBody {
776 id,
777 mass,
778 use_gravity,
779 } => {
780 let entity = world.entity(id);
781 if let Some(e) = entity {
782 let rb = gizmo_physics_rigid::components::RigidBody::new(mass, use_gravity);
783 world.add_component(e, rb);
784 if world
786 .borrow::<gizmo_physics_rigid::components::Velocity>()
787 .get(id)
788 .is_none()
789 {
790 world.add_component(
791 e,
792 gizmo_physics_rigid::components::Velocity::new(gizmo_math::Vec3::ZERO),
793 );
794 }
795 } else {
796 trace!(entity = id, "[Scripting] AddRigidBody: entity bulunamadı, komut atlandı");
797 }
798 }
799 ScriptCommand::AddBoxCollider { id, hx, hy, hz } => {
800 let entity = world.entity(id);
801 if let Some(e) = entity {
802 let col =
803 gizmo_physics_core::Collider::aabb(gizmo_math::Vec3::new(hx, hy, hz));
804 world.add_component(e, col);
805 } else {
806 trace!(entity = id, "[Scripting] AddBoxCollider: entity bulunamadı, komut atlandı");
807 }
808 }
809 ScriptCommand::AddSphereCollider { id, radius } => {
810 let entity = world.entity(id);
811 if let Some(e) = entity {
812 let col = gizmo_physics_core::Collider::sphere(radius);
813 world.add_component(e, col);
814 } else {
815 trace!(entity = id, "[Scripting] AddSphereCollider: entity bulunamadı, komut atlandı");
816 }
817 }
818
819 ScriptCommand::SpawnEntity { name, position } => {
827 let entity = world.spawn();
828 world.add_component(entity, gizmo_core::EntityName::new(&name));
829 world
830 .add_component(entity, gizmo_physics_core::Transform::new(position));
831 let msg = format!(
832 "Entity spawn: '{}' at ({:.1}, {:.1}, {:.1})",
833 name, position.x, position.y, position.z
834 );
835 if let Ok(mut q) = self.log_queue.lock() {
836 q.push(("info".to_string(), msg));
837 }
838 }
839 ScriptCommand::SpawnPrefab {
840 name,
841 prefab_type,
842 position,
843 } => {
844 let entity = world.spawn();
845 world.add_component(entity, gizmo_core::EntityName::new(&name));
846 world
847 .add_component(entity, gizmo_physics_core::Transform::new(position));
848 world.add_component(entity, gizmo_core::PrefabRequest(prefab_type.clone()));
849 }
850 ScriptCommand::DestroyEntity(id) => {
851 world.despawn_by_id(id);
852 if let Ok(mut q) = self.log_queue.lock() {
853 q.push(("info".to_string(), format!("Entity destroyed: {}", id)));
854 }
855 }
856ScriptCommand::SetEntityName(id, name) => {
857 let mut names = world.borrow_mut::<gizmo_core::EntityName>();
858 if let Some(mut n) = names.get_mut(id) {
859 n.0 = name;
860 } else {
861 trace!(entity = id, "[Scripting] SetEntityName: hedefte EntityName yok, komut atlandı");
862 }
863 }
864ScriptCommand::PlayAnimation { id, name, blend, loop_anim } => {
865 let mut players = world.borrow_mut::<gizmo_animation::skeletal::AnimationPlayer>();
866 if let Some(mut player) = players.get_mut(id) {
867 player.play_animation_by_name(&name, blend, loop_anim);
868 } else {
869 trace!(entity = id, anim = %name, "[Scripting] PlayAnimation: hedefte AnimationPlayer yok, komut atlandı");
870 }
871 }
872 ScriptCommand::SetAnimationSpeed(id, speed) => {
873 let mut players = world.borrow_mut::<gizmo_animation::skeletal::AnimationPlayer>();
874 if let Some(mut player) = players.get_mut(id) {
875 player.speed = speed;
876 } else {
877 trace!(entity = id, "[Scripting] SetAnimationSpeed: hedefte AnimationPlayer yok, komut atlandı");
878 }
879 }
880 ScriptCommand::AddNavAgent(id) => {
881 let entity = world.entity(id);
882 if let Some(e) = entity {
883 world.add_component(e, gizmo_ai::components::NavAgent::default());
884 } else {
885 trace!(entity = id, "[Scripting] AddNavAgent: entity bulunamadı, komut atlandı");
886 }
887 }
888 ScriptCommand::SetAiTarget(id, target) => {
889 let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
890 if let Some(mut agent) = agents.get_mut(id) {
891 agent.set_target(target);
892 } else {
893 trace!(entity = id, "[Scripting] SetAiTarget: hedefte NavAgent yok, komut atlandı");
894 }
895 }
896 ScriptCommand::ClearAiTarget(id) => {
897 let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
898 if let Some(mut agent) = agents.get_mut(id) {
899 agent.clear_target();
902 } else {
903 trace!(entity = id, "[Scripting] ClearAiTarget: hedefte NavAgent yok, komut atlandı");
904 }
905 }
906 ScriptCommand::SetFighterMove { id, name, startup, active, recovery, damage } => {
907 let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
908 if let Some(mut fighter) = fighters.get_mut(id) {
909 let mut frame_data =
910 gizmo_physics_core::components::fighter::FrameData::default();
911 frame_data.startup = startup;
912 frame_data.active = active;
913 frame_data.recovery = recovery;
914 frame_data.damage = damage;
915 let mut combat_move =
916 gizmo_physics_core::components::fighter::CombatMove::default();
917 combat_move.name = name;
918 combat_move.frame_data = frame_data;
919 fighter.active_move = Some(combat_move);
920 fighter.current_move_frame = 0;
921 } else {
922 trace!(entity = id, "[Scripting] SetFighterMove: hedefte FighterController yok, komut atlandı");
923 }
924 }
925 ScriptCommand::ApplyHitstop(id, frames) => {
926 let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
927 if let Some(mut fighter) = fighters.get_mut(id) {
928 fighter.apply_hitstop(frames);
929 } else {
930 trace!(entity = id, frames, "[Scripting] ApplyHitstop: hedefte FighterController yok, komut atlandı");
931 }
932 }
933 ScriptCommand::ApplyHitstun(id, frames) => {
934 let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
935 if let Some(mut fighter) = fighters.get_mut(id) {
936 fighter.apply_hitstun(frames);
937 } else {
938 trace!(entity = id, frames, "[Scripting] ApplyHitstun: hedefte FighterController yok, komut atlandı");
939 }
940 }
941 other => {
947 unhandled.push(other);
948 }
949 }
950 }
951
952 if total > 0 {
953 trace!(
954 total,
955 unhandled = unhandled.len(),
956 "[Scripting] script komut kuyruğu boşaltıldı"
957 );
958 }
959 unhandled
960 }
961
962 pub fn get_pending_audio_scene_commands(&self) -> Vec<ScriptCommand> {
964 Vec::new()
967 }
968
969 pub fn reload_if_changed(&mut self, path: &str) -> Result<bool, String> {
971 let current =
972 std::fs::read_to_string(path).map_err(|e| format!("Script okunamadı: {}", e))?;
973
974 if let Some((cached_code, _)) = self.loaded_scripts.get(path) {
975 if *cached_code == current {
976 return Ok(false);
977 }
978 }
979
980 self.load_script(path)?;
981 Ok(true)
982 }
983
984 pub fn has_function(&mut self, path: &str, name: &str) -> bool {
990 if let Some((_, key)) = self.loaded_scripts.get(path) {
991 if let Ok(env) = self.lua.registry_value::<mlua::Table>(key) {
992 return env.get::<_, LuaFunction>(name).is_ok();
993 }
994 }
995 false
996 }
997
998 pub fn run_entity_update(
1003 &mut self,
1004 path: &str,
1005 func_name: &str,
1006 ctx: &ScriptContext,
1007 ) -> Result<ScriptResult, String> {
1008 let env: mlua::Table = if let Some((_, key)) = self.loaded_scripts.get(path) {
1009 self.lua.registry_value(key).map_err(|e| e.to_string())?
1010 } else {
1011 return Err(format!("Script not loaded: {}", path));
1012 };
1013
1014 let func: LuaFunction = match env.get(func_name) {
1015 Ok(f) => f,
1016 Err(e) => {
1017 trace!(path, func_name, error = %e, "[Scripting] run_entity_update: fonksiyon alınamadı, varsayılan sonuç");
1018 return Ok(ScriptResult::default());
1019 }
1020 };
1021
1022 let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
1023 ctx_table
1024 .set("entity_id", ctx.entity_id)
1025 .map_err(|e| e.to_string())?;
1026 ctx_table.set("dt", ctx.dt).map_err(|e| e.to_string())?;
1027 ctx_table
1028 .set("elapsed", self.elapsed_time)
1029 .map_err(|e| e.to_string())?;
1030
1031 let pos = self.lua.create_table().map_err(|e| e.to_string())?;
1032 pos.set("x", ctx.position[0]).map_err(|e| e.to_string())?;
1033 pos.set("y", ctx.position[1]).map_err(|e| e.to_string())?;
1034 pos.set("z", ctx.position[2]).map_err(|e| e.to_string())?;
1035 ctx_table.set("position", pos).map_err(|e| e.to_string())?;
1036
1037 let vel = self.lua.create_table().map_err(|e| e.to_string())?;
1038 vel.set("x", ctx.velocity[0]).map_err(|e| e.to_string())?;
1039 vel.set("y", ctx.velocity[1]).map_err(|e| e.to_string())?;
1040 vel.set("z", ctx.velocity[2]).map_err(|e| e.to_string())?;
1041 ctx_table.set("velocity", vel).map_err(|e| e.to_string())?;
1042
1043 let input = self.lua.create_table().map_err(|e| e.to_string())?;
1044 input.set("w", ctx.key_w).map_err(|e| e.to_string())?;
1045 input.set("a", ctx.key_a).map_err(|e| e.to_string())?;
1046 input.set("s", ctx.key_s).map_err(|e| e.to_string())?;
1047 input.set("d", ctx.key_d).map_err(|e| e.to_string())?;
1048 input
1049 .set("space", ctx.key_space)
1050 .map_err(|e| e.to_string())?;
1051 input.set("up", ctx.key_up).map_err(|e| e.to_string())?;
1052 input.set("down", ctx.key_down).map_err(|e| e.to_string())?;
1053 input.set("left", ctx.key_left).map_err(|e| e.to_string())?;
1054 input
1055 .set("right", ctx.key_right)
1056 .map_err(|e| e.to_string())?;
1057 ctx_table.set("input", input).map_err(|e| e.to_string())?;
1058
1059 self.arm_budget();
1060 let result_table: LuaTable = func.call(ctx_table).map_err(|e| {
1061 warn!(path, func_name, error = %e, "[Scripting] run_entity_update: Lua çalışma-zamanı hatası");
1062 format!("Lua runtime: {}", e)
1063 })?;
1064
1065 let mut result = ScriptResult::default();
1066
1067 if let Ok(pos) = result_table.get::<_, LuaTable>("position") {
1068 let x: f32 = pos.get("x").unwrap_or(0.0);
1069 let y: f32 = pos.get("y").unwrap_or(0.0);
1070 let z: f32 = pos.get("z").unwrap_or(0.0);
1071 result.new_position = Some([x, y, z]);
1072 }
1073
1074 if let Ok(vel) = result_table.get::<_, LuaTable>("velocity") {
1075 let x: f32 = vel.get("x").unwrap_or(0.0);
1076 let y: f32 = vel.get("y").unwrap_or(0.0);
1077 let z: f32 = vel.get("z").unwrap_or(0.0);
1078 result.new_velocity = Some([x, y, z]);
1079 }
1080
1081 Ok(result)
1082 }
1083
1084 pub fn command_queue(&self) -> &Arc<CommandQueue> {
1086 &self.command_queue
1087 }
1088}
1089
1090gizmo_core::impl_component!(Script);
1091
1092#[cfg(test)]
1093mod soundness {
1094 use super::*;
1095
1096 #[test]
1102 fn script_engine_is_send_and_sync() {
1103 fn assert_send_sync<T: Send + Sync>() {}
1104 assert_send_sync::<ScriptEngine>();
1105 }
1106
1107 #[test]
1125 fn shared_methods_never_reach_the_lua_vm() {
1126 let engine = ScriptEngine::new().expect("Lua VM");
1127 let shared = &engine;
1128
1129 std::thread::scope(|s| {
1130 for _ in 0..2 {
1131 s.spawn(move || {
1132 let _ = shared.get_pending_audio_scene_commands();
1136 let _ = shared.command_queue().len();
1137 });
1138 }
1139 });
1140
1141 let mut world = gizmo_core::World::new();
1144 let _ = shared.flush_commands(&mut world, 1.0 / 60.0);
1145 }
1146
1147 #[test]
1153 fn vm_touching_methods_require_exclusive_access() {
1154 fn _needs_mut(e: &mut ScriptEngine) {
1155 let _ = e.has_function("nope.lua", "on_update");
1156 }
1157 fn _needs_mut_2(e: &mut ScriptEngine, ctx: &ScriptContext) {
1158 let _ = e.run_entity_update("nope.lua", "on_update", ctx);
1159 }
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165
1166 #[test]
1175 fn a_script_cannot_reach_another_through_g() {
1176 let dir = std::env::temp_dir().join(format!("gizmo_sandbox_{}", std::process::id()));
1177 std::fs::create_dir_all(&dir).unwrap();
1178 let a = dir.join("a_writer.lua");
1179 let b = dir.join("b_reader.lua");
1180 std::fs::write(&a, "function on_update(c)\n _G.LEAK = 'from-a'\n IMPLICIT = 'also-a'\nend\n")
1181 .unwrap();
1182 std::fs::write(
1183 &b,
1184 "function on_update(c)\n print('LEAK=' .. tostring(_G.LEAK))\n print('IMPLICIT=' .. tostring(IMPLICIT))\nend\n",
1185 )
1186 .unwrap();
1187
1188 let mut engine = ScriptEngine::new().unwrap();
1189 engine.load_script(a.to_str().unwrap()).unwrap();
1190 engine.load_script(b.to_str().unwrap()).unwrap();
1191 engine.update(&World::new(), &Input::default(), 0.016).unwrap();
1192
1193 let log = engine.log_queue.lock().unwrap().clone();
1194 let said = |needle: &str| log.iter().any(|(_, m)| m.contains(needle));
1195 assert!(said("LEAK=nil"), "`_G.X` from one script reached another: {log:?}");
1196 assert!(said("IMPLICIT=nil"), "an implicit global reached another script: {log:?}");
1197 std::fs::remove_dir_all(&dir).ok();
1198 }
1199
1200 #[test]
1204 fn a_script_still_reaches_the_engine_api_and_its_own_globals() {
1205 let dir = std::env::temp_dir().join(format!("gizmo_sandbox2_{}", std::process::id()));
1206 std::fs::create_dir_all(&dir).unwrap();
1207 let path = dir.join("s.lua");
1208 std::fs::write(
1209 &path,
1210 "function on_update(c)\n _G.MINE = 5\n print('mine=' .. tostring(MINE))\n print('api=' .. tostring(_G.input ~= nil and _G.print ~= nil))\n print('std=' .. tostring(string.rep('x', 2)))\nend\n",
1211 )
1212 .unwrap();
1213
1214 let mut engine = ScriptEngine::new().unwrap();
1215 engine.load_script(path.to_str().unwrap()).unwrap();
1216 engine.update(&World::new(), &Input::default(), 0.016).unwrap();
1217
1218 let log = engine.log_queue.lock().unwrap().clone();
1219 let said = |needle: &str| log.iter().any(|(_, m)| m.contains(needle));
1220 assert!(said("mine=5"), "a script's own `_G` write must be visible to itself: {log:?}");
1221 assert!(said("api=true"), "the engine API must still resolve through `_G`: {log:?}");
1222 assert!(said("std=xx"), "the Lua standard library must still resolve: {log:?}");
1223 std::fs::remove_dir_all(&dir).ok();
1224 }
1225
1226 #[test]
1237 fn a_script_cannot_rewrite_the_api_for_everyone_else() {
1238 let dir = std::env::temp_dir().join(format!("gizmo_api_ro_{}", std::process::id()));
1239 std::fs::create_dir_all(&dir).unwrap();
1240 let a = dir.join("a_vandal.lua");
1241 let b = dir.join("b_victim.lua");
1242 std::fs::write(
1243 &a,
1244 "function on_update(c)\n input.is_pressed = function(k) return 'CLOBBERED' end\nend\n",
1245 )
1246 .unwrap();
1247 std::fs::write(
1248 &b,
1249 "function on_update(c)\n print('sees=' .. tostring(input.is_pressed('w')))\nend\n",
1250 )
1251 .unwrap();
1252
1253 let mut engine = ScriptEngine::new().unwrap();
1254 engine.load_script(a.to_str().unwrap()).unwrap();
1255 engine.load_script(b.to_str().unwrap()).unwrap();
1256
1257 let err = engine.update(&World::new(), &Input::default(), 0.016).unwrap_err();
1259 assert!(err.contains("read-only"), "expected a read-only refusal, got: {err}");
1260
1261 let log = engine.log_queue.lock().unwrap().clone();
1262 assert!(
1263 log.iter().any(|(_, m)| m.contains("sees=false")),
1264 "the neighbour saw a rewritten API: {log:?}"
1265 );
1266 std::fs::remove_dir_all(&dir).ok();
1267 }
1268
1269 #[test]
1282 fn a_script_can_ask_a_question_the_engine_did_not_precompute() {
1283 use gizmo_physics_rigid::world::PhysicsWorld;
1284
1285 let dir = std::env::temp_dir().join(format!("gizmo_probe_{}", std::process::id()));
1286 std::fs::create_dir_all(&dir).unwrap();
1287 let path = dir.join("probe.lua");
1288 std::fs::write(
1289 &path,
1290 "function on_update(c)\n \x20 print('on_slab=' .. tostring(physics.ground_at(0.0, 0.0)))\n \x20 print('off_slab=' .. tostring(physics.ground_at(500.0, 500.0)))\n end\n",
1291 )
1292 .unwrap();
1293
1294 use gizmo_math::Vec3;
1296 use gizmo_physics_core::{BodyHandle, Collider, Transform};
1297 use gizmo_physics_rigid::{RigidBody, Velocity};
1298
1299 let mut world = World::new();
1300 let mut pw = PhysicsWorld::new();
1301 pw.add_body(
1302 BodyHandle::from_id(0),
1303 RigidBody::new_static(),
1304 Transform::new(Vec3::new(0.0, 0.0, 0.0)),
1305 Velocity::default(),
1306 Collider::box_collider(Vec3::new(50.0, 2.0, 50.0)),
1307 );
1308 world.insert_resource(pw);
1309
1310 let mut engine = ScriptEngine::new().unwrap();
1311 engine.load_script(path.to_str().unwrap()).unwrap();
1312 engine.update(&world, &Input::default(), 0.016).unwrap();
1313
1314 let log = engine.log_queue.lock().unwrap().clone();
1315 let line = |k: &str| {
1316 log.iter()
1317 .find_map(|(_, m)| m.strip_prefix(k).map(str::to_string))
1318 .unwrap_or_else(|| panic!("no `{k}` line in {log:?}"))
1319 };
1320 let on_slab: f32 = line("on_slab=").parse().expect("a height over the slab");
1321 assert!((on_slab - 2.0).abs() < 0.01, "expected the slab top at 2.0, got {on_slab}");
1322 assert_eq!(line("off_slab="), "nil", "no floor there must read as nil, not as zero");
1323 std::fs::remove_dir_all(&dir).ok();
1324 }
1325
1326 #[test]
1329 fn the_call_time_query_is_not_available_outside_the_frame() {
1330 let lua = Lua::new();
1331 crate::api_physics::register_physics_api(&lua, Arc::new(CommandQueue::new())).unwrap();
1332 let world = World::new();
1333
1334 crate::api_physics::with_call_time_queries(&lua, &world, || {
1335 let present: bool = lua.load("return physics.ground_at ~= nil").eval()?;
1336 assert!(present, "the query must exist while the frame is running");
1337 Ok(())
1338 })
1339 .unwrap();
1340
1341 let present: bool = lua.load("return physics.ground_at ~= nil").eval().unwrap();
1342 assert!(!present, "the query must be gone once the frame is over");
1343 }
1344
1345 #[test]
1352 fn an_infinite_loop_ends_the_call_instead_of_the_process() {
1353 let dir = std::env::temp_dir().join(format!("gizmo_budget_{}", std::process::id()));
1354 std::fs::create_dir_all(&dir).unwrap();
1355 let path = dir.join("runaway.lua");
1356 std::fs::write(&path, "function on_update(ctx)\n while true do end\nend\n").unwrap();
1357
1358 let mut engine = ScriptEngine::new().unwrap();
1359 engine.set_instruction_budget(200_000);
1361 engine.load_script(path.to_str().unwrap()).unwrap();
1362
1363 let world = World::new();
1364 let input = Input::default();
1365 let started = std::time::Instant::now();
1366 let err = engine.update(&world, &input, 0.016).unwrap_err();
1367 let took = started.elapsed();
1368
1369 assert!(err.contains("instruction budget"), "unexpected error: {err}");
1370 assert!(took.as_secs() < 5, "the guard took {took:?} — that is a hang with extra steps");
1371 std::fs::remove_dir_all(&dir).ok();
1372 }
1373
1374 #[test]
1377 fn a_runaway_script_does_not_spend_another_scripts_budget() {
1378 let dir = std::env::temp_dir().join(format!("gizmo_budget2_{}", std::process::id()));
1379 std::fs::create_dir_all(&dir).unwrap();
1380 let runaway = dir.join("a_runaway.lua");
1382 let neighbour = dir.join("b_neighbour.lua");
1383 std::fs::write(&runaway, "function on_update(ctx)\n while true do end\nend\n").unwrap();
1384 std::fs::write(&neighbour, "function on_update(ctx)\n print('neighbour ran')\nend\n")
1387 .unwrap();
1388
1389 let mut engine = ScriptEngine::new().unwrap();
1390 engine.set_instruction_budget(200_000);
1391 engine.load_script(runaway.to_str().unwrap()).unwrap();
1392 engine.load_script(neighbour.to_str().unwrap()).unwrap();
1393
1394 let world = World::new();
1395 let input = Input::default();
1396 let err = engine.update(&world, &input, 0.016).unwrap_err();
1397 assert!(err.contains("instruction budget"), "unexpected error: {err}");
1398
1399 let logged = engine
1400 .log_queue
1401 .lock()
1402 .unwrap()
1403 .iter()
1404 .any(|(_, m)| m.contains("neighbour ran"));
1405 assert!(logged, "the second script never got its turn");
1406 std::fs::remove_dir_all(&dir).ok();
1407 }
1408
1409 #[test]
1411 fn runaway_allocation_fails_as_a_lua_error() {
1412 let dir = std::env::temp_dir().join(format!("gizmo_mem_{}", std::process::id()));
1413 std::fs::create_dir_all(&dir).unwrap();
1414 let path = dir.join("hungry.lua");
1415 std::fs::write(
1416 &path,
1417 "function on_update(ctx)\n local t = {}\n while true do t[#t+1] = string.rep('x', 1024) end\nend\n",
1418 )
1419 .unwrap();
1420
1421 let mut engine = ScriptEngine::new().unwrap();
1422 engine.set_memory_limit(4 * 1024 * 1024).unwrap();
1423 engine.set_instruction_budget(500_000_000);
1425 engine.load_script(path.to_str().unwrap()).unwrap();
1426
1427 let err = engine.update(&World::new(), &Input::default(), 0.016).unwrap_err();
1428 assert!(
1429 err.to_lowercase().contains("memory"),
1430 "expected a memory error, got: {err}"
1431 );
1432 std::fs::remove_dir_all(&dir).ok();
1433 }
1434 use super::*;
1435 use gizmo_math::{Quat, Vec3};
1436 use gizmo_physics_core::{Collider, ColliderShape, Transform};
1437 use gizmo_physics_rigid::components::{RigidBody, Velocity};
1438
1439 fn unique_temp(tag: &str) -> String {
1441 use std::sync::atomic::{AtomicU64, Ordering};
1442 static N: AtomicU64 = AtomicU64::new(0);
1443 let n = N.fetch_add(1, Ordering::Relaxed);
1444 let nanos = std::time::SystemTime::now()
1445 .duration_since(std::time::UNIX_EPOCH)
1446 .unwrap()
1447 .as_nanos();
1448 std::env::temp_dir()
1449 .join(format!("gizmo_scripting_{tag}_{n}_{nanos}.lua"))
1450 .to_string_lossy()
1451 .into_owned()
1452 }
1453
1454 #[test]
1458 fn on_update_hook_fires_from_script_env() {
1459 let mut engine = ScriptEngine::new().unwrap();
1460 let world = World::new();
1461 let input = gizmo_core::input::Input::default();
1462
1463 let path = std::env::temp_dir()
1464 .join("gizmo_on_update_test.lua")
1465 .to_string_lossy()
1466 .into_owned();
1467 std::fs::write(&path, "function on_update(ctx)\n entity.spawn(\"bullet\", 0, 0, 0)\nend\n")
1468 .unwrap();
1469 engine.load_script(&path).expect("load_script");
1470
1471 let before = engine.command_queue().len();
1472 engine.update(&world, &input, 1.0 / 60.0).expect("update");
1473 let after = engine.command_queue().len();
1474 let _ = std::fs::remove_file(&path);
1475
1476 assert!(
1477 after > before,
1478 "on_update must run and queue a spawn command (before={before}, after={after})"
1479 );
1480 }
1481
1482 #[test]
1485 fn apply_force_creates_velocity_when_missing() {
1486 let engine = ScriptEngine::new().unwrap();
1487 let mut world = World::new();
1488
1489 let entity = world.spawn();
1490 world.add_component(entity, RigidBody::new(2.0, false));
1491 assert!(world.borrow::<Velocity>().get(entity.id()).is_none());
1493
1494 engine
1495 .command_queue()
1496 .push(ScriptCommand::ApplyForce(entity.id(), Vec3::new(4.0, 0.0, 0.0)));
1497
1498 let dt = 0.5_f32;
1499 engine.flush_commands(&mut world, dt);
1500
1501 let vels = world.borrow::<Velocity>();
1502 let v = vels
1503 .get(entity.id())
1504 .expect("Velocity ApplyForce tarafından oluşturulmalıydı");
1505 assert!((v.linear.x - 1.0).abs() < 1e-5, "x hızı yanlış: {}", v.linear.x);
1507 }
1508
1509 #[test]
1512 fn apply_impulse_creates_velocity_when_missing() {
1513 let engine = ScriptEngine::new().unwrap();
1514 let mut world = World::new();
1515
1516 let entity = world.spawn();
1517 world.add_component(entity, RigidBody::new(2.0, false));
1518 assert!(world.borrow::<Velocity>().get(entity.id()).is_none());
1519
1520 engine
1521 .command_queue()
1522 .push(ScriptCommand::ApplyImpulse(entity.id(), Vec3::new(6.0, 0.0, 0.0)));
1523
1524 engine.flush_commands(&mut world, 0.016);
1525
1526 let vels = world.borrow::<Velocity>();
1527 let v = vels
1528 .get(entity.id())
1529 .expect("Velocity ApplyImpulse tarafından oluşturulmalıydı");
1530 assert!((v.linear.x - 3.0).abs() < 1e-5, "x hızı yanlış: {}", v.linear.x);
1532 }
1533
1534 #[test]
1537 fn transform_commands_apply_to_component() {
1538 let engine = ScriptEngine::new().unwrap();
1539 let mut world = World::new();
1540 let e = world.spawn();
1541 world.add_component(e, Transform::new(Vec3::ZERO));
1542 let id = e.id();
1543
1544 engine.command_queue().push(ScriptCommand::SetPosition(id, Vec3::new(1.0, 2.0, 3.0)));
1545 engine.command_queue().push(ScriptCommand::SetScale(id, Vec3::new(2.0, 4.0, 8.0)));
1546 engine.command_queue().push(ScriptCommand::SetRotation(id, Quat::from_xyzw(1.0, 0.0, 0.0, 0.0)));
1547 engine.flush_commands(&mut world, 0.016);
1548
1549 let transforms = world.borrow::<Transform>();
1550 let t = transforms.get(id).unwrap();
1551 assert_eq!(t.position, Vec3::new(1.0, 2.0, 3.0));
1552 assert_eq!(t.scale, Vec3::new(2.0, 4.0, 8.0));
1553 assert!((t.rotation.x - 1.0).abs() < 1e-6 && t.rotation.w.abs() < 1e-6);
1554 }
1555
1556 #[test]
1558 fn velocity_commands_apply_to_component() {
1559 let engine = ScriptEngine::new().unwrap();
1560 let mut world = World::new();
1561 let e = world.spawn();
1562 world.add_component(e, Velocity::new(Vec3::ZERO));
1563 let id = e.id();
1564
1565 engine.command_queue().push(ScriptCommand::SetVelocity(id, Vec3::new(3.0, 0.0, -2.0)));
1566 engine.command_queue().push(ScriptCommand::SetAngularVelocity(id, Vec3::new(0.0, 1.0, 0.0)));
1567 engine.flush_commands(&mut world, 0.016);
1568
1569 let vels = world.borrow::<Velocity>();
1570 let v = vels.get(id).unwrap();
1571 assert_eq!(v.linear, Vec3::new(3.0, 0.0, -2.0));
1572 assert_eq!(v.angular, Vec3::new(0.0, 1.0, 0.0));
1573 }
1574
1575 #[test]
1578 fn apply_force_on_zero_mass_creates_no_velocity() {
1579 let engine = ScriptEngine::new().unwrap();
1580 let mut world = World::new();
1581 let e = world.spawn();
1582 world.add_component(e, RigidBody::new(0.0, false));
1583 let id = e.id();
1584
1585 engine.command_queue().push(ScriptCommand::ApplyForce(id, Vec3::new(100.0, 0.0, 0.0)));
1586 engine.flush_commands(&mut world, 0.016);
1587
1588 assert!(
1589 world.borrow::<Velocity>().get(id).is_none(),
1590 "sıfır kütle için Velocity oluşturulmamalı"
1591 );
1592 }
1593
1594 #[test]
1596 fn multiple_forces_accumulate_in_one_flush() {
1597 let engine = ScriptEngine::new().unwrap();
1598 let mut world = World::new();
1599 let e = world.spawn();
1600 world.add_component(e, RigidBody::new(2.0, false));
1601 world.add_component(e, Velocity::new(Vec3::ZERO));
1602 let id = e.id();
1603
1604 engine.command_queue().push(ScriptCommand::ApplyForce(id, Vec3::new(4.0, 0.0, 0.0)));
1605 engine.command_queue().push(ScriptCommand::ApplyForce(id, Vec3::new(0.0, 6.0, 0.0)));
1606 engine.flush_commands(&mut world, 0.5);
1607
1608 let vels = world.borrow::<Velocity>();
1609 let v = vels.get(id).unwrap();
1610 assert!((v.linear.x - 1.0).abs() < 1e-5, "x: {}", v.linear.x);
1612 assert!((v.linear.y - 1.5).abs() < 1e-5, "y: {}", v.linear.y);
1613 }
1614
1615 #[test]
1617 fn add_rigidbody_also_creates_velocity() {
1618 let engine = ScriptEngine::new().unwrap();
1619 let mut world = World::new();
1620 let e = world.spawn();
1621 let id = e.id();
1622
1623 engine.command_queue().push(ScriptCommand::AddRigidBody { id, mass: 3.0, use_gravity: true });
1624 engine.flush_commands(&mut world, 0.016);
1625
1626 let rbs = world.borrow::<RigidBody>();
1627 assert!((rbs.get(id).unwrap().mass - 3.0).abs() < 1e-6);
1628 drop(rbs);
1629 assert!(
1630 world.borrow::<Velocity>().get(id).is_some(),
1631 "AddRigidBody Velocity de eklemeli"
1632 );
1633 }
1634
1635 #[test]
1637 fn colliders_are_created_with_correct_shape() {
1638 let engine = ScriptEngine::new().unwrap();
1639 let mut world = World::new();
1640 let e_box = world.spawn();
1641 let e_sphere = world.spawn();
1642 let (bid, sid) = (e_box.id(), e_sphere.id());
1643
1644 engine.command_queue().push(ScriptCommand::AddBoxCollider { id: bid, hx: 1.0, hy: 2.0, hz: 3.0 });
1645 engine.command_queue().push(ScriptCommand::AddSphereCollider { id: sid, radius: 4.0 });
1646 engine.flush_commands(&mut world, 0.016);
1647
1648 let cols = world.borrow::<Collider>();
1649 match &cols.get(bid).unwrap().shape {
1650 ColliderShape::Box(b) => assert_eq!(b.half_extents, Vec3::new(1.0, 2.0, 3.0)),
1651 other => panic!("beklenen Box, gelen {other:?}"),
1652 }
1653 match &cols.get(sid).unwrap().shape {
1654 ColliderShape::Sphere(s) => assert!((s.radius - 4.0).abs() < 1e-6),
1655 other => panic!("beklenen Sphere, gelen {other:?}"),
1656 }
1657 }
1658
1659 #[test]
1661 fn spawn_entity_creates_named_transform_and_logs() {
1662 let engine = ScriptEngine::new().unwrap();
1663 let mut world = World::new();
1664
1665 let logs_before = engine.log_queue.lock().unwrap().len();
1666 engine
1667 .command_queue()
1668 .push(ScriptCommand::SpawnEntity { name: "hero".into(), position: Vec3::new(5.0, 6.0, 7.0) });
1669 engine.flush_commands(&mut world, 0.016);
1670
1671 let names = world.borrow::<gizmo_core::EntityName>();
1673 let found = names.iter().filter_map(|(eid, _)| names.get(eid).map(|n| (eid, n.0.clone())))
1674 .find(|(_, name)| name == "hero");
1675 let (eid, _) = found.expect("'hero' isimli entity oluşmalıydı");
1676 drop(names);
1677
1678 let transforms = world.borrow::<Transform>();
1679 assert_eq!(transforms.get(eid).unwrap().position, Vec3::new(5.0, 6.0, 7.0));
1680 drop(transforms);
1681
1682 assert!(
1683 engine.log_queue.lock().unwrap().len() > logs_before,
1684 "spawn log kuyruğuna kayıt düşmeliydi"
1685 );
1686 }
1687
1688 #[test]
1690 fn destroy_entity_removes_it() {
1691 let engine = ScriptEngine::new().unwrap();
1692 let mut world = World::new();
1693 let e = world.spawn();
1694 let id = e.id();
1695 assert!(world.entity(id).is_some());
1696
1697 engine.command_queue().push(ScriptCommand::DestroyEntity(id));
1698 engine.flush_commands(&mut world, 0.016);
1699
1700 assert!(world.entity(id).is_none(), "entity despawn edilmeliydi");
1701 }
1702
1703 #[test]
1705 fn set_entity_name_renames() {
1706 let engine = ScriptEngine::new().unwrap();
1707 let mut world = World::new();
1708 let e = world.spawn();
1709 world.add_component(e, gizmo_core::EntityName::new("old"));
1710 let id = e.id();
1711
1712 engine.command_queue().push(ScriptCommand::SetEntityName(id, "new".into()));
1713 engine.flush_commands(&mut world, 0.016);
1714
1715 let names = world.borrow::<gizmo_core::EntityName>();
1716 assert_eq!(names.get(id).unwrap().0, "new");
1717 }
1718
1719 #[test]
1721 fn nav_agent_target_set_then_cleared() {
1722 use gizmo_ai::components::NavAgent;
1723 let engine = ScriptEngine::new().unwrap();
1724 let mut world = World::new();
1725 let e = world.spawn();
1726 let id = e.id();
1727
1728 engine.command_queue().push(ScriptCommand::AddNavAgent(id));
1729 engine.command_queue().push(ScriptCommand::SetAiTarget(id, Vec3::new(9.0, 0.0, 0.0)));
1730 engine.flush_commands(&mut world, 0.016);
1731 {
1732 let agents = world.borrow::<NavAgent>();
1733 assert_eq!(agents.get(id).unwrap().target, Some(Vec3::new(9.0, 0.0, 0.0)));
1734 }
1735
1736 engine.command_queue().push(ScriptCommand::ClearAiTarget(id));
1737 engine.flush_commands(&mut world, 0.016);
1738 {
1739 let agents = world.borrow::<NavAgent>();
1740 assert_eq!(agents.get(id).unwrap().target, None, "hedef temizlenmeliydi");
1741 }
1742 }
1743
1744 #[test]
1753 fn flush_returns_everything_it_cannot_apply_itself() {
1754 let engine = ScriptEngine::new().unwrap();
1755 let mut world = World::new();
1756
1757 let cq = engine.command_queue();
1758 cq.push(ScriptCommand::PlaySound("boom".into()));
1759 cq.push(ScriptCommand::PlaySound3D("bird".into(), Vec3::ZERO));
1760 cq.push(ScriptCommand::StopSound("music".into()));
1761 cq.push(ScriptCommand::LoadScene("level.scene".into()));
1762 cq.push(ScriptCommand::SaveScene("slot.scene".into()));
1763 cq.push(ScriptCommand::SetVehicleBrake(1, 500.0));
1764
1765 let unhandled = engine.flush_commands(&mut world, 0.016);
1766
1767 assert_eq!(unhandled.len(), 6, "ses(3) + LoadScene + SaveScene + araç(1) — hepsi dönmeli");
1768 assert!(unhandled.iter().any(|c| matches!(c, ScriptCommand::PlaySound(n) if n == "boom")));
1769 assert!(unhandled.iter().any(|c| matches!(c, ScriptCommand::PlaySound3D(n, _) if n == "bird")));
1770 assert!(unhandled.iter().any(|c| matches!(c, ScriptCommand::StopSound(n) if n == "music")));
1771 assert!(unhandled.iter().any(|c| matches!(c, ScriptCommand::LoadScene(n) if n == "level.scene")));
1772 assert!(
1773 unhandled.iter().any(|c| matches!(c, ScriptCommand::SaveScene(n) if n == "slot.scene")),
1774 "SaveScene sessizce yutulmamalı"
1775 );
1776 assert!(
1777 unhandled.iter().any(|c| matches!(c, ScriptCommand::SetVehicleBrake(1, _))),
1778 "araç komutları sessizce yutulmamalı — bu crate onları uygulayamıyor, ev sahibi uygular"
1779 );
1780 }
1781
1782 #[test]
1790 fn scripts_run_in_a_stable_order() {
1791 let mut engine = ScriptEngine::new().unwrap();
1792 let dir = std::env::temp_dir();
1793 let mut written = Vec::new();
1796 for stem in ["zebra", "alpha", "midori", "beta"] {
1797 let path = dir
1798 .join(format!("gizmo_order_{stem}.lua"))
1799 .to_string_lossy()
1800 .into_owned();
1801 std::fs::write(&path, "function on_update(ctx) end\n").unwrap();
1802 engine.load_script(&path).unwrap_or_else(|e| panic!("{path}: {e}"));
1803 written.push(path);
1804 }
1805
1806 let order: Vec<String> = engine.loaded_scripts.keys().cloned().collect();
1807 let mut sorted = order.clone();
1808 sorted.sort();
1809 assert_eq!(
1810 order, sorted,
1811 "çalışma sırası yola göre sabit olmalı — bir HashMap'te bu proses başına değişirdi"
1812 );
1813
1814 for path in written {
1815 let _ = std::fs::remove_file(path);
1816 }
1817 }
1818
1819 #[test]
1821 fn flush_drains_the_queue() {
1822 let engine = ScriptEngine::new().unwrap();
1823 let mut world = World::new();
1824 engine.command_queue().push(ScriptCommand::StartRace);
1825 engine.command_queue().push(ScriptCommand::HideDialogue);
1826 assert_eq!(engine.command_queue().len(), 2);
1827
1828 engine.flush_commands(&mut world, 0.016);
1829 assert!(engine.command_queue().is_empty(), "flush kuyruğu boşaltmalı");
1830 }
1831
1832 #[test]
1834 fn script_new_starts_uninitialized() {
1835 let s = Script::new("scripts/player.lua");
1836 assert_eq!(s.file_path, "scripts/player.lua");
1837 assert!(!s.initialized);
1838 }
1839
1840 #[test]
1849 fn declared_properties_are_read_from_the_script() {
1850 let mut engine = ScriptEngine::new().unwrap();
1851 let path = unique_temp("declared_props");
1852 std::fs::write(
1853 &path,
1854 r#"
1855properties = {
1856 open_speed = 2.4,
1857 locked = false,
1858 label = "gate",
1859 nested = { nope = 1 },
1860}
1861"#,
1862 )
1863 .unwrap();
1864 engine.load_script(&path).unwrap();
1865
1866 let declared = engine.declared_properties(&path);
1867 assert_eq!(declared.get("open_speed"), Some(&ScriptValue::Num(2.4)));
1868 assert_eq!(declared.get("locked"), Some(&ScriptValue::Bool(false)));
1869 assert_eq!(declared.get("label"), Some(&ScriptValue::Text("gate".into())));
1870 assert!(
1871 !declared.contains_key("nested"),
1872 "a table is not an inspector row and must not be guessed at"
1873 );
1874 let _ = std::fs::remove_file(&path);
1875 }
1876
1877 #[test]
1879 fn a_script_without_properties_declares_none() {
1880 let mut engine = ScriptEngine::new().unwrap();
1881 let path = unique_temp("no_props");
1882 std::fs::write(&path, "function on_entity_update(id, dt, props) end\n").unwrap();
1883 engine.load_script(&path).unwrap();
1884 assert!(engine.declared_properties(&path).is_empty());
1885 let _ = std::fs::remove_file(&path);
1886 }
1887
1888 #[test]
1895 fn each_entity_sees_its_own_property_values() {
1896 let mut engine = ScriptEngine::new().unwrap();
1897 let path = unique_temp("per_entity_props");
1898 std::fs::write(
1899 &path,
1900 r#"
1901seen = {}
1902function on_entity_update(id, dt, props)
1903 seen[id] = props.open_speed
1904end
1905"#,
1906 )
1907 .unwrap();
1908 engine.load_script(&path).unwrap();
1909
1910 let mut a = std::collections::BTreeMap::new();
1911 a.insert("open_speed".to_string(), ScriptValue::Num(1.5));
1912 let mut b = std::collections::BTreeMap::new();
1913 b.insert("open_speed".to_string(), ScriptValue::Num(9.25));
1914
1915 engine.update_entity(1, &path, 0.016, &a).unwrap();
1916 engine.update_entity(2, &path, 0.016, &b).unwrap();
1917
1918 let seen_1 = engine.eval_number(&path, "seen[1]").expect("entity 1 value");
1919 let seen_2 = engine.eval_number(&path, "seen[2]").expect("entity 2 value");
1920 assert_eq!(seen_1, 1.5);
1921 assert_eq!(
1922 seen_2, 9.25,
1923 "the second entity saw the first one's value — the properties are being shared"
1924 );
1925 let _ = std::fs::remove_file(&path);
1926 }
1927
1928 #[test]
1942 fn every_stored_property_reaches_the_script_declared_or_not() {
1943 let mut engine = ScriptEngine::new().unwrap();
1944 let path = unique_temp("undeclared_props");
1945 std::fs::write(
1946 &path,
1947 r#"
1948properties = { open_speed = 2.4, locked = false }
1949seen_speed = nil
1950seen_locked_is_string = nil
1951seen_undeclared = nil
1952function on_entity_update(id, dt, props)
1953 seen_speed = props.open_speed
1954 seen_locked_is_string = (type(props.locked) == "string") and 1 or 0
1955 seen_undeclared = props.nobody_declared_me
1956end
1957"#,
1958 )
1959 .unwrap();
1960 engine.load_script(&path).unwrap();
1961
1962 let declared = engine.declared_properties(&path);
1964 assert_eq!(declared.get("locked").map(|v| v.kind()), Some("bool"));
1965 assert!(!declared.contains_key("nobody_declared_me"));
1966
1967 let mut stored = std::collections::BTreeMap::new();
1968 stored.insert("open_speed".to_string(), ScriptValue::Num(7.5));
1969 stored.insert("locked".to_string(), ScriptValue::Text("yes".to_string()));
1971 stored.insert("nobody_declared_me".to_string(), ScriptValue::Num(42.0));
1973
1974 engine.update_entity(1, &path, 0.016, &stored).unwrap();
1975
1976 assert_eq!(engine.eval_number(&path, "seen_speed"), Some(7.5));
1977 assert_eq!(
1978 engine.eval_number(&path, "seen_locked_is_string"),
1979 Some(1.0),
1980 "the type-mismatched override is handed to the script verbatim — it is NOT ignored"
1981 );
1982 assert_eq!(
1983 engine.eval_number(&path, "seen_undeclared"),
1984 Some(42.0),
1985 "an undeclared key reaches the script too, so the editor must not pretend it is absent"
1986 );
1987 let _ = std::fs::remove_file(&path);
1988 }
1989
1990 #[test]
1991 fn script_serde_roundtrip_resets_initialized() {
1992 let mut s = Script::new("a.lua");
1993 s.initialized = true;
1994
1995 let json = serde_json::to_string(&s).unwrap();
1996 assert!(!json.contains("initialized"), "skip'li alan JSON'da olmamalı: {json}");
1997
1998 let back: Script = serde_json::from_str(&json).unwrap();
1999 assert_eq!(back.file_path, "a.lua");
2000 assert!(!back.initialized, "deserialize sonrası initialized=false olmalı");
2001 }
2002
2003 #[test]
2006 fn sandbox_disables_dangerous_globals() {
2007 let mut engine = ScriptEngine::new().unwrap();
2008 let path = unique_temp("sandbox");
2009 std::fs::write(
2010 &path,
2011 r#"
2012 assert(os == nil, "os kapatılmalı")
2013 assert(io == nil, "io kapatılmalı")
2014 assert(require == nil, "require kapatılmalı")
2015 assert(dofile == nil, "dofile kapatılmalı")
2016 assert(loadfile == nil, "loadfile kapatılmalı")
2017 assert(package == nil, "package kapatılmalı")
2018 assert(debug == nil, "debug kapatılmalı")
2019 assert(load == nil, "load kapatılmalı")
2020 assert(loadstring == nil, "loadstring kapatılmalı")
2021 "#,
2022 )
2023 .unwrap();
2024 let res = engine.load_script(&path);
2025 let _ = std::fs::remove_file(&path);
2026 res.expect("sandbox assert'leri geçmeli (global'ler nil olmalı)");
2027 }
2028
2029 #[test]
2031 fn lua_math_helpers_are_correct() {
2032 let mut engine = ScriptEngine::new().unwrap();
2033 let path = unique_temp("mathhelpers");
2034 std::fs::write(
2035 &path,
2036 r#"
2037 assert(math.abs(vec3_length(vec3(3,4,0)) - 5.0) < 1e-5, "length 3-4-5")
2038 local c = vec3_cross(vec3(1,0,0), vec3(0,1,0))
2039 assert(c.x == 0 and c.y == 0 and c.z == 1, "x cross y = z")
2040 assert(clamp(5, 0, 3) == 3, "clamp üst sınır")
2041 assert(clamp(-1, 0, 3) == 0, "clamp alt sınır")
2042 assert(clamp(2, 0, 3) == 2, "clamp aralık içi")
2043 assert(lerp(0, 10, 0.5) == 5, "lerp orta")
2044 local n = vec3_normalize(vec3(0,0,0))
2045 assert(n.x == 0 and n.y == 0 and n.z == 0, "sıfır vektör normalize => sıfır")
2046 assert(math.abs(vec3_distance(vec3(0,0,0), vec3(0,3,4)) - 5.0) < 1e-5, "distance")
2047 local d = vec3_dot(vec3(1,2,3), vec3(4,5,6))
2048 assert(d == 32, "dot 1*4+2*5+3*6=32")
2049 "#,
2050 )
2051 .unwrap();
2052 let res = engine.load_script(&path);
2053 let _ = std::fs::remove_file(&path);
2054 res.expect("matematik yardımcı assert'leri geçmeli");
2055 }
2056
2057 #[test]
2059 fn load_missing_file_returns_error() {
2060 let mut engine = ScriptEngine::new().unwrap();
2061 let err = engine
2062 .load_script("/nonexistent/gizmo/definitely_missing_5f2a.lua")
2063 .unwrap_err();
2064 assert!(err.contains("okunamadı"), "okuma hatası mesajı beklenir, gelen: {err}");
2065 }
2066
2067 #[test]
2069 fn run_entity_update_on_unloaded_script_errors() {
2070 let mut engine = ScriptEngine::new().unwrap();
2071 let ctx = ScriptContext::default();
2072 let err = engine
2073 .run_entity_update("never_loaded.lua", "on_entity_update", &ctx)
2074 .unwrap_err();
2075 assert!(err.contains("not loaded"), "mesaj: {err}");
2076 }
2077
2078 #[test]
2081 fn run_entity_update_marshals_position_and_extracts_result() {
2082 let mut engine = ScriptEngine::new().unwrap();
2083 let path = unique_temp("marshal_pos");
2084 std::fs::write(
2085 &path,
2086 "function mv(ctx)\n return { position = { x = ctx.position.x + ctx.dt, y = ctx.position.y, z = ctx.position.z } }\nend\n",
2087 )
2088 .unwrap();
2089 engine.load_script(&path).unwrap();
2090
2091 let ctx = ScriptContext {
2092 entity_id: 42,
2093 dt: 0.5,
2094 position: [10.0, -1.0, 2.0],
2095 ..Default::default()
2096 };
2097
2098 let result = engine.run_entity_update(&path, "mv", &ctx).unwrap();
2099 assert_eq!(result.new_position, Some([10.5, -1.0, 2.0]));
2100 assert_eq!(result.new_velocity, None, "script velocity döndürmedi");
2101
2102 let empty = engine.run_entity_update(&path, "yok_boyle_fn", &ctx).unwrap();
2104 assert_eq!(empty.new_position, None);
2105 assert_eq!(empty.new_velocity, None);
2106
2107 let _ = std::fs::remove_file(&path);
2108 }
2109
2110 #[test]
2113 fn run_entity_update_marshals_input_flags() {
2114 let mut engine = ScriptEngine::new().unwrap();
2115 let path = unique_temp("marshal_input");
2116 std::fs::write(
2117 &path,
2118 "function ctl(ctx)\n local vx = 0\n if ctx.input.d then vx = 1 end\n if ctx.input.a then vx = vx - 1 end\n return { velocity = { x = vx, y = 0, z = 0 } }\nend\n",
2119 )
2120 .unwrap();
2121 engine.load_script(&path).unwrap();
2122
2123 let mut ctx = ScriptContext {
2124 key_d: true, ..Default::default()
2126 };
2127 let r = engine.run_entity_update(&path, "ctl", &ctx).unwrap();
2128 assert_eq!(r.new_velocity, Some([1.0, 0.0, 0.0]));
2129
2130 ctx.key_d = false;
2131 ctx.key_a = true; let r2 = engine.run_entity_update(&path, "ctl", &ctx).unwrap();
2133 assert_eq!(r2.new_velocity, Some([-1.0, 0.0, 0.0]));
2134
2135 let _ = std::fs::remove_file(&path);
2136 }
2137
2138 #[test]
2140 fn has_function_detects_defined_and_missing() {
2141 let mut engine = ScriptEngine::new().unwrap();
2142 let path = unique_temp("hasfn");
2143 std::fs::write(&path, "function on_update(ctx) end\n").unwrap();
2144 engine.load_script(&path).unwrap();
2145
2146 assert!(engine.has_function(&path, "on_update"));
2147 assert!(!engine.has_function(&path, "on_missing"));
2148 assert!(!engine.has_function("unloaded.lua", "on_update"));
2149
2150 let _ = std::fs::remove_file(&path);
2151 }
2152
2153 #[test]
2156 fn reload_if_changed_detects_content_change() {
2157 let mut engine = ScriptEngine::new().unwrap();
2158 let path = unique_temp("reload");
2159 std::fs::write(&path, "function on_update(ctx) end\n").unwrap();
2160 engine.load_script(&path).unwrap();
2161
2162 assert!(!engine.reload_if_changed(&path).unwrap(), "değişmemişken false");
2163
2164 std::fs::write(&path, "function on_update(ctx) end\n-- değişti\n").unwrap();
2165 assert!(engine.reload_if_changed(&path).unwrap(), "değişince true");
2166
2167 assert!(!engine.reload_if_changed(&path).unwrap(), "tekrar değişmemişken false");
2168
2169 let _ = std::fs::remove_file(&path);
2170 }
2171}