Skip to main content

gizmo_scripting/
engine.rs

1use gizmo_core::input::Input;
2use gizmo_core::World;
3use mlua::prelude::*;
4use mlua::RegistryKey;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::api_ai;
9use crate::api_audio;
10use crate::api_entity;
11use crate::api_fighter;
12use crate::api_input;
13use crate::api_physics;
14use crate::api_scene;
15use crate::api_time;
16use crate::api_vehicle;
17use crate::commands::{CommandQueue, ScriptCommand};
18
19/// Lua Scripting Motoru — Genişletilmiş API ile oyun mantığını yönetir
20pub struct ScriptEngine {
21    lua: Lua,
22    loaded_scripts: HashMap<String, (String, RegistryKey)>,
23    command_queue: Arc<CommandQueue>,
24    elapsed_time: f32,
25    /// Log messages emitted from Lua (`print`), stored as `(level, message)` pairs.
26    pub log_queue: Arc<std::sync::Mutex<Vec<(String, String)>>>, // (Level, Message)
27}
28
29unsafe impl Send for ScriptEngine {}
30unsafe impl Sync for ScriptEngine {}
31
32// `Lua` does not implement `Debug`, so the engine provides a manual summary that
33// omits the VM internals while still surfacing useful state.
34impl std::fmt::Debug for ScriptEngine {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("ScriptEngine")
37            .field("lua", &"<Lua VM>")
38            .field("loaded_scripts", &self.loaded_scripts.keys())
39            .field("elapsed_time", &self.elapsed_time)
40            .field(
41                "queued_commands",
42                &self.command_queue.len(),
43            )
44            .field(
45                "queued_logs",
46                &self.log_queue.lock().map(|q| q.len()).unwrap_or(0),
47            )
48            .finish()
49    }
50}
51
52/// ECS Componenti: Varlığın üzerine hangi Lua script'inin takılı olduğunu tutar
53#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
54pub struct Script {
55    pub file_path: String,
56    #[serde(default, skip)]
57    pub initialized: bool, // on_init çağrıldı mı?
58}
59
60impl Script {
61    pub fn new(path: &str) -> Self {
62        Self {
63            file_path: path.to_string(),
64            initialized: false,
65        }
66    }
67}
68
69/// Lua'ya geçirilecek entity verisi (geriye dönük uyumluluk için)
70#[derive(Clone, Debug, Default)]
71#[non_exhaustive]
72pub struct ScriptContext {
73    pub entity_id: u32,
74    pub dt: f32,
75    pub position: [f32; 3],
76    pub velocity: [f32; 3],
77    pub key_w: bool,
78    pub key_a: bool,
79    pub key_s: bool,
80    pub key_d: bool,
81    pub key_space: bool,
82    pub key_up: bool,
83    pub key_down: bool,
84    pub key_left: bool,
85    pub key_right: bool,
86}
87
88/// Lua'dan dönen değişiklikler (geriye dönük uyumluluk)
89#[derive(Clone, Debug, Default)]
90pub struct ScriptResult {
91    pub new_position: Option<[f32; 3]>,
92    pub new_velocity: Option<[f32; 3]>,
93}
94
95impl ScriptEngine {
96    pub fn new() -> Result<Self, LuaError> {
97        let lua = Lua::new();
98        let command_queue = Arc::new(CommandQueue::new());
99        let log_queue = Arc::new(std::sync::Mutex::new(Vec::new()));
100
101        // === SANDBOX: Tehlikeli modülleri kapat ===
102        lua.globals().set("os", LuaNil)?;
103        lua.globals().set("io", LuaNil)?;
104        lua.globals().set("loadfile", LuaNil)?;
105        lua.globals().set("dofile", LuaNil)?;
106        lua.globals().set("require", LuaNil)?;
107        lua.globals().set("package", LuaNil)?;
108        lua.globals().set("debug", LuaNil)?;
109        lua.globals().set("loadstring", LuaNil)?;
110        lua.globals().set("load", LuaNil)?;
111
112        // === TEMEL PRINT FONKSİYONU ===
113        let lq_clone1 = log_queue.clone();
114        lua.globals().set(
115            "print_engine",
116            lua.create_function(move |_, msg: String| {
117                if let Ok(mut q) = lq_clone1.lock() {
118                    q.push(("info".to_string(), msg));
119                }
120                Ok(())
121            })?,
122        )?;
123
124        // Orijinal print'i de engine çıktısına yönlendir
125        let lq_clone2 = log_queue.clone();
126        lua.globals().set(
127            "print",
128            lua.create_function(move |_, values: LuaMultiValue| {
129                let parts: Vec<String> = values
130                    .iter()
131                    .map(|v| {
132                        if let mlua::Value::String(s) = v {
133                            s.to_str().unwrap_or("").to_string()
134                        } else if let mlua::Value::Number(n) = v {
135                            n.to_string()
136                        } else if let mlua::Value::Integer(i) = v {
137                            i.to_string()
138                        } else if let mlua::Value::Boolean(b) = v {
139                            b.to_string()
140                        } else {
141                            format!("{:?}", v)
142                        }
143                    })
144                    .collect();
145                if let Ok(mut q) = lq_clone2.lock() {
146                    q.push(("info".to_string(), parts.join("\t")));
147                }
148                Ok(())
149            })?,
150        )?;
151
152        // === VEC3 YARDIMCI FONKSİYONLARI ===
153        lua.load(
154            r#"
155            function vec3(x, y, z)
156                return { x = x or 0, y = y or 0, z = z or 0 }
157            end
158            
159            function vec3_add(a, b)
160                return vec3(a.x + b.x, a.y + b.y, a.z + b.z)
161            end
162            
163            function vec3_sub(a, b)
164                return vec3(a.x - b.x, a.y - b.y, a.z - b.z)
165            end
166            
167            function vec3_scale(v, s)
168                return vec3(v.x * s, v.y * s, v.z * s)
169            end
170            
171            function vec3_length(v)
172                return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
173            end
174            
175            function vec3_normalize(v)
176                local len = vec3_length(v)
177                if len > 0.0001 then
178                    return vec3(v.x / len, v.y / len, v.z / len)
179                end
180                return vec3(0, 0, 0)
181            end
182            
183            function vec3_dot(a, b)
184                return a.x * b.x + a.y * b.y + a.z * b.z
185            end
186            
187            function vec3_cross(a, b)
188                return vec3(
189                    a.y * b.z - a.z * b.y,
190                    a.z * b.x - a.x * b.z,
191                    a.x * b.y - a.y * b.x
192                )
193            end
194            
195            function vec3_lerp(a, b, t)
196                return vec3(
197                    a.x + (b.x - a.x) * t,
198                    a.y + (b.y - a.y) * t,
199                    a.z + (b.z - a.z) * t
200                )
201            end
202            
203            function vec3_distance(a, b)
204                return vec3_length(vec3_sub(a, b))
205            end
206            
207            -- Clamp utility
208            function clamp(value, min, max)
209                return math.max(min, math.min(max, value))
210            end
211            
212            -- Lerp utility
213            function lerp(a, b, t)
214                return a + (b - a) * t
215            end
216        "#,
217        )
218        .exec()?;
219
220        // === API MODÜLLERİNİ KAYDET ===
221        api_entity::register_entity_api(&lua, command_queue.clone())?;
222        api_fighter::register_fighter_api(&lua, command_queue.clone())?;
223        api_input::register_input_api(&lua)?;
224        api_physics::register_physics_api(&lua, command_queue.clone())?;
225        api_scene::register_scene_api(&lua, command_queue.clone())?;
226        api_audio::register_audio_api(&lua, command_queue.clone())?;
227        api_time::register_time_api(&lua)?;
228        api_vehicle::register_vehicle_api(&lua, command_queue.clone())?;
229        api_ai::register_ai_api(&lua, command_queue.clone())?;
230
231        Ok(Self {
232            lua,
233            loaded_scripts: HashMap::new(),
234            command_queue,
235            elapsed_time: 0.0,
236            log_queue,
237        })
238    }
239
240    pub fn load_script(&mut self, path: &str) -> Result<(), String> {
241        let content = std::fs::read_to_string(path)
242            .map_err(|e| format!("Script okunamadı {}: {}", path, e))?;
243
244        let env = self.lua.create_table().map_err(|e| e.to_string())?;
245
246        // Link to _G via metatable
247        let meta = self.lua.create_table().map_err(|e| e.to_string())?;
248        meta.set("__index", self.lua.globals())
249            .map_err(|e| e.to_string())?;
250        env.set_metatable(Some(meta));
251
252        // Script'i İzole env içinde çalıştır
253        self.lua
254            .load(&content)
255            .set_environment(env.clone())
256            .exec()
257            .map_err(|e| format!("Lua hata {}: {}", path, e))?;
258
259        let key = self
260            .lua
261            .create_registry_value(env)
262            .map_err(|e| e.to_string())?;
263
264        // Replace existing key if it exists to free old memory
265        if let Some((_, old_key)) = self.loaded_scripts.insert(path.to_string(), (content, key)) {
266            let _ = self.lua.remove_registry_value(old_key);
267        }
268
269        tracing::info!("🔧 ScriptEngine: Yüklendi ve İzole Edildi → {}", path);
270        Ok(())
271    }
272
273    /// Her frame çağrılan güncelleme — World verilerini Lua'ya aktarır, scriptleri çalıştırır
274    pub fn update(&mut self, world: &World, input: &Input, dt: f32) -> Result<(), String> {
275        self.elapsed_time += dt;
276
277        // 1. World verilerini Lua'ya aktar (read snapshot)
278        api_entity::update_entity_read_api(&self.lua, world)
279            .map_err(|e| format!("Entity API güncelleme hatası: {}", e))?;
280        api_fighter::update_fighter_read_api(&self.lua, world)
281            .map_err(|e| format!("Fighter API güncelleme hatası: {}", e))?;
282        api_input::update_input_api(&self.lua, input)
283            .map_err(|e| format!("Input API güncelleme hatası: {}", e))?;
284        api_scene::update_scene_api(&self.lua, world)
285            .map_err(|e| format!("Scene API güncelleme hatası: {}", e))?;
286        api_time::update_time_api(&self.lua, dt, self.elapsed_time, 1.0 / dt.max(0.0001))
287            .map_err(|e| format!("Time API güncelleme hatası: {}", e))?;
288        api_physics::update_physics_api(&self.lua, world)
289            .map_err(|e| format!("Physics API güncelleme hatası: {}", e))?;
290
291        // 2. on_update callback'ini çağır — her yüklü script'in KENDİ env'inden.
292        //    Script'ler izole bir env içinde çalıştırıldığından (load_script), top-level
293        //    `function on_update` globals'a DEĞİL o env'e yazılır; globals'tan okumak
294        //    (eski kod) onu ASLA bulamaz → hook sessizce hiç çalışmazdı.
295        let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
296        ctx_table.set("dt", dt).map_err(|e| e.to_string())?;
297        ctx_table
298            .set("elapsed", self.elapsed_time)
299            .map_err(|e| e.to_string())?;
300
301        for (path, (_, key)) in &self.loaded_scripts {
302            let env: mlua::Table = self.lua.registry_value(key).map_err(|e| e.to_string())?;
303            if let Ok(func) = env.get::<_, LuaFunction>("on_update") {
304                func.call::<_, ()>(ctx_table.clone())
305                    .map_err(|e| format!("Lua on_update hatası ({}): {}", path, e))?;
306            }
307        }
308
309        Ok(())
310    }
311
312    /// Per-entity script güncelleme — Script component'i olan entity'ler için izole ortamda çalıştırır
313    pub fn update_entity(
314        &mut self,
315        entity_id: u32,
316        script_path: &str,
317        dt: f32,
318    ) -> Result<(), String> {
319        if let Some((_, key)) = self.loaded_scripts.get(script_path) {
320            let env: mlua::Table = self.lua.registry_value(key).map_err(|e| e.to_string())?;
321
322            // on_entity_update(entity_id, dt) çağır (varsa)
323            if let Ok(func) = env.get::<_, LuaFunction>("on_entity_update") {
324                func.call::<_, ()>((entity_id, dt)).map_err(|e| {
325                    format!(
326                        "Lua on_entity_update hatası (entity {} mod {}): {}",
327                        entity_id, script_path, e
328                    )
329                })?;
330            }
331        }
332        Ok(())
333    }
334
335    /// Komut kuyruğundaki tüm komutları World'e uygular ve oyun mantığı için kalan komutları döndürür
336    pub fn flush_commands(&self, world: &mut World, dt: f32) -> Vec<ScriptCommand> {
337        let commands = self.command_queue.drain();
338        let mut unhandled = Vec::new();
339
340        for cmd in commands {
341            match cmd {
342                ScriptCommand::SetPosition(id, pos) => {
343                    let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
344                    if let Some(mut t) = transforms.get_mut(id) {
345                        t.position = pos;
346                    }
347                }
348                ScriptCommand::SetRotation(id, rot) => {
349                    let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
350                    if let Some(mut t) = transforms.get_mut(id) {
351                        t.rotation = rot;
352                    }
353                }
354                ScriptCommand::SetScale(id, scale) => {
355                    let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
356                    if let Some(mut t) = transforms.get_mut(id) {
357                        t.scale = scale;
358                    }
359                }
360                ScriptCommand::SetVelocity(id, vel) => {
361                    let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
362                    if let Some(mut v) = velocities.get_mut(id) {
363                        v.linear = vel;
364                    }
365                }
366                ScriptCommand::SetAngularVelocity(id, ang_vel) => {
367                    let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
368                    if let Some(mut v) = velocities.get_mut(id) {
369                        v.angular = ang_vel;
370                    }
371                }
372                ScriptCommand::ApplyForce(id, force) => {
373                    let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
374                    if let Some(rb) = rbs.get(id) {
375                        if rb.mass > 0.0 {
376                            let accel = force * (1.0 / rb.mass);
377                            drop(rbs);
378                            // RigidBody var ama Velocity yoksa sıfır hızla oluştur ki
379                            // kuvvet sessizce kaybolmasın.
380                            if world
381                                .borrow::<gizmo_physics_rigid::components::Velocity>()
382                                .get(id)
383                                .is_none()
384                            {
385                                if let Some(e) = world.entity(id) {
386                                    world.add_component(
387                                        e,
388                                        gizmo_physics_rigid::components::Velocity::new(
389                                            gizmo_math::Vec3::ZERO,
390                                        ),
391                                    );
392                                }
393                            }
394                            let mut vels =
395                                world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
396                            if let Some(mut v) = vels.get_mut(id) {
397                                v.linear += accel * dt;
398                            }
399                        }
400                    }
401                }
402                ScriptCommand::ApplyImpulse(id, impulse) => {
403                    let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
404                    if let Some(rb) = rbs.get(id) {
405                        if rb.mass > 0.0 {
406                            let delta_v = impulse * (1.0 / rb.mass);
407                            drop(rbs);
408                            // RigidBody var ama Velocity yoksa sıfır hızla oluştur ki
409                            // impuls sessizce kaybolmasın.
410                            if world
411                                .borrow::<gizmo_physics_rigid::components::Velocity>()
412                                .get(id)
413                                .is_none()
414                            {
415                                if let Some(e) = world.entity(id) {
416                                    world.add_component(
417                                        e,
418                                        gizmo_physics_rigid::components::Velocity::new(
419                                            gizmo_math::Vec3::ZERO,
420                                        ),
421                                    );
422                                }
423                            }
424                            let mut vels =
425                                world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
426                            if let Some(mut v) = vels.get_mut(id) {
427                                v.linear += delta_v;
428                            }
429                        }
430                    }
431                }
432                ScriptCommand::AddRigidBody {
433                    id,
434                    mass,
435                    use_gravity,
436                } => {
437                    let entity = world.entity(id);
438                    if let Some(e) = entity {
439                        let rb = gizmo_physics_rigid::components::RigidBody::new(mass, use_gravity);
440                        world.add_component(e, rb);
441                        // Make sure velocity exists so it can move
442                        if world
443                            .borrow::<gizmo_physics_rigid::components::Velocity>()
444                            .get(id)
445                            .is_none()
446                        {
447                            world.add_component(
448                                e,
449                                gizmo_physics_rigid::components::Velocity::new(gizmo_math::Vec3::ZERO),
450                            );
451                        }
452                    }
453                }
454                ScriptCommand::AddBoxCollider { id, hx, hy, hz } => {
455                    let entity = world.entity(id);
456                    if let Some(e) = entity {
457                        let col =
458                            gizmo_physics_core::Collider::aabb(gizmo_math::Vec3::new(hx, hy, hz));
459                        world.add_component(e, col);
460                    }
461                }
462                ScriptCommand::AddSphereCollider { id, radius } => {
463                    let entity = world.entity(id);
464                    if let Some(e) = entity {
465                        let col = gizmo_physics_core::Collider::sphere(radius);
466                        world.add_component(e, col);
467                    }
468                }
469
470                ScriptCommand::SetVehicleEngineForce(_id, _force) => {}
471                ScriptCommand::SetVehicleSteering(_id, _angle) => {}
472                ScriptCommand::SetVehicleBrake(_id, _force) => {}
473
474                ScriptCommand::SpawnEntity { name, position } => {
475                    let entity = world.spawn();
476                    world.add_component(entity, gizmo_core::EntityName::new(&name));
477                    world
478                        .add_component(entity, gizmo_physics_core::Transform::new(position));
479                    let msg = format!(
480                        "Entity spawn: '{}' at ({:.1}, {:.1}, {:.1})",
481                        name, position.x, position.y, position.z
482                    );
483                    if let Ok(mut q) = self.log_queue.lock() {
484                        q.push(("info".to_string(), msg));
485                    }
486                }
487                ScriptCommand::SpawnPrefab {
488                    name,
489                    prefab_type,
490                    position,
491                } => {
492                    let entity = world.spawn();
493                    world.add_component(entity, gizmo_core::EntityName::new(&name));
494                    world
495                        .add_component(entity, gizmo_physics_core::Transform::new(position));
496                    world.add_component(entity, gizmo_core::PrefabRequest(prefab_type.clone()));
497                }
498                ScriptCommand::DestroyEntity(id) => {
499                    world.despawn_by_id(id);
500                    if let Ok(mut q) = self.log_queue.lock() {
501                        q.push(("info".to_string(), format!("Entity destroyed: {}", id)));
502                    }
503                }
504ScriptCommand::SetEntityName(id, name) => {
505                    let mut names = world.borrow_mut::<gizmo_core::EntityName>();
506                    if let Some(mut n) = names.get_mut(id) {
507                        n.0 = name;
508                    }
509                }
510ScriptCommand::PlayAnimation { id, name, blend, loop_anim } => {
511                    let mut players = world.borrow_mut::<gizmo_animation::skeletal::AnimationPlayer>();
512                    if let Some(mut player) = players.get_mut(id) {
513                        player.play_animation_by_name(&name, blend, loop_anim);
514                    }
515                }
516                ScriptCommand::SetAnimationSpeed(id, speed) => {
517                    let mut players = world.borrow_mut::<gizmo_animation::skeletal::AnimationPlayer>();
518                    if let Some(mut player) = players.get_mut(id) {
519                        player.speed = speed;
520                    }
521                }
522                ScriptCommand::AddNavAgent(id) => {
523                    let entity = world.entity(id);
524                    if let Some(e) = entity {
525                        world.add_component(e, gizmo_ai::components::NavAgent::default());
526                    }
527                }
528                ScriptCommand::SetAiTarget(id, target) => {
529                    let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
530                    if let Some(mut agent) = agents.get_mut(id) {
531                        agent.set_target(target);
532                    }
533                }
534                ScriptCommand::ClearAiTarget(id) => {
535                    let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
536                    if let Some(mut agent) = agents.get_mut(id) {
537                        // Must clear the TARGET, not just the path — clearing only the path
538                        // leaves target set, so ai_navigation_system recomputes and keeps going.
539                        agent.clear_target();
540                    }
541                }
542                ScriptCommand::SetFighterMove { id, name, startup, active, recovery, damage } => {
543                    let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
544                    if let Some(mut fighter) = fighters.get_mut(id) {
545                        let mut frame_data =
546                            gizmo_physics_core::components::fighter::FrameData::default();
547                        frame_data.startup = startup;
548                        frame_data.active = active;
549                        frame_data.recovery = recovery;
550                        frame_data.damage = damage;
551                        let mut combat_move =
552                            gizmo_physics_core::components::fighter::CombatMove::default();
553                        combat_move.name = name;
554                        combat_move.frame_data = frame_data;
555                        fighter.active_move = Some(combat_move);
556                        fighter.current_move_frame = 0;
557                    }
558                }
559                ScriptCommand::ApplyHitstop(id, frames) => {
560                    let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
561                    if let Some(mut fighter) = fighters.get_mut(id) {
562                        fighter.apply_hitstop(frames);
563                    }
564                }
565                ScriptCommand::ApplyHitstun(id, frames) => {
566                    let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
567                    if let Some(mut fighter) = fighters.get_mut(id) {
568                        fighter.apply_hitstun(frames);
569                    }
570                }
571                ScriptCommand::SaveScene(_)
572                | ScriptCommand::ShowDialogue { .. }
573                | ScriptCommand::HideDialogue
574                | ScriptCommand::TriggerCutscene(_)
575                | ScriptCommand::EndCutscene
576                | ScriptCommand::AddCheckpoint { .. }
577                | ScriptCommand::ActivateCheckpoint(_)
578                | ScriptCommand::StartRace
579                | ScriptCommand::FinishRace { .. }
580                | ScriptCommand::ResetRace
581                | ScriptCommand::SetCameraTarget(_)
582                | ScriptCommand::SetCameraFov(_)
583                | ScriptCommand::SetFightCamera { .. } => {
584                    // Bu komutlar flush_commands'ın dönüş değerinde (unhandled) zaten yer alacak
585                }
586                other => {
587                    unhandled.push(other);
588                }
589            }
590        }
591
592        unhandled
593    }
594
595    /// Runtime'da bekleyen ses/sahne komutlarını döndürür (demo tarafında ele alınır)
596    pub fn get_pending_audio_scene_commands(&self) -> Vec<ScriptCommand> {
597        // Flush zaten çağrıldıysa bu boş dönecek
598        // Alternatif: flush'tan önce çağrılmalı
599        Vec::new()
600    }
601
602    /// Script'in hot-reload edilip edilmeyeceğini kontrol eder
603    pub fn reload_if_changed(&mut self, path: &str) -> Result<bool, String> {
604        let current =
605            std::fs::read_to_string(path).map_err(|e| format!("Script okunamadı: {}", e))?;
606
607        if let Some((cached_code, _)) = self.loaded_scripts.get(path) {
608            if *cached_code == current {
609                return Ok(false);
610            }
611        }
612
613        self.load_script(path)?;
614        Ok(true)
615    }
616
617    /// Belirli bir isimdeki Lua fonksiyonunun var olup olmadığını kontrol eder
618    pub fn has_function(&self, path: &str, name: &str) -> bool {
619        if let Some((_, key)) = self.loaded_scripts.get(path) {
620            if let Ok(env) = self.lua.registry_value::<mlua::Table>(key) {
621                return env.get::<_, LuaFunction>(name).is_ok();
622            }
623        }
624        false
625    }
626
627    /// Belirli bir isimdeki Lua fonksiyonunu çağırır (per-entity scriptler için)
628    pub fn run_entity_update(
629        &self,
630        path: &str,
631        func_name: &str,
632        ctx: &ScriptContext,
633    ) -> Result<ScriptResult, String> {
634        let env: mlua::Table = if let Some((_, key)) = self.loaded_scripts.get(path) {
635            self.lua.registry_value(key).map_err(|e| e.to_string())?
636        } else {
637            return Err(format!("Script not loaded: {}", path));
638        };
639
640        let func: LuaFunction = match env.get(func_name) {
641            Ok(f) => f,
642            Err(_) => return Ok(ScriptResult::default()),
643        };
644
645        let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
646        ctx_table
647            .set("entity_id", ctx.entity_id)
648            .map_err(|e| e.to_string())?;
649        ctx_table.set("dt", ctx.dt).map_err(|e| e.to_string())?;
650        ctx_table
651            .set("elapsed", self.elapsed_time)
652            .map_err(|e| e.to_string())?;
653
654        let pos = self.lua.create_table().map_err(|e| e.to_string())?;
655        pos.set("x", ctx.position[0]).map_err(|e| e.to_string())?;
656        pos.set("y", ctx.position[1]).map_err(|e| e.to_string())?;
657        pos.set("z", ctx.position[2]).map_err(|e| e.to_string())?;
658        ctx_table.set("position", pos).map_err(|e| e.to_string())?;
659
660        let vel = self.lua.create_table().map_err(|e| e.to_string())?;
661        vel.set("x", ctx.velocity[0]).map_err(|e| e.to_string())?;
662        vel.set("y", ctx.velocity[1]).map_err(|e| e.to_string())?;
663        vel.set("z", ctx.velocity[2]).map_err(|e| e.to_string())?;
664        ctx_table.set("velocity", vel).map_err(|e| e.to_string())?;
665
666        let input = self.lua.create_table().map_err(|e| e.to_string())?;
667        input.set("w", ctx.key_w).map_err(|e| e.to_string())?;
668        input.set("a", ctx.key_a).map_err(|e| e.to_string())?;
669        input.set("s", ctx.key_s).map_err(|e| e.to_string())?;
670        input.set("d", ctx.key_d).map_err(|e| e.to_string())?;
671        input
672            .set("space", ctx.key_space)
673            .map_err(|e| e.to_string())?;
674        input.set("up", ctx.key_up).map_err(|e| e.to_string())?;
675        input.set("down", ctx.key_down).map_err(|e| e.to_string())?;
676        input.set("left", ctx.key_left).map_err(|e| e.to_string())?;
677        input
678            .set("right", ctx.key_right)
679            .map_err(|e| e.to_string())?;
680        ctx_table.set("input", input).map_err(|e| e.to_string())?;
681
682        let result_table: LuaTable = func
683            .call(ctx_table)
684            .map_err(|e| format!("Lua runtime: {}", e))?;
685
686        let mut result = ScriptResult::default();
687
688        if let Ok(pos) = result_table.get::<_, LuaTable>("position") {
689            let x: f32 = pos.get("x").unwrap_or(0.0);
690            let y: f32 = pos.get("y").unwrap_or(0.0);
691            let z: f32 = pos.get("z").unwrap_or(0.0);
692            result.new_position = Some([x, y, z]);
693        }
694
695        if let Ok(vel) = result_table.get::<_, LuaTable>("velocity") {
696            let x: f32 = vel.get("x").unwrap_or(0.0);
697            let y: f32 = vel.get("y").unwrap_or(0.0);
698            let z: f32 = vel.get("z").unwrap_or(0.0);
699            result.new_velocity = Some([x, y, z]);
700        }
701
702        Ok(result)
703    }
704
705    /// Komut kuyruğuna doğrudan erişim (internals)
706    pub fn command_queue(&self) -> &Arc<CommandQueue> {
707        &self.command_queue
708    }
709}
710
711gizmo_core::impl_component!(Script);
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716    use gizmo_math::Vec3;
717    use gizmo_physics_rigid::components::{RigidBody, Velocity};
718
719    /// A top-level `on_update` in a loaded script must fire every frame. It's written
720    /// into the script's isolated env, so the old code that read `on_update` from
721    /// `_G` never found it and the hook was a silent no-op.
722    #[test]
723    fn on_update_hook_fires_from_script_env() {
724        let mut engine = ScriptEngine::new().unwrap();
725        let world = World::new();
726        let input = gizmo_core::input::Input::default();
727
728        let path = std::env::temp_dir()
729            .join("gizmo_on_update_test.lua")
730            .to_string_lossy()
731            .into_owned();
732        std::fs::write(&path, "function on_update(ctx)\n  entity.spawn(\"bullet\", 0, 0, 0)\nend\n")
733            .unwrap();
734        engine.load_script(&path).expect("load_script");
735
736        let before = engine.command_queue().len();
737        engine.update(&world, &input, 1.0 / 60.0).expect("update");
738        let after = engine.command_queue().len();
739        let _ = std::fs::remove_file(&path);
740
741        assert!(
742            after > before,
743            "on_update must run and queue a spawn command (before={before}, after={after})"
744        );
745    }
746
747    /// Regression: RigidBody var ama Velocity yoksa ApplyForce sessizce
748    /// kaybolmamalı; Velocity oluşturulup ivme uygulanmalı.
749    #[test]
750    fn apply_force_creates_velocity_when_missing() {
751        let engine = ScriptEngine::new().unwrap();
752        let mut world = World::new();
753
754        let entity = world.spawn();
755        world.add_component(entity, RigidBody::new(2.0, false));
756        // Kasıtlı olarak Velocity EKLENMEDİ.
757        assert!(world.borrow::<Velocity>().get(entity.id()).is_none());
758
759        engine
760            .command_queue()
761            .push(ScriptCommand::ApplyForce(entity.id(), Vec3::new(4.0, 0.0, 0.0)));
762
763        let dt = 0.5_f32;
764        engine.flush_commands(&mut world, dt);
765
766        let vels = world.borrow::<Velocity>();
767        let v = vels
768            .get(entity.id())
769            .expect("Velocity ApplyForce tarafından oluşturulmalıydı");
770        // accel = force/mass = 4/2 = 2; dv = accel*dt = 2*0.5 = 1.0
771        assert!((v.linear.x - 1.0).abs() < 1e-5, "x hızı yanlış: {}", v.linear.x);
772    }
773
774    /// Regression: RigidBody var ama Velocity yoksa ApplyImpulse sessizce
775    /// kaybolmamalı; Velocity oluşturulup delta-v uygulanmalı.
776    #[test]
777    fn apply_impulse_creates_velocity_when_missing() {
778        let engine = ScriptEngine::new().unwrap();
779        let mut world = World::new();
780
781        let entity = world.spawn();
782        world.add_component(entity, RigidBody::new(2.0, false));
783        assert!(world.borrow::<Velocity>().get(entity.id()).is_none());
784
785        engine
786            .command_queue()
787            .push(ScriptCommand::ApplyImpulse(entity.id(), Vec3::new(6.0, 0.0, 0.0)));
788
789        engine.flush_commands(&mut world, 0.016);
790
791        let vels = world.borrow::<Velocity>();
792        let v = vels
793            .get(entity.id())
794            .expect("Velocity ApplyImpulse tarafından oluşturulmalıydı");
795        // dv = impulse/mass = 6/2 = 3.0 (dt'den bağımsız)
796        assert!((v.linear.x - 3.0).abs() < 1e-5, "x hızı yanlış: {}", v.linear.x);
797    }
798}