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    pub log_queue: Arc<std::sync::Mutex<Vec<(String, String)>>>, // (Level, Message)
26}
27
28unsafe impl Send for ScriptEngine {}
29unsafe impl Sync for ScriptEngine {}
30
31/// ECS Componenti: Varlığın üzerine hangi Lua script'inin takılı olduğunu tutar
32#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
33pub struct Script {
34    pub file_path: String,
35    #[serde(default, skip)]
36    pub initialized: bool, // on_init çağrıldı mı?
37}
38
39impl Script {
40    pub fn new(path: &str) -> Self {
41        Self {
42            file_path: path.to_string(),
43            initialized: false,
44        }
45    }
46}
47
48/// Lua'ya geçirilecek entity verisi (geriye dönük uyumluluk için)
49#[derive(Clone, Debug)]
50pub struct ScriptContext {
51    pub entity_id: u32,
52    pub dt: f32,
53    pub position: [f32; 3],
54    pub velocity: [f32; 3],
55    pub key_w: bool,
56    pub key_a: bool,
57    pub key_s: bool,
58    pub key_d: bool,
59    pub key_space: bool,
60    pub key_up: bool,
61    pub key_down: bool,
62    pub key_left: bool,
63    pub key_right: bool,
64}
65
66/// Lua'dan dönen değişiklikler (geriye dönük uyumluluk)
67#[derive(Clone, Debug, Default)]
68pub struct ScriptResult {
69    pub new_position: Option<[f32; 3]>,
70    pub new_velocity: Option<[f32; 3]>,
71}
72
73impl ScriptEngine {
74    pub fn new() -> Result<Self, LuaError> {
75        let lua = Lua::new();
76        let command_queue = Arc::new(CommandQueue::new());
77        let log_queue = Arc::new(std::sync::Mutex::new(Vec::new()));
78
79        // === SANDBOX: Tehlikeli modülleri kapat ===
80        lua.globals().set("os", LuaNil)?;
81        lua.globals().set("io", LuaNil)?;
82        lua.globals().set("loadfile", LuaNil)?;
83        lua.globals().set("dofile", LuaNil)?;
84        lua.globals().set("require", LuaNil)?;
85        lua.globals().set("package", LuaNil)?;
86        lua.globals().set("debug", LuaNil)?;
87        lua.globals().set("loadstring", LuaNil)?;
88        lua.globals().set("load", LuaNil)?;
89
90        // === TEMEL PRINT FONKSİYONU ===
91        let lq_clone1 = log_queue.clone();
92        lua.globals().set(
93            "print_engine",
94            lua.create_function(move |_, msg: String| {
95                if let Ok(mut q) = lq_clone1.lock() {
96                    q.push(("info".to_string(), msg));
97                }
98                Ok(())
99            })?,
100        )?;
101
102        // Orijinal print'i de engine çıktısına yönlendir
103        let lq_clone2 = log_queue.clone();
104        lua.globals().set(
105            "print",
106            lua.create_function(move |_, values: LuaMultiValue| {
107                let parts: Vec<String> = values
108                    .iter()
109                    .map(|v| {
110                        if let mlua::Value::String(s) = v {
111                            s.to_str().unwrap_or("").to_string()
112                        } else if let mlua::Value::Number(n) = v {
113                            n.to_string()
114                        } else if let mlua::Value::Integer(i) = v {
115                            i.to_string()
116                        } else if let mlua::Value::Boolean(b) = v {
117                            b.to_string()
118                        } else {
119                            format!("{:?}", v)
120                        }
121                    })
122                    .collect();
123                if let Ok(mut q) = lq_clone2.lock() {
124                    q.push(("info".to_string(), parts.join("\t")));
125                }
126                Ok(())
127            })?,
128        )?;
129
130        // === VEC3 YARDIMCI FONKSİYONLARI ===
131        lua.load(
132            r#"
133            function vec3(x, y, z)
134                return { x = x or 0, y = y or 0, z = z or 0 }
135            end
136            
137            function vec3_add(a, b)
138                return vec3(a.x + b.x, a.y + b.y, a.z + b.z)
139            end
140            
141            function vec3_sub(a, b)
142                return vec3(a.x - b.x, a.y - b.y, a.z - b.z)
143            end
144            
145            function vec3_scale(v, s)
146                return vec3(v.x * s, v.y * s, v.z * s)
147            end
148            
149            function vec3_length(v)
150                return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
151            end
152            
153            function vec3_normalize(v)
154                local len = vec3_length(v)
155                if len > 0.0001 then
156                    return vec3(v.x / len, v.y / len, v.z / len)
157                end
158                return vec3(0, 0, 0)
159            end
160            
161            function vec3_dot(a, b)
162                return a.x * b.x + a.y * b.y + a.z * b.z
163            end
164            
165            function vec3_cross(a, b)
166                return vec3(
167                    a.y * b.z - a.z * b.y,
168                    a.z * b.x - a.x * b.z,
169                    a.x * b.y - a.y * b.x
170                )
171            end
172            
173            function vec3_lerp(a, b, t)
174                return vec3(
175                    a.x + (b.x - a.x) * t,
176                    a.y + (b.y - a.y) * t,
177                    a.z + (b.z - a.z) * t
178                )
179            end
180            
181            function vec3_distance(a, b)
182                return vec3_length(vec3_sub(a, b))
183            end
184            
185            -- Clamp utility
186            function clamp(value, min, max)
187                return math.max(min, math.min(max, value))
188            end
189            
190            -- Lerp utility
191            function lerp(a, b, t)
192                return a + (b - a) * t
193            end
194        "#,
195        )
196        .exec()?;
197
198        // === API MODÜLLERİNİ KAYDET ===
199        api_entity::register_entity_api(&lua, command_queue.clone())?;
200        api_fighter::register_fighter_api(&lua, command_queue.clone())?;
201        api_input::register_input_api(&lua)?;
202        api_physics::register_physics_api(&lua, command_queue.clone())?;
203        api_scene::register_scene_api(&lua, command_queue.clone())?;
204        api_audio::register_audio_api(&lua, command_queue.clone())?;
205        api_time::register_time_api(&lua)?;
206        api_vehicle::register_vehicle_api(&lua, command_queue.clone())?;
207        api_ai::register_ai_api(&lua, command_queue.clone())?;
208
209        Ok(Self {
210            lua,
211            loaded_scripts: HashMap::new(),
212            command_queue,
213            elapsed_time: 0.0,
214            log_queue,
215        })
216    }
217
218    pub fn load_script(&mut self, path: &str) -> Result<(), String> {
219        let content = std::fs::read_to_string(path)
220            .map_err(|e| format!("Script okunamadı {}: {}", path, e))?;
221
222        let env = self.lua.create_table().map_err(|e| e.to_string())?;
223
224        // Link to _G via metatable
225        let meta = self.lua.create_table().map_err(|e| e.to_string())?;
226        meta.set("__index", self.lua.globals()).unwrap();
227        env.set_metatable(Some(meta));
228
229        // Script'i İzole env içinde çalıştır
230        self.lua
231            .load(&content)
232            .set_environment(env.clone())
233            .exec()
234            .map_err(|e| format!("Lua hata {}: {}", path, e))?;
235
236        let key = self
237            .lua
238            .create_registry_value(env)
239            .map_err(|e| e.to_string())?;
240
241        // Replace existing key if it exists to free old memory
242        if let Some((_, old_key)) = self.loaded_scripts.insert(path.to_string(), (content, key)) {
243            let _ = self.lua.remove_registry_value(old_key);
244        }
245
246        tracing::info!("🔧 ScriptEngine: Yüklendi ve İzole Edildi → {}", path);
247        Ok(())
248    }
249
250    /// Her frame çağrılan güncelleme — World verilerini Lua'ya aktarır, scriptleri çalıştırır
251    pub fn update(&mut self, world: &World, input: &Input, dt: f32) -> Result<(), String> {
252        self.elapsed_time += dt;
253
254        // 1. World verilerini Lua'ya aktar (read snapshot)
255        api_entity::update_entity_read_api(&self.lua, world)
256            .map_err(|e| format!("Entity API güncelleme hatası: {}", e))?;
257        api_fighter::update_fighter_read_api(&self.lua, world)
258            .map_err(|e| format!("Fighter API güncelleme hatası: {}", e))?;
259        api_input::update_input_api(&self.lua, input)
260            .map_err(|e| format!("Input API güncelleme hatası: {}", e))?;
261        api_scene::update_scene_api(&self.lua, world)
262            .map_err(|e| format!("Scene API güncelleme hatası: {}", e))?;
263        api_time::update_time_api(&self.lua, dt, self.elapsed_time, 1.0 / dt.max(0.0001))
264            .map_err(|e| format!("Time API güncelleme hatası: {}", e))?;
265        api_physics::update_physics_api(&self.lua, world)
266            .map_err(|e| format!("Physics API güncelleme hatası: {}", e))?;
267
268        // 2. on_update callback'ini çağır (varsa)
269        let globals = self.lua.globals();
270        if let Ok(func) = globals.get::<_, LuaFunction>("on_update") {
271            let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
272            ctx_table.set("dt", dt).map_err(|e| e.to_string())?;
273            ctx_table
274                .set("elapsed", self.elapsed_time)
275                .map_err(|e| e.to_string())?;
276
277            func.call::<_, ()>(ctx_table)
278                .map_err(|e| format!("Lua on_update hatası: {}", e))?;
279        }
280
281        Ok(())
282    }
283
284    /// Per-entity script güncelleme — Script component'i olan entity'ler için izole ortamda çalıştırır
285    pub fn update_entity(
286        &mut self,
287        entity_id: u32,
288        script_path: &str,
289        dt: f32,
290    ) -> Result<(), String> {
291        if let Some((_, key)) = self.loaded_scripts.get(script_path) {
292            let env: mlua::Table = self.lua.registry_value(key).map_err(|e| e.to_string())?;
293
294            // on_entity_update(entity_id, dt) çağır (varsa)
295            if let Ok(func) = env.get::<_, LuaFunction>("on_entity_update") {
296                func.call::<_, ()>((entity_id, dt)).map_err(|e| {
297                    format!(
298                        "Lua on_entity_update hatası (entity {} mod {}): {}",
299                        entity_id, script_path, e
300                    )
301                })?;
302            }
303        }
304        Ok(())
305    }
306
307    /// Komut kuyruğundaki tüm komutları World'e uygular ve oyun mantığı için kalan komutları döndürür
308    pub fn flush_commands(&self, world: &mut World, dt: f32) -> Vec<ScriptCommand> {
309        let commands = self.command_queue.drain();
310        let mut unhandled = Vec::new();
311
312        for cmd in commands {
313            match cmd {
314                ScriptCommand::SetPosition(id, pos) => {
315                    let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
316                    if let Some(mut t) = transforms.get_mut(id) {
317                        t.position = pos;
318                    }
319                }
320                ScriptCommand::SetRotation(id, rot) => {
321                    let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
322                    if let Some(mut t) = transforms.get_mut(id) {
323                        t.rotation = rot;
324                    }
325                }
326                ScriptCommand::SetScale(id, scale) => {
327                    let mut transforms = world.borrow_mut::<gizmo_physics_core::Transform>();
328                    if let Some(mut t) = transforms.get_mut(id) {
329                        t.scale = scale;
330                    }
331                }
332                ScriptCommand::SetVelocity(id, vel) => {
333                    let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
334                    if let Some(mut v) = velocities.get_mut(id) {
335                        v.linear = vel;
336                    }
337                }
338                ScriptCommand::SetAngularVelocity(id, ang_vel) => {
339                    let mut velocities = world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
340                    if let Some(mut v) = velocities.get_mut(id) {
341                        v.angular = ang_vel;
342                    }
343                }
344                ScriptCommand::ApplyForce(id, force) => {
345                    let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
346                    if let Some(rb) = rbs.get(id) {
347                        if rb.mass > 0.0 {
348                            let accel = force * (1.0 / rb.mass);
349                            drop(rbs);
350                            let mut vels =
351                                world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
352                            if let Some(mut v) = vels.get_mut(id) {
353                                v.linear += accel * dt;
354                            }
355                        }
356                    }
357                }
358                ScriptCommand::ApplyImpulse(id, impulse) => {
359                    let rbs = world.borrow::<gizmo_physics_rigid::components::RigidBody>();
360                    if let Some(rb) = rbs.get(id) {
361                        if rb.mass > 0.0 {
362                            let delta_v = impulse * (1.0 / rb.mass);
363                            drop(rbs);
364                            let mut vels =
365                                world.borrow_mut::<gizmo_physics_rigid::components::Velocity>();
366                            if let Some(mut v) = vels.get_mut(id) {
367                                v.linear += delta_v;
368                            }
369                        }
370                    }
371                }
372                ScriptCommand::AddRigidBody {
373                    id,
374                    mass,
375                    restitution,
376                    friction,
377                    use_gravity,
378                } => {
379                    let entity = world
380                        .iter_alive_entities()
381                        .into_iter()
382                        .find(|e| e.id() == id);
383                    if let Some(e) = entity {
384                        let rb = gizmo_physics_rigid::components::RigidBody::new(
385                            mass,
386                            restitution,
387                            friction,
388                            use_gravity,
389                        );
390                        world.add_component(e, rb);
391                        // Make sure velocity exists so it can move
392                        if world
393                            .borrow::<gizmo_physics_rigid::components::Velocity>()
394                            .get(id)
395                            .is_none()
396                        {
397                            world.add_component(
398                                e,
399                                gizmo_physics_rigid::components::Velocity::new(gizmo_math::Vec3::ZERO),
400                            );
401                        }
402                    }
403                }
404                ScriptCommand::AddBoxCollider { id, hx, hy, hz } => {
405                    let entity = world
406                        .iter_alive_entities()
407                        .into_iter()
408                        .find(|e| e.id() == id);
409                    if let Some(e) = entity {
410                        let col =
411                            gizmo_physics_core::Collider::aabb(gizmo_math::Vec3::new(hx, hy, hz));
412                        world.add_component(e, col);
413                    }
414                }
415                ScriptCommand::AddSphereCollider { id, radius } => {
416                    let entity = world
417                        .iter_alive_entities()
418                        .into_iter()
419                        .find(|e| e.id() == id);
420                    if let Some(e) = entity {
421                        let col = gizmo_physics_core::Collider::sphere(radius);
422                        world.add_component(e, col);
423                    }
424                }
425
426                ScriptCommand::SetVehicleEngineForce(_id, _force) => {}
427                ScriptCommand::SetVehicleSteering(_id, _angle) => {}
428                ScriptCommand::SetVehicleBrake(_id, _force) => {}
429
430                ScriptCommand::SpawnEntity { name, position } => {
431                    let entity = world.spawn();
432                    world.add_component(entity, gizmo_core::EntityName::new(&name));
433                    world
434                        .add_component(entity, gizmo_physics_core::Transform::new(position));
435                    let msg = format!(
436                        "Entity spawn: '{}' at ({:.1}, {:.1}, {:.1})",
437                        name, position.x, position.y, position.z
438                    );
439                    if let Ok(mut q) = self.log_queue.lock() {
440                        q.push(("info".to_string(), msg));
441                    }
442                }
443                ScriptCommand::SpawnPrefab {
444                    name,
445                    prefab_type,
446                    position,
447                } => {
448                    let entity = world.spawn();
449                    world.add_component(entity, gizmo_core::EntityName::new(&name));
450                    world
451                        .add_component(entity, gizmo_physics_core::Transform::new(position));
452                    world.add_component(entity, gizmo_core::PrefabRequest(prefab_type.clone()));
453                }
454                ScriptCommand::DestroyEntity(id) => {
455                    world.despawn_by_id(id);
456                    if let Ok(mut q) = self.log_queue.lock() {
457                        q.push(("info".to_string(), format!("Entity destroyed: {}", id)));
458                    }
459                }
460ScriptCommand::SetEntityName(id, name) => {
461                    let mut names = world.borrow_mut::<gizmo_core::EntityName>();
462                    if let Some(mut n) = names.get_mut(id) {
463                        n.0 = name;
464                    }
465                }
466ScriptCommand::PlayAnimation { id, name, blend, loop_anim } => {
467                    let mut players = world.borrow_mut::<gizmo_renderer::components::AnimationPlayer>();
468                    if let Some(mut player) = players.get_mut(id) {
469                        player.play_animation_by_name(&name, blend, loop_anim);
470                    }
471                }
472                ScriptCommand::SetAnimationSpeed(id, speed) => {
473                    let mut players = world.borrow_mut::<gizmo_renderer::components::AnimationPlayer>();
474                    if let Some(mut player) = players.get_mut(id) {
475                        player.speed = speed;
476                    }
477                }
478                ScriptCommand::AddNavAgent(id) => {
479                    let entity = world
480                        .iter_alive_entities()
481                        .into_iter()
482                        .find(|e| e.id() == id);
483                    if let Some(e) = entity {
484                        world.add_component(e, gizmo_ai::components::NavAgent::default());
485                    }
486                }
487                ScriptCommand::SetAiTarget(id, target) => {
488                    let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
489                    if let Some(mut agent) = agents.get_mut(id) {
490                        agent.set_target(target);
491                    }
492                }
493ScriptCommand::ClearAiTarget(id) => {
494                    let mut agents = world.borrow_mut::<gizmo_ai::components::NavAgent>();
495                    if let Some(mut agent) = agents.get_mut(id) {
496                        agent.clear_path();
497                    }
498                }
499                ScriptCommand::SetFighterMove { id, name, startup, active, recovery, damage } => {
500                    let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
501                    if let Some(mut fighter) = fighters.get_mut(id) {
502                        fighter.active_move = Some(gizmo_physics_core::components::fighter::CombatMove {
503                            name,
504                            frame_data: gizmo_physics_core::components::fighter::FrameData {
505                                startup,
506                                active,
507                                recovery,
508                                damage,
509                                ..Default::default()
510                            }
511                        });
512                        fighter.current_move_frame = 0;
513                    }
514                }
515                ScriptCommand::ApplyHitstop(id, frames) => {
516                    let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
517                    if let Some(mut fighter) = fighters.get_mut(id) {
518                        fighter.apply_hitstop(frames);
519                    }
520                }
521                ScriptCommand::ApplyHitstun(id, frames) => {
522                    let mut fighters = world.borrow_mut::<gizmo_physics_core::components::FighterController>();
523                    if let Some(mut fighter) = fighters.get_mut(id) {
524                        fighter.apply_hitstun(frames);
525                    }
526                }
527                ScriptCommand::SaveScene(_)
528                | ScriptCommand::ShowDialogue { .. }
529                | ScriptCommand::HideDialogue
530                | ScriptCommand::TriggerCutscene(_)
531                | ScriptCommand::EndCutscene
532                | ScriptCommand::AddCheckpoint { .. }
533                | ScriptCommand::ActivateCheckpoint(_)
534                | ScriptCommand::StartRace
535                | ScriptCommand::FinishRace { .. }
536                | ScriptCommand::ResetRace
537                | ScriptCommand::SetCameraTarget(_)
538                | ScriptCommand::SetCameraFov(_)
539                | ScriptCommand::SetFightCamera { .. } => {
540                    // Bu komutlar flush_commands'ın dönüş değerinde (unhandled) zaten yer alacak
541                }
542                other => {
543                    unhandled.push(other);
544                }
545            }
546        }
547
548        unhandled
549    }
550
551    /// Runtime'da bekleyen ses/sahne komutlarını döndürür (demo tarafında ele alınır)
552    pub fn get_pending_audio_scene_commands(&self) -> Vec<ScriptCommand> {
553        // Flush zaten çağrıldıysa bu boş dönecek
554        // Alternatif: flush'tan önce çağrılmalı
555        Vec::new()
556    }
557
558    /// Script'in hot-reload edilip edilmeyeceğini kontrol eder
559    pub fn reload_if_changed(&mut self, path: &str) -> Result<bool, String> {
560        let current =
561            std::fs::read_to_string(path).map_err(|e| format!("Script okunamadı: {}", e))?;
562
563        if let Some((cached_code, _)) = self.loaded_scripts.get(path) {
564            if *cached_code == current {
565                return Ok(false);
566            }
567        }
568
569        self.load_script(path)?;
570        Ok(true)
571    }
572
573    /// Belirli bir isimdeki Lua fonksiyonunun var olup olmadığını kontrol eder
574    pub fn has_function(&self, path: &str, name: &str) -> bool {
575        if let Some((_, key)) = self.loaded_scripts.get(path) {
576            if let Ok(env) = self.lua.registry_value::<mlua::Table>(key) {
577                return env.get::<_, LuaFunction>(name).is_ok();
578            }
579        }
580        false
581    }
582
583    /// Belirli bir isimdeki Lua fonksiyonunu çağırır (per-entity scriptler için)
584    pub fn run_entity_update(
585        &self,
586        path: &str,
587        func_name: &str,
588        ctx: &ScriptContext,
589    ) -> Result<ScriptResult, String> {
590        let env: mlua::Table = if let Some((_, key)) = self.loaded_scripts.get(path) {
591            self.lua.registry_value(key).map_err(|e| e.to_string())?
592        } else {
593            return Err(format!("Script not loaded: {}", path));
594        };
595
596        let func: LuaFunction = match env.get(func_name) {
597            Ok(f) => f,
598            Err(_) => return Ok(ScriptResult::default()),
599        };
600
601        let ctx_table = self.lua.create_table().map_err(|e| e.to_string())?;
602        ctx_table
603            .set("entity_id", ctx.entity_id)
604            .map_err(|e| e.to_string())?;
605        ctx_table.set("dt", ctx.dt).map_err(|e| e.to_string())?;
606        ctx_table
607            .set("elapsed", self.elapsed_time)
608            .map_err(|e| e.to_string())?;
609
610        let pos = self.lua.create_table().map_err(|e| e.to_string())?;
611        pos.set("x", ctx.position[0]).map_err(|e| e.to_string())?;
612        pos.set("y", ctx.position[1]).map_err(|e| e.to_string())?;
613        pos.set("z", ctx.position[2]).map_err(|e| e.to_string())?;
614        ctx_table.set("position", pos).map_err(|e| e.to_string())?;
615
616        let vel = self.lua.create_table().map_err(|e| e.to_string())?;
617        vel.set("x", ctx.velocity[0]).map_err(|e| e.to_string())?;
618        vel.set("y", ctx.velocity[1]).map_err(|e| e.to_string())?;
619        vel.set("z", ctx.velocity[2]).map_err(|e| e.to_string())?;
620        ctx_table.set("velocity", vel).map_err(|e| e.to_string())?;
621
622        let input = self.lua.create_table().map_err(|e| e.to_string())?;
623        input.set("w", ctx.key_w).map_err(|e| e.to_string())?;
624        input.set("a", ctx.key_a).map_err(|e| e.to_string())?;
625        input.set("s", ctx.key_s).map_err(|e| e.to_string())?;
626        input.set("d", ctx.key_d).map_err(|e| e.to_string())?;
627        input
628            .set("space", ctx.key_space)
629            .map_err(|e| e.to_string())?;
630        input.set("up", ctx.key_up).map_err(|e| e.to_string())?;
631        input.set("down", ctx.key_down).map_err(|e| e.to_string())?;
632        input.set("left", ctx.key_left).map_err(|e| e.to_string())?;
633        input
634            .set("right", ctx.key_right)
635            .map_err(|e| e.to_string())?;
636        ctx_table.set("input", input).map_err(|e| e.to_string())?;
637
638        let result_table: LuaTable = func
639            .call(ctx_table)
640            .map_err(|e| format!("Lua runtime: {}", e))?;
641
642        let mut result = ScriptResult::default();
643
644        if let Ok(pos) = result_table.get::<_, LuaTable>("position") {
645            let x: f32 = pos.get("x").unwrap_or(0.0);
646            let y: f32 = pos.get("y").unwrap_or(0.0);
647            let z: f32 = pos.get("z").unwrap_or(0.0);
648            result.new_position = Some([x, y, z]);
649        }
650
651        if let Ok(vel) = result_table.get::<_, LuaTable>("velocity") {
652            let x: f32 = vel.get("x").unwrap_or(0.0);
653            let y: f32 = vel.get("y").unwrap_or(0.0);
654            let z: f32 = vel.get("z").unwrap_or(0.0);
655            result.new_velocity = Some([x, y, z]);
656        }
657
658        Ok(result)
659    }
660
661    /// Komut kuyruğuna doğrudan erişim (internals)
662    pub fn command_queue(&self) -> &Arc<CommandQueue> {
663        &self.command_queue
664    }
665}
666
667gizmo_core::impl_component!(Script);