Skip to main content

gizmo_scripting/
api_input.rs

1//! Input API — Lua'ya sunulan girdi sorgulama fonksiyonları
2//!
3//! Lua scriptlerinden tuş ve fare durumunu sorgulamak için kullanılır.
4//! Read-only API'dir, komut kuyruğuna yazmaz.
5
6use gizmo_core::input::Input;
7use mlua::prelude::*;
8
9/// Input API fonksiyonlarını Lua'ya kaydeder
10pub fn register_input_api(lua: &Lua) -> Result<(), LuaError> {
11    crate::api_table::register_protected(lua, "input", |input_table| {
12
13    // Placeholder fonksiyonlar - her frame update_input_api ile güncellenir
14    input_table.raw_set("_keys", lua.create_table()?)?;
15    input_table.raw_set("_just_keys", lua.create_table()?)?;
16    input_table.raw_set("_mouse_x", 0.0f32)?;
17    input_table.raw_set("_mouse_y", 0.0f32)?;
18    input_table.raw_set("_mouse_dx", 0.0f32)?;
19    input_table.raw_set("_mouse_dy", 0.0f32)?;
20    input_table.raw_set("_mouse_left", false)?;
21    input_table.raw_set("_mouse_right", false)?;
22    input_table.raw_set("_mouse_middle", false)?;
23
24    // The name → key-code table comes from `gizmo_core::input::NAMED_KEYS`, not from a copy
25    // written here. The copy this replaces held USB HID usage codes (`w = 17`, `space = 44`)
26    // while the engine stores winit `KeyCode` discriminants, so EVERY entry was wrong — and
27    // `down`/`right` held each other's codes, which meant a script reading the arrow keys moved
28    // the player right when they pressed down. `gizmo-app` carries the test that proves the
29    // table, because it is the crate that can see the enum.
30    let key_map = lua.create_table()?;
31    for (name, code) in gizmo_core::input::NAMED_KEYS {
32        key_map.set(*name, *code)?;
33    }
34    input_table.raw_set("_key_map", key_map)?;
35
36    // Lua helper fonksiyonlarını tanımla
37    lua.load(
38        r#"
39        function input.is_pressed(key_name)
40            local code = input._key_map[string.lower(key_name)]
41            if code and input._keys[code] then
42                return true
43            end
44            return false
45        end
46
47        function input.is_just_pressed(key_name)
48            local code = input._key_map[string.lower(key_name)]
49            if code and input._just_keys[code] then
50                return true
51            end
52            return false
53        end
54
55        function input.mouse_position()
56            return { x = input._mouse_x, y = input._mouse_y }
57        end
58
59        function input.mouse_delta()
60            return { x = input._mouse_dx, y = input._mouse_dy }
61        end
62
63        function input.is_mouse_pressed(button)
64            if button == "left" then return input._mouse_left
65            elseif button == "right" then return input._mouse_right
66            elseif button == "middle" then return input._mouse_middle
67            end
68            return false
69        end
70    "#,
71        )
72        .exec()
73    })
74}
75
76/// Her frame Input durumunu Lua'ya aktarır
77#[tracing::instrument(skip_all, name = "script_input_read")]
78pub fn update_input_api(lua: &Lua, input: &Input) -> Result<(), LuaError> {
79    // The real table, not the global: the global is a read-only proxy so a script cannot rewrite
80    // the API (see `api_table`), and the engine's own per-frame writes go behind it.
81    let input_table = crate::api_table::raw(lua, "input")?;
82
83    // Basılı tuşları Lua table'ına aktar
84    let keys = lua.create_table()?;
85    let just_keys = lua.create_table()?;
86
87    // Yaygın tuş kodlarını kontrol et (winit KeyCode enum değerleri)
88    for code in 0..256u32 {
89        if input.is_key_pressed(code) {
90            keys.set(code, true)?;
91        }
92        if input.is_key_just_pressed(code) {
93            just_keys.set(code, true)?;
94        }
95    }
96
97    input_table.raw_set("_keys", keys)?;
98    input_table.raw_set("_just_keys", just_keys)?;
99
100    let (mx, my) = input.mouse_position();
101    input_table.raw_set("_mouse_x", mx)?;
102    input_table.raw_set("_mouse_y", my)?;
103
104    let (dx, dy) = input.mouse_delta();
105    input_table.raw_set("_mouse_dx", dx)?;
106    input_table.raw_set("_mouse_dy", dy)?;
107
108    input_table.raw_set(
109        "_mouse_left",
110        input.is_mouse_button_pressed(gizmo_core::input::mouse::LEFT),
111    )?;
112    input_table.raw_set(
113        "_mouse_right",
114        input.is_mouse_button_pressed(gizmo_core::input::mouse::RIGHT),
115    )?;
116    input_table.raw_set(
117        "_mouse_middle",
118        input.is_mouse_button_pressed(gizmo_core::input::mouse::MIDDLE),
119    )?;
120
121    Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use gizmo_core::input::code_from_name;
128
129    /// Marks exactly the named keys as held, by looking their codes up the same way the API does.
130    ///
131    /// The codes are NOT written out here. They were, and that is how these tests went on passing
132    /// while every one of them described the wrong keyboard: the table under test and the table in
133    /// the test were the same transcription of USB HID codes, so they agreed with each other and
134    /// with nothing else.
135    ///
136    /// Written through the registry rather than from Lua, because the API table is read-only to
137    /// scripts now — and that is the honest path anyway: this is where real input arrives from.
138    fn press(lua: &Lua, held: &[&str], just: &[&str]) {
139        let table = |names: &[&str]| {
140            let t = lua.create_table().unwrap();
141            for n in names {
142                t.raw_set(code_from_name(n).expect("known key"), true).unwrap();
143            }
144            t
145        };
146        let input_table = crate::api_table::raw(lua, "input").unwrap();
147        input_table.raw_set("_keys", table(held)).unwrap();
148        input_table.raw_set("_just_keys", table(just)).unwrap();
149    }
150
151    /// Regression: 'n' and 'w' must not share a code, and each must answer on its own.
152    #[test]
153    fn n_and_w_keys_do_not_collide() {
154        let lua = Lua::new();
155        register_input_api(&lua).unwrap();
156
157        press(&lua, &["w"], &[]);
158        lua.load(
159            r#"
160            assert(input.is_pressed("w") == true, "w basili olmali")
161            assert(input.is_pressed("n") == false, "n basili OLMAMALI (w ile cakisma)")
162            "#,
163        )
164        .exec()
165        .unwrap();
166
167        press(&lua, &["n"], &[]);
168        lua.load(
169            r#"
170            assert(input.is_pressed("n") == true, "n kendi keycode'unda basili olmali")
171            assert(input.is_pressed("w") == false, "w basili OLMAMALI")
172            "#,
173        )
174        .exec()
175        .unwrap();
176    }
177
178    /// The arrow keys, because the old table had `down` and `right` holding each other's codes —
179    /// a script reading them moved the player right when the player pressed down.
180    ///
181    /// This checks the *plumbing*: that a name reaches the slot it looked up. It cannot check that
182    /// the numbers are right, because it gets them from the same table the API does — that is
183    /// `gizmo-app`'s `key_convention` test, which compares them against the winit enum.
184    #[test]
185    fn the_arrow_keys_are_not_swapped() {
186        let lua = Lua::new();
187        register_input_api(&lua).unwrap();
188
189        press(&lua, &["down"], &[]);
190        lua.load(
191            r#"
192            assert(input.is_pressed("down") == true, "asagi basili")
193            assert(input.is_pressed("right") == false, "sag basili DEGIL")
194            assert(input.is_pressed("up") == false and input.is_pressed("left") == false)
195            "#,
196        )
197        .exec()
198        .unwrap();
199
200        press(&lua, &["right"], &[]);
201        lua.load(
202            r#"
203            assert(input.is_pressed("right") == true, "sag basili")
204            assert(input.is_pressed("down") == false, "asagi basili DEGIL")
205            "#,
206        )
207        .exec()
208        .unwrap();
209    }
210
211    /// `is_just_pressed` reads `_just_keys` and is independent of `_keys`: a key held from an
212    /// earlier frame is pressed but not just-pressed.
213    #[test]
214    fn is_just_pressed_is_independent_from_held() {
215        let lua = Lua::new();
216        register_input_api(&lua).unwrap();
217
218        press(&lua, &["space"], &[]);
219        lua.load(
220            r#"
221            assert(input.is_pressed("space") == true, "space surekli basili")
222            assert(input.is_just_pressed("space") == false, "space bu frame basilmadi")
223            "#,
224        )
225        .exec()
226        .unwrap();
227
228        press(&lua, &["space"], &["space"]);
229        lua.load(r#"assert(input.is_just_pressed("space") == true, "space bu frame basildi")"#)
230            .exec()
231            .unwrap();
232    }
233
234    /// Names are case-insensitive, an unknown name is false rather than an error, and the digit
235    /// row answers on its own codes.
236    #[test]
237    fn key_name_casing_unknown_and_digits() {
238        let lua = Lua::new();
239        register_input_api(&lua).unwrap();
240
241        press(&lua, &["w", "1"], &[]);
242        lua.load(
243            r#"
244            assert(input.is_pressed("W") == true, "buyuk harf W basili sayilmali")
245            assert(input.is_pressed("w") == true, "kucuk harf w basili")
246            assert(input.is_pressed("1") == true, "rakam tusu 1")
247            assert(input.is_pressed("bilinmeyen_tus") == false, "haritada olmayan ad false")
248            assert(input.is_pressed("2") == false, "basilmayan rakam false")
249            "#,
250        )
251        .exec()
252        .unwrap();
253    }
254
255    /// Fare yardımcıları: pozisyon/delta tablo döndürmeli; is_mouse_pressed sol/sağ/orta
256    /// ve bilinmeyen düğme için doğru sonuç vermeli.
257    #[test]
258    fn mouse_helpers_read_snapshot() {
259        let lua = Lua::new();
260        register_input_api(&lua).unwrap();
261        let t = crate::api_table::raw(&lua, "input").unwrap();
262        for (k, v) in [("_mouse_x", 120.0f32), ("_mouse_y", 45.0), ("_mouse_dx", -3.0), ("_mouse_dy", 7.0)] {
263            t.raw_set(k, v).unwrap();
264        }
265        for (k, v) in [("_mouse_left", true), ("_mouse_right", false), ("_mouse_middle", true)] {
266            t.raw_set(k, v).unwrap();
267        }
268        lua.load(
269            r#"
270            local p = input.mouse_position()
271            assert(p.x == 120.0 and p.y == 45.0, "pozisyon")
272            local d = input.mouse_delta()
273            assert(d.x == -3.0 and d.y == 7.0, "delta")
274            assert(input.is_mouse_pressed("left") == true, "sol basılı")
275            assert(input.is_mouse_pressed("right") == false, "sağ basılı değil")
276            assert(input.is_mouse_pressed("middle") == true, "orta basılı")
277            assert(input.is_mouse_pressed("side") == false, "bilinmeyen düğme false")
278            "#,
279        )
280        .exec()
281        .unwrap();
282    }
283
284    /// update_input_api gerçek bir Input durumunu Lua'ya doğru aktarmalı:
285    /// basılı tuş, fare konumu/deltası ve fare düğmeleri.
286    #[test]
287    fn update_input_api_mirrors_real_input() {
288        use gizmo_core::input::{mouse, Input};
289
290        let lua = Lua::new();
291        register_input_api(&lua).unwrap();
292
293        let mut input = Input::default();
294        input.on_key_pressed(code_from_name("w").unwrap());
295        input.set_mouse_position(200.0, 100.0);
296        input.on_mouse_delta(5.0, -2.0);
297        input.on_mouse_button_pressed(mouse::RIGHT);
298
299        update_input_api(&lua, &input).unwrap();
300
301        lua.load(
302            r#"
303            assert(input.is_pressed("w") == true, "w World'den aktarılmalı")
304            assert(input.is_just_pressed("w") == true, "w bu frame basıldı")
305            local p = input.mouse_position()
306            assert(p.x == 200.0 and p.y == 100.0, "fare konumu aktarılmalı")
307            local d = input.mouse_delta()
308            assert(math.abs(d.x - 5.0) < 1e-5 and math.abs(d.y + 2.0) < 1e-5, "fare delta")
309            assert(input.is_mouse_pressed("right") == true, "sağ tık aktarılmalı")
310            assert(input.is_mouse_pressed("left") == false, "sol tık basılı değil")
311            "#,
312        )
313        .exec()
314        .unwrap();
315    }
316}