1use crate::commands::{CommandQueue, ScriptCommand};
6use gizmo_math::Vec3;
7use mlua::prelude::*;
8use std::sync::Arc;
9use tracing::trace;
10
11const MIN_COLLIDER_DIM: f32 = 1e-4;
14
15fn sanitize_dim(v: f32) -> f32 {
18 if v.is_finite() && v > MIN_COLLIDER_DIM {
19 v
20 } else {
21 MIN_COLLIDER_DIM
22 }
23}
24
25pub fn register_physics_api(lua: &Lua, command_queue: Arc<CommandQueue>) -> Result<(), LuaError> {
27 crate::api_table::register_protected(lua, "physics", |physics_table| {
28
29 {
31 let cq = command_queue.clone();
32 physics_table.raw_set(
33 "apply_force",
34 lua.create_function(move |_, (id, fx, fy, fz): (u32, f32, f32, f32)| {
35 cq.push(ScriptCommand::ApplyForce(id, Vec3::new(fx, fy, fz)));
36 Ok(())
37 })?,
38 )?;
39 }
40
41 {
43 let cq = command_queue.clone();
44 physics_table.raw_set(
45 "apply_impulse",
46 lua.create_function(move |_, (id, ix, iy, iz): (u32, f32, f32, f32)| {
47 cq.push(ScriptCommand::ApplyImpulse(id, Vec3::new(ix, iy, iz)));
48 Ok(())
49 })?,
50 )?;
51 }
52
53 {
55 let cq = command_queue.clone();
56 physics_table.raw_set(
57 "add_rigidbody",
58 lua.create_function(
59 move |_, (id, mass, use_gravity): (u32, f32, bool)| {
62 cq.push(ScriptCommand::AddRigidBody {
63 id,
64 mass,
65 use_gravity,
66 });
67 Ok(())
68 },
69 )?,
70 )?;
71 }
72
73 {
75 let cq = command_queue.clone();
76 physics_table.raw_set(
77 "add_box_collider",
78 lua.create_function(move |_, (id, hx, hy, hz): (u32, f32, f32, f32)| {
79 cq.push(ScriptCommand::AddBoxCollider {
80 id,
81 hx: sanitize_dim(hx),
82 hy: sanitize_dim(hy),
83 hz: sanitize_dim(hz),
84 });
85 Ok(())
86 })?,
87 )?;
88 }
89
90 {
91 let cq = command_queue.clone();
92 physics_table.raw_set(
93 "add_sphere_collider",
94 lua.create_function(move |_, (id, radius): (u32, f32)| {
95 cq.push(ScriptCommand::AddSphereCollider {
96 id,
97 radius: sanitize_dim(radius),
98 });
99 Ok(())
100 })?,
101 )?;
102 }
103
104 Ok(())
105 })
106}
107
108pub fn with_call_time_queries<R>(
129 lua: &Lua,
130 world: &gizmo_core::World,
131 run: impl FnOnce() -> Result<R, LuaError>,
132) -> Result<R, LuaError> {
133 let physics_table = crate::api_table::raw(lua, "physics")?;
134
135 let out = lua.scope(|scope| {
136 let ground_at = scope.create_function(|_, (x, z): (f32, f32)| {
137 let Ok(pw) = world.try_get_resource::<gizmo_physics_rigid::world::PhysicsWorld>()
139 else {
140 return Ok(LuaValue::Nil);
141 };
142 let ray = gizmo_physics_core::raycast::Ray::new(
143 gizmo_math::Vec3::new(x, GROUND_PROBE_HEIGHT, z),
144 gizmo_math::Vec3::new(0.0, -1.0, 0.0),
145 );
146 Ok(match pw.raycast(&ray, GROUND_PROBE_HEIGHT * 2.0) {
147 Some(hit) => LuaValue::Number(f64::from(hit.point.y)),
148 None => LuaValue::Nil,
152 })
153 })?;
154 physics_table.raw_set("ground_at", ground_at)?;
155 run()
156 });
157
158 physics_table.raw_set("ground_at", LuaValue::Nil)?;
160 out
161}
162
163const GROUND_PROBE_HEIGHT: f32 = 10_000.0;
166
167#[tracing::instrument(skip_all, name = "script_physics_read")]
169pub fn update_physics_api(
170 lua: &Lua,
171 world: &gizmo_core::World,
172) -> Result<(), LuaError> {
173 let physics_table = crate::api_table::raw(lua, "physics")?;
176
177 let triggers = lua.create_table()?;
178 let collisions = lua.create_table()?;
179
180 if let Ok(physics_world) = world.try_get_resource::<gizmo_physics_rigid::world::PhysicsWorld>() {
181 for (i, t_event) in physics_world.trigger_events().iter().enumerate() {
183 let ev = lua.create_table()?;
184 ev.set("trigger_id", t_event.trigger_entity.id())?;
185 ev.set("other_id", t_event.other_entity.id())?;
186 let status = match t_event.event_type {
187 gizmo_physics_core::collision::CollisionEventType::Started => "enter",
188 gizmo_physics_core::collision::CollisionEventType::Persisting => "stay",
189 gizmo_physics_core::collision::CollisionEventType::Ended => "exit",
190 };
191 ev.set("status", status)?;
192 triggers.set(i + 1, ev)?;
193 }
194
195 for (i, c_event) in physics_world.collision_events().iter().enumerate() {
197 let ev = lua.create_table()?;
198 ev.set("entity_a", c_event.entity_a.id())?;
199 ev.set("entity_b", c_event.entity_b.id())?;
200 let status = match c_event.event_type {
201 gizmo_physics_core::collision::CollisionEventType::Started => "enter",
202 gizmo_physics_core::collision::CollisionEventType::Persisting => "stay",
203 gizmo_physics_core::collision::CollisionEventType::Ended => "exit",
204 };
205 ev.set("status", status)?;
206 collisions.set(i + 1, ev)?;
207 }
208
209 trace!(
210 trigger_count = physics_world.trigger_events().len(),
211 collision_count = physics_world.collision_events().len(),
212 "[Scripting] fizik olayları Lua'ya aktarıldı"
213 );
214 } else {
215 trace!("[Scripting] PhysicsWorld kaynağı yok — trigger/collision listeleri boş");
218 }
219
220 physics_table.raw_set("triggers", triggers)?;
222 physics_table.raw_set("collisions", collisions)?;
223
224 Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use gizmo_core::World;
231 use gizmo_physics_rigid::world::PhysicsWorld;
232 use gizmo_physics_core::collision::{TriggerEvent, CollisionEvent, CollisionEventType};
233 use mlua::Lua;
234
235 #[test]
236 fn test_update_physics_api() {
237 let lua = Lua::new();
238 register_physics_api(&lua, Arc::new(CommandQueue::new())).unwrap();
241
242 let mut world = World::new();
243 let ent1 = world.spawn();
244 let ent2 = world.spawn();
245
246 let mut physics_world = PhysicsWorld::new();
247 physics_world.trigger_events.push(TriggerEvent {
248 trigger_entity: gizmo_physics_core::BodyHandle::from_id(ent1.id()),
249 other_entity: gizmo_physics_core::BodyHandle::from_id(ent2.id()),
250 event_type: CollisionEventType::Started,
251 });
252
253 physics_world.collision_events.push(CollisionEvent {
254 entity_a: gizmo_physics_core::BodyHandle::from_id(ent1.id()),
255 entity_b: gizmo_physics_core::BodyHandle::from_id(ent2.id()),
256 event_type: CollisionEventType::Started,
257 contact_points: Default::default(),
258 });
259
260 world.insert_resource(physics_world);
261
262 update_physics_api(&lua, &world).unwrap();
263
264 let script = r#"
265 local triggers = physics.triggers
266 local collisions = physics.collisions
267 assert(#triggers == 1)
268 assert(triggers[1].status == "enter")
269 assert(#collisions == 1)
270 assert(collisions[1].status == "enter")
271 "#;
272
273 lua.load(script).exec().unwrap();
274 }
275
276 #[test]
277 fn sanitize_dim_rejects_bad_values() {
278 assert_eq!(sanitize_dim(f32::NAN), MIN_COLLIDER_DIM);
279 assert_eq!(sanitize_dim(f32::INFINITY), MIN_COLLIDER_DIM);
280 assert_eq!(sanitize_dim(f32::NEG_INFINITY), MIN_COLLIDER_DIM);
281 assert_eq!(sanitize_dim(-5.0), MIN_COLLIDER_DIM);
282 assert_eq!(sanitize_dim(0.0), MIN_COLLIDER_DIM);
283 assert_eq!(sanitize_dim(2.5), 2.5); }
285
286 #[test]
287 fn box_collider_dims_are_sanitized_from_lua() {
288 let lua = Lua::new();
289 let cq = Arc::new(CommandQueue::new());
290 register_physics_api(&lua, cq.clone()).unwrap();
291 lua.load("physics.add_box_collider(1, -5.0, 0/0, 2.0)").exec().unwrap();
293
294 let cmds = cq.drain();
295 assert_eq!(cmds.len(), 1);
296 match &cmds[0] {
297 ScriptCommand::AddBoxCollider { hx, hy, hz, .. } => {
298 assert!(hx.is_finite() && *hx > 0.0, "negative hx must be clamped, got {hx}");
299 assert!(hy.is_finite() && *hy > 0.0, "NaN hy must be clamped, got {hy}");
300 assert_eq!(*hz, 2.0, "valid hz must pass through untouched");
301 }
302 other => panic!("expected AddBoxCollider, got {other:?}"),
303 }
304 }
305
306 #[test]
307 fn sphere_collider_radius_is_sanitized_from_lua() {
308 let lua = Lua::new();
309 let cq = Arc::new(CommandQueue::new());
310 register_physics_api(&lua, cq.clone()).unwrap();
311 lua.load("physics.add_sphere_collider(7, -3.0)").exec().unwrap();
312
313 let cmds = cq.drain();
314 assert_eq!(cmds.len(), 1);
315 match &cmds[0] {
316 ScriptCommand::AddSphereCollider { radius, .. } => {
317 assert!(radius.is_finite() && *radius > 0.0, "radius clamped, got {radius}");
318 }
319 other => panic!("expected AddSphereCollider, got {other:?}"),
320 }
321 }
322
323 #[test]
325 fn force_impulse_rigidbody_calls_push_expected_commands() {
326 let lua = Lua::new();
327 let cq = Arc::new(CommandQueue::new());
328 register_physics_api(&lua, cq.clone()).unwrap();
329
330 lua.load(
331 r#"
332 physics.apply_force(1, 0.0, -9.8, 0.0)
333 physics.apply_impulse(1, 10.0, 0.0, 0.0)
334 physics.add_rigidbody(1, 2.5, true)
335 "#,
336 )
337 .exec()
338 .unwrap();
339
340 let cmds = cq.drain();
341 assert_eq!(cmds.len(), 3);
342 assert!(matches!(cmds[0], ScriptCommand::ApplyForce(1, f) if f == Vec3::new(0.0, -9.8, 0.0)));
343 assert!(matches!(cmds[1], ScriptCommand::ApplyImpulse(1, i) if i == Vec3::new(10.0, 0.0, 0.0)));
344 match cmds[2] {
345 ScriptCommand::AddRigidBody { id, mass, use_gravity } => {
346 assert_eq!(id, 1);
347 assert!((mass - 2.5).abs() < 1e-6);
348 assert!(use_gravity);
349 }
350 ref other => panic!("beklenen AddRigidBody, gelen {other:?}"),
351 }
352 }
353
354 #[test]
356 fn valid_box_dims_pass_through() {
357 let lua = Lua::new();
358 let cq = Arc::new(CommandQueue::new());
359 register_physics_api(&lua, cq.clone()).unwrap();
360 lua.load("physics.add_box_collider(1, 0.5, 1.0, 2.0)").exec().unwrap();
361
362 let cmds = cq.drain();
363 match &cmds[0] {
364 ScriptCommand::AddBoxCollider { hx, hy, hz, .. } => {
365 assert_eq!((*hx, *hy, *hz), (0.5, 1.0, 2.0));
366 }
367 other => panic!("beklenen AddBoxCollider, gelen {other:?}"),
368 }
369 }
370
371 #[test]
374 fn update_without_physics_world_yields_empty_lists() {
375 let lua = Lua::new();
376 register_physics_api(&lua, Arc::new(CommandQueue::new())).unwrap();
377
378 let world = World::new(); update_physics_api(&lua, &world).unwrap();
380
381 lua.load(
382 r#"
383 assert(type(physics.triggers) == "table", "triggers tablo olmalı")
384 assert(#physics.triggers == 0, "triggers boş olmalı")
385 assert(#physics.collisions == 0, "collisions boş olmalı")
386 "#,
387 )
388 .exec()
389 .unwrap();
390 }
391
392 #[test]
394 fn collision_event_status_maps_stay_and_exit() {
395 let lua = Lua::new();
396 register_physics_api(&lua, Arc::new(CommandQueue::new())).unwrap();
397
398 let mut world = World::new();
399 let a = world.spawn();
400 let b = world.spawn();
401 let mut pw = PhysicsWorld::new();
402 pw.collision_events.push(CollisionEvent {
403 entity_a: gizmo_physics_core::BodyHandle::from_id(a.id()),
404 entity_b: gizmo_physics_core::BodyHandle::from_id(b.id()),
405 event_type: CollisionEventType::Persisting,
406 contact_points: Default::default(),
407 });
408 pw.trigger_events.push(TriggerEvent {
409 trigger_entity: gizmo_physics_core::BodyHandle::from_id(a.id()),
410 other_entity: gizmo_physics_core::BodyHandle::from_id(b.id()),
411 event_type: CollisionEventType::Ended,
412 });
413 world.insert_resource(pw);
414
415 update_physics_api(&lua, &world).unwrap();
416
417 lua.load(
418 r#"
419 assert(physics.collisions[1].status == "stay", "Persisting -> stay")
420 assert(physics.triggers[1].status == "exit", "Ended -> exit")
421 "#,
422 )
423 .exec()
424 .unwrap();
425 }
426}