Skip to main content

gizmo_scripting/
api_entity.rs

1//! Entity API — Lua'ya sunulan entity yönetim fonksiyonları
2//!
3//! Lua scriptlerinden entity pozisyon, rotasyon, hız ve ölçek bilgilerine
4//! erişim sağlar. Tüm değişiklikler komut kuyruğuna yazılır.
5
6use crate::commands::{CommandQueue, ScriptCommand};
7use gizmo_core::World;
8use gizmo_math::{Quat, Vec3};
9use mlua::prelude::*;
10use std::sync::Arc;
11
12/// Entity API fonksiyonlarını Lua'ya kaydeder
13pub fn register_entity_api(lua: &Lua, command_queue: Arc<CommandQueue>) -> Result<(), LuaError> {
14    crate::api_table::register_protected(lua, "entity", |entity_table| {
15
16    // === POSITION ===
17    {
18        let cq = command_queue.clone();
19        entity_table.raw_set(
20            "set_position",
21            lua.create_function(move |_, (id, x, y, z): (u32, f32, f32, f32)| {
22                cq.push(ScriptCommand::SetPosition(id, Vec3::new(x, y, z)));
23                Ok(())
24            })?,
25        )?;
26    }
27
28    // === ROTATION ===
29    {
30        let cq = command_queue.clone();
31        entity_table.raw_set(
32            "set_rotation",
33            lua.create_function(move |_, (id, x, y, z, w): (u32, f32, f32, f32, f32)| {
34                cq.push(ScriptCommand::SetRotation(id, Quat::from_xyzw(x, y, z, w)));
35                Ok(())
36            })?,
37        )?;
38    }
39
40    // === SCALE ===
41    {
42        let cq = command_queue.clone();
43        entity_table.raw_set(
44            "set_scale",
45            lua.create_function(move |_, (id, x, y, z): (u32, f32, f32, f32)| {
46                cq.push(ScriptCommand::SetScale(id, Vec3::new(x, y, z)));
47                Ok(())
48            })?,
49        )?;
50    }
51
52    // === VELOCITY ===
53    {
54        let cq = command_queue.clone();
55        entity_table.raw_set(
56            "set_velocity",
57            lua.create_function(move |_, (id, x, y, z): (u32, f32, f32, f32)| {
58                cq.push(ScriptCommand::SetVelocity(id, Vec3::new(x, y, z)));
59                Ok(())
60            })?,
61        )?;
62    }
63
64    {
65        let cq = command_queue.clone();
66        entity_table.raw_set(
67            "set_angular_velocity",
68            lua.create_function(move |_, (id, x, y, z): (u32, f32, f32, f32)| {
69                cq.push(ScriptCommand::SetAngularVelocity(id, Vec3::new(x, y, z)));
70                Ok(())
71            })?,
72        )?;
73    }
74
75    // === SPAWN ===
76    {
77        let cq = command_queue.clone();
78        entity_table.raw_set(
79            "spawn",
80            lua.create_function(move |_, (name, x, y, z): (String, f32, f32, f32)| {
81                cq.push(ScriptCommand::SpawnEntity {
82                    name,
83                    position: Vec3::new(x, y, z),
84                });
85                Ok(())
86            })?,
87        )?;
88    }
89
90    // === SPAWN PREFAB ===
91    {
92        let cq = command_queue.clone();
93        entity_table.raw_set(
94            "spawn_prefab",
95            lua.create_function(
96                move |_, (name, prefab_type, x, y, z): (String, String, f32, f32, f32)| {
97                    cq.push(ScriptCommand::SpawnPrefab {
98                        name,
99                        prefab_type,
100                        position: Vec3::new(x, y, z),
101                    });
102                    Ok(())
103                },
104            )?,
105        )?;
106    }
107
108    // === DESTROY ===
109    {
110        let cq = command_queue.clone();
111        entity_table.raw_set(
112            "destroy",
113            lua.create_function(move |_, id: u32| {
114                cq.push(ScriptCommand::DestroyEntity(id));
115                Ok(())
116            })?,
117        )?;
118    }
119
120    // === SET NAME ===
121    {
122        let cq = command_queue.clone();
123        entity_table.raw_set(
124            "set_name",
125            lua.create_function(move |_, (id, name): (u32, String)| {
126                cq.push(ScriptCommand::SetEntityName(id, name));
127                Ok(())
128            })?,
129        )?;
130    }
131
132        // Defined once, at registration: these are static helpers, and the per-frame update path
133        // used to re-`load` them on every frame — which is both wasted work and, now that the API
134        // table is read-only to Lua, a write the proxy rejects.
135        // Lua tarafı get_position(id) gibi helper fonksiyonları kullanır
136        lua.load(
137            r#"
138            function entity.get_position(id)
139                return entity._positions[id] or {x=0, y=0, z=0}
140            end
141            function entity.get_velocity(id)
142                return entity._velocities[id] or {x=0, y=0, z=0}
143            end
144            function entity.get_rotation(id)
145                return entity._rotations[id] or {x=0, y=0, z=0, w=1}
146            end
147            function entity.get_scale(id)
148                return entity._scales[id] or {x=1, y=1, z=1}
149            end
150            function entity.get_name(id)
151                return entity._names[id] or ""
152            end
153        "#,
154        )
155        .exec()?;
156
157        Ok(())
158    })
159}
160
161/// World'den okunan verilerle entity read API'sini günceller (her frame)
162#[tracing::instrument(skip_all, name = "script_entity_read")]
163pub fn update_entity_read_api(lua: &Lua, world: &World) -> Result<(), LuaError> {
164    // The real table, not the global: the global is a read-only proxy so a script cannot
165    // rewrite the API (see `api_table`), and the engine's per-frame writes go behind it.
166    let entity_table = crate::api_table::raw(lua, "entity")?;
167
168    // get_position: World'den doğrudan okunan veriye dayalı closure
169    // Her frame snapshot alarak Lua table'ına yazıyoruz
170    let positions = lua.create_table()?;
171    let velocities = lua.create_table()?;
172    let rotations = lua.create_table()?;
173    let scales = lua.create_table()?;
174    let names = lua.create_table()?;
175
176    let transforms = world.borrow::<gizmo_physics_core::Transform>();
177    for (eid, _) in transforms.iter() {
178        if let Some(t) = transforms.get(eid) {
179            let pos = lua.create_table()?;
180            pos.set("x", t.position.x)?;
181            pos.set("y", t.position.y)?;
182            pos.set("z", t.position.z)?;
183            positions.set(eid, pos)?;
184
185            let rot = lua.create_table()?;
186            rot.set("x", t.rotation.x)?;
187            rot.set("y", t.rotation.y)?;
188            rot.set("z", t.rotation.z)?;
189            rot.set("w", t.rotation.w)?;
190            rotations.set(eid, rot)?;
191
192            let scl = lua.create_table()?;
193            scl.set("x", t.scale.x)?;
194            scl.set("y", t.scale.y)?;
195            scl.set("z", t.scale.z)?;
196            scales.set(eid, scl)?;
197        }
198    }
199
200    let vels = world.borrow::<gizmo_physics_rigid::components::Velocity>();
201    for (eid, _) in vels.iter() {
202        if let Some(v) = vels.get(eid) {
203            let vel = lua.create_table()?;
204            vel.set("x", v.linear.x)?;
205            vel.set("y", v.linear.y)?;
206            vel.set("z", v.linear.z)?;
207            velocities.set(eid, vel)?;
208        }
209    }
210
211    let entity_names = world.borrow::<gizmo_core::EntityName>();
212    for (eid, _) in entity_names.iter() {
213        if let Some(n) = entity_names.get(eid) {
214            names.set(eid, n.0.clone())?;
215        }
216    }
217
218    // Snapshot table'ları entity API'sine bağla
219    entity_table.raw_set("_positions", positions)?;
220    entity_table.raw_set("_velocities", velocities)?;
221    entity_table.raw_set("_rotations", rotations)?;
222    entity_table.raw_set("_scales", scales)?;
223    entity_table.raw_set("_names", names)?;
224
225
226    Ok(())
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use gizmo_physics_core::Transform;
233    use mlua::Lua;
234
235    /// Yazma tarafı: set_position/scale/velocity/angular_velocity ve set_rotation
236    /// argümanları doğru ScriptCommand'lara (Vec3/Quat) dönüştürüp kuyruğa yazmalı.
237    #[test]
238    fn write_calls_push_expected_commands() {
239        let lua = Lua::new();
240        let cq = Arc::new(CommandQueue::new());
241        register_entity_api(&lua, cq.clone()).unwrap();
242
243        lua.load(
244            r#"
245            entity.set_position(1, 1.0, 2.0, 3.0)
246            entity.set_scale(1, 2.0, 2.0, 2.0)
247            entity.set_velocity(1, -5.0, 0.0, 0.0)
248            entity.set_angular_velocity(1, 0.0, 1.5, 0.0)
249            entity.set_rotation(1, 0.0, 0.0, 0.0, 1.0)
250            "#,
251        )
252        .exec()
253        .unwrap();
254
255        let cmds = cq.drain();
256        assert_eq!(cmds.len(), 5);
257        assert!(matches!(cmds[0], ScriptCommand::SetPosition(1, p) if p == Vec3::new(1.0, 2.0, 3.0)));
258        assert!(matches!(cmds[1], ScriptCommand::SetScale(1, s) if s == Vec3::new(2.0, 2.0, 2.0)));
259        assert!(matches!(cmds[2], ScriptCommand::SetVelocity(1, v) if v == Vec3::new(-5.0, 0.0, 0.0)));
260        assert!(matches!(cmds[3], ScriptCommand::SetAngularVelocity(1, v) if v == Vec3::new(0.0, 1.5, 0.0)));
261        match cmds[4] {
262            ScriptCommand::SetRotation(id, q) => {
263                assert_eq!(id, 1);
264                // Kimlik kuaterniyonu: (0,0,0,1)
265                assert!((q.w - 1.0).abs() < 1e-6 && q.x.abs() < 1e-6);
266            }
267            ref other => panic!("beklenen SetRotation, gelen {other:?}"),
268        }
269    }
270
271    /// Yaşam döngüsü: spawn / spawn_prefab / destroy / set_name doğru komutlara dönüşmeli.
272    #[test]
273    fn lifecycle_calls_push_expected_commands() {
274        let lua = Lua::new();
275        let cq = Arc::new(CommandQueue::new());
276        register_entity_api(&lua, cq.clone()).unwrap();
277
278        lua.load(
279            r#"
280            entity.spawn("hero", 4.0, 5.0, 6.0)
281            entity.spawn_prefab("crate", "wooden_box", 0.0, 0.0, 0.0)
282            entity.set_name(9, "renamed")
283            entity.destroy(9)
284            "#,
285        )
286        .exec()
287        .unwrap();
288
289        let cmds = cq.drain();
290        assert_eq!(cmds.len(), 4);
291        match &cmds[0] {
292            ScriptCommand::SpawnEntity { name, position } => {
293                assert_eq!(name, "hero");
294                assert_eq!(*position, Vec3::new(4.0, 5.0, 6.0));
295            }
296            other => panic!("beklenen SpawnEntity, gelen {other:?}"),
297        }
298        match &cmds[1] {
299            ScriptCommand::SpawnPrefab { name, prefab_type, position } => {
300                assert_eq!(name, "crate");
301                assert_eq!(prefab_type, "wooden_box");
302                assert_eq!(*position, Vec3::ZERO);
303            }
304            other => panic!("beklenen SpawnPrefab, gelen {other:?}"),
305        }
306        match &cmds[2] {
307            ScriptCommand::SetEntityName(id, name) => {
308                assert_eq!(*id, 9);
309                assert_eq!(name, "renamed");
310            }
311            other => panic!("beklenen SetEntityName, gelen {other:?}"),
312        }
313        assert!(matches!(cmds[3], ScriptCommand::DestroyEntity(9)));
314    }
315
316    /// Okuma tarafı: update_entity_read_api World'den snapshot alır; get_position bilinen
317    /// entity için gerçek değeri döndürmeli.
318    #[test]
319    fn read_api_reflects_world_transform() {
320        let lua = Lua::new();
321        let cq = Arc::new(CommandQueue::new());
322        register_entity_api(&lua, cq).unwrap();
323
324        let mut world = World::new();
325        let e = world.spawn();
326        world.add_component(e, Transform::new(Vec3::new(7.0, 8.0, 9.0)));
327        let id = e.id();
328
329        update_entity_read_api(&lua, &world).unwrap();
330
331        lua.load(format!(
332            r#"
333            local p = entity.get_position({id})
334            assert(math.abs(p.x - 7.0) < 1e-5, "x")
335            assert(math.abs(p.y - 8.0) < 1e-5, "y")
336            assert(math.abs(p.z - 9.0) < 1e-5, "z")
337            "#
338        ))
339        .exec()
340        .unwrap();
341    }
342
343    /// Bilinmeyen entity için getter'lar güvenli varsayılanlar döndürmeli:
344    /// pozisyon/hız sıfır, rotasyon kimlik (w=1), ölçek birim (1,1,1), isim boş.
345    #[test]
346    fn read_api_returns_safe_defaults_for_unknown_id() {
347        let lua = Lua::new();
348        let cq = Arc::new(CommandQueue::new());
349        register_entity_api(&lua, cq).unwrap();
350
351        // Boş dünya → snapshot tabloları boş.
352        let world = World::new();
353        update_entity_read_api(&lua, &world).unwrap();
354
355        lua.load(
356            r#"
357            local p = entity.get_position(999)
358            assert(p.x == 0 and p.y == 0 and p.z == 0, "pozisyon varsayılanı sıfır")
359            local v = entity.get_velocity(999)
360            assert(v.x == 0 and v.y == 0 and v.z == 0, "hız varsayılanı sıfır")
361            local r = entity.get_rotation(999)
362            assert(r.x == 0 and r.y == 0 and r.z == 0 and r.w == 1, "rotasyon varsayılanı kimlik")
363            local s = entity.get_scale(999)
364            assert(s.x == 1 and s.y == 1 and s.z == 1, "ölçek varsayılanı birim")
365            assert(entity.get_name(999) == "", "isim varsayılanı boş string")
366            "#,
367        )
368        .exec()
369        .unwrap();
370    }
371}