use crate::binding_api;
use crate::joint::{BallJoint, FixedJoint, HingeJoint, Joint, SliderJoint, SpringJoint};
use crate::math::{Fix128, QuatFix, Vec3Fix};
use crate::solver::{PhysicsWorld, RigidBody, SolverConfig};
mod guard {
use std::cell::RefCell;
use std::panic::{catch_unwind, AssertUnwindSafe};
thread_local! {
static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub fn set_last_error(msg: impl Into<String>) {
LAST_ERROR.with(|slot| *slot.borrow_mut() = Some(msg.into()));
}
pub fn take_last_error() -> Option<String> {
LAST_ERROR.with(|slot| slot.borrow_mut().take())
}
pub fn clear_last_error() {
LAST_ERROR.with(|slot| *slot.borrow_mut() = None);
}
#[inline]
pub fn ffi_guard<T>(default: T, body: impl FnOnce() -> T) -> T {
match catch_unwind(AssertUnwindSafe(body)) {
Ok(v) => v,
Err(payload) => {
let msg = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "panic with non-string payload".to_string());
set_last_error(format!("alice-physics FFI panic: {msg}"));
default
}
}
}
}
use guard::ffi_guard;
#[no_mangle]
pub extern "C" fn alice_physics_last_error() -> *mut std::os::raw::c_char {
ffi_guard(std::ptr::null_mut(), || match guard::take_last_error() {
Some(msg) => std::ffi::CString::new(msg.replace('\0', " "))
.map_or(std::ptr::null_mut(), std::ffi::CString::into_raw),
None => std::ptr::null_mut(),
})
}
#[no_mangle]
pub extern "C" fn alice_physics_clear_last_error() {
ffi_guard((), guard::clear_last_error);
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_string_free(s: *mut std::os::raw::c_char) {
ffi_guard((), || {
if !s.is_null() {
drop(std::ffi::CString::from_raw(s));
}
});
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AliceVec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AliceQuat {
pub x: f64,
pub y: f64,
pub z: f64,
pub w: f64,
}
#[repr(C)]
pub struct AlicePhysicsConfig {
pub substeps: u32,
pub iterations: u32,
pub gravity_x: f64,
pub gravity_y: f64,
pub gravity_z: f64,
pub damping: f64,
}
#[repr(C)]
pub struct AliceBodyInfo {
pub position: AliceVec3,
pub velocity: AliceVec3,
pub angular_velocity: AliceVec3,
pub rotation: AliceQuat,
pub inv_mass: f64,
pub is_static: u8,
pub is_sensor: u8,
}
impl AliceVec3 {
fn to_vec3fix(self) -> Vec3Fix {
Vec3Fix::new(
Fix128::from_f64(self.x),
Fix128::from_f64(self.y),
Fix128::from_f64(self.z),
)
}
fn from_vec3fix(v: Vec3Fix) -> Self {
Self {
x: v.x.to_f64(),
y: v.y.to_f64(),
z: v.z.to_f64(),
}
}
}
impl AliceQuat {
fn from_quatfix(q: QuatFix) -> Self {
Self {
x: q.x.to_f64(),
y: q.y.to_f64(),
z: q.z.to_f64(),
w: q.w.to_f64(),
}
}
}
#[no_mangle]
pub extern "C" fn alice_physics_world_create() -> *mut PhysicsWorld {
ffi_guard(std::ptr::null_mut(), || {
let world = PhysicsWorld::new(SolverConfig::default());
Box::into_raw(Box::new(world))
})
}
#[no_mangle]
pub extern "C" fn alice_physics_world_create_with_config(
config: AlicePhysicsConfig,
) -> *mut PhysicsWorld {
ffi_guard(std::ptr::null_mut(), || {
let solver_config = SolverConfig {
substeps: config.substeps as usize,
iterations: config.iterations as usize,
gravity: Vec3Fix::new(
Fix128::from_f64(config.gravity_x),
Fix128::from_f64(config.gravity_y),
Fix128::from_f64(config.gravity_z),
),
damping: Fix128::from_f64(config.damping),
..Default::default()
};
let world = PhysicsWorld::new(solver_config);
Box::into_raw(Box::new(world))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_destroy(world: *mut PhysicsWorld) {
ffi_guard((), || {
if !world.is_null() {
drop(Box::from_raw(world));
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_step(world: *mut PhysicsWorld, dt: f64) -> u8 {
ffi_guard(0, || {
let w = match world.as_mut() {
Some(w) => w as *mut PhysicsWorld,
None => return 0,
};
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(*w).step(Fix128::from_f64(dt));
}));
result.is_ok() as u8
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_step_n(
world: *mut PhysicsWorld,
dt: f64,
steps: u32,
) -> u8 {
ffi_guard(0, || {
let w = match world.as_mut() {
Some(w) => w as *mut PhysicsWorld,
None => return 0,
};
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let dt_fix = Fix128::from_f64(dt);
for _ in 0..steps {
(*w).step(dt_fix);
}
}));
result.is_ok() as u8
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_body_count(world: *const PhysicsWorld) -> u32 {
ffi_guard(0, || match world.as_ref() {
Some(w) => w.bodies.len() as u32,
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_get_positions_batch(
world: *const PhysicsWorld,
out: *mut f64,
out_capacity: u32,
) -> u8 {
ffi_guard(0, || {
let w = match world.as_ref() {
Some(w) => w,
None => return 0,
};
if out.is_null() {
return 0;
}
let n = w.bodies.len();
if (out_capacity as usize) < n * 3 {
return 0;
}
let buf = std::slice::from_raw_parts_mut(out, n * 3);
for (i, body) in w.bodies.iter().enumerate() {
buf[i * 3] = body.position.x.to_f64();
buf[i * 3 + 1] = body.position.y.to_f64();
buf[i * 3 + 2] = body.position.z.to_f64();
}
1
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_get_velocities_batch(
world: *const PhysicsWorld,
out: *mut f64,
out_capacity: u32,
) -> u8 {
ffi_guard(0, || {
let w = match world.as_ref() {
Some(w) => w,
None => return 0,
};
if out.is_null() {
return 0;
}
let n = w.bodies.len();
if (out_capacity as usize) < n * 3 {
return 0;
}
let buf = std::slice::from_raw_parts_mut(out, n * 3);
for (i, body) in w.bodies.iter().enumerate() {
buf[i * 3] = body.velocity.x.to_f64();
buf[i * 3 + 1] = body.velocity.y.to_f64();
buf[i * 3 + 2] = body.velocity.z.to_f64();
}
1
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_set_velocities_batch(
world: *mut PhysicsWorld,
data: *const f64,
count: u32,
) -> u8 {
ffi_guard(0, || {
let w = match world.as_mut() {
Some(w) => w,
None => return 0,
};
if data.is_null() {
return 0;
}
let n = w.bodies.len();
if (count as usize) < n * 3 {
return 0;
}
let buf = std::slice::from_raw_parts(data, n * 3);
for (i, body) in w.bodies.iter_mut().enumerate() {
body.velocity = Vec3Fix::new(
Fix128::from_f64(buf[i * 3]),
Fix128::from_f64(buf[i * 3 + 1]),
Fix128::from_f64(buf[i * 3 + 2]),
);
}
1
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_apply_impulses_batch(
world: *mut PhysicsWorld,
data: *const f64,
count: u32,
) -> u8 {
ffi_guard(0, || {
let w = match world.as_mut() {
Some(w) => w,
None => return 0,
};
if data.is_null() || count % 4 != 0 {
return 0;
}
let buf = std::slice::from_raw_parts(data, count as usize);
let n_bodies = w.bodies.len();
for chunk in buf.chunks_exact(4) {
let body_id = chunk[0] as usize;
if body_id >= n_bodies {
continue;
}
let impulse = Vec3Fix::new(
Fix128::from_f64(chunk[1]),
Fix128::from_f64(chunk[2]),
Fix128::from_f64(chunk[3]),
);
w.bodies[body_id].apply_impulse(impulse);
}
1
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_add_dynamic(
world: *mut PhysicsWorld,
position: AliceVec3,
mass: f64,
) -> u32 {
ffi_guard(u32::MAX, || match world.as_mut() {
Some(w) => {
let body = RigidBody::new_dynamic(position.to_vec3fix(), Fix128::from_f64(mass));
w.add_body(body) as u32
}
None => u32::MAX,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_add_static(
world: *mut PhysicsWorld,
position: AliceVec3,
) -> u32 {
ffi_guard(u32::MAX, || match world.as_mut() {
Some(w) => {
let body = RigidBody::new_static(position.to_vec3fix());
w.add_body(body) as u32
}
None => u32::MAX,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_add_sensor(
world: *mut PhysicsWorld,
position: AliceVec3,
) -> u32 {
ffi_guard(u32::MAX, || match world.as_mut() {
Some(w) => {
let body = RigidBody::new_sensor(position.to_vec3fix());
w.add_body(body) as u32
}
None => u32::MAX,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_get_info(
world: *const PhysicsWorld,
body_id: u32,
out: *mut AliceBodyInfo,
) -> u8 {
ffi_guard(0, || {
let (w, o) = match (world.as_ref(), out.as_mut()) {
(Some(w), Some(o)) => (w, o),
_ => return 0,
};
match w.bodies.get(body_id as usize) {
Some(b) => {
o.position = AliceVec3::from_vec3fix(b.position);
o.velocity = AliceVec3::from_vec3fix(b.velocity);
o.angular_velocity = AliceVec3::from_vec3fix(b.angular_velocity);
o.rotation = AliceQuat::from_quatfix(b.rotation);
o.inv_mass = b.inv_mass.to_f64();
o.is_static = b.is_static() as u8;
o.is_sensor = b.is_sensor as u8;
1
}
None => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_get_position(
world: *const PhysicsWorld,
body_id: u32,
out: *mut AliceVec3,
) -> u8 {
ffi_guard(0, || {
let (w, o) = match (world.as_ref(), out.as_mut()) {
(Some(w), Some(o)) => (w, o),
_ => return 0,
};
match w.bodies.get(body_id as usize) {
Some(b) => {
*o = AliceVec3::from_vec3fix(b.position);
1
}
None => 0,
}
})
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AliceFix128Raw {
pub hi: i64,
pub lo: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AliceVec3Fix128Raw {
pub x: AliceFix128Raw,
pub y: AliceFix128Raw,
pub z: AliceFix128Raw,
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_get_position_fix128_raw(
world: *const PhysicsWorld,
body_id: u32,
out: *mut AliceVec3Fix128Raw,
) -> u8 {
ffi_guard(0, || {
let (w, o) = match (world.as_ref(), out.as_mut()) {
(Some(w), Some(o)) => (w, o),
_ => return 0,
};
match w.bodies.get(body_id as usize) {
Some(b) => {
*o = AliceVec3Fix128Raw {
x: AliceFix128Raw {
hi: b.position.x.hi,
lo: b.position.x.lo,
},
y: AliceFix128Raw {
hi: b.position.y.hi,
lo: b.position.y.lo,
},
z: AliceFix128Raw {
hi: b.position.z.hi,
lo: b.position.z.lo,
},
};
1
}
None => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_set_position(
world: *mut PhysicsWorld,
body_id: u32,
position: AliceVec3,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => match w.bodies.get_mut(body_id as usize) {
Some(b) => {
b.position = position.to_vec3fix();
1
}
None => 0,
},
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_get_velocity(
world: *const PhysicsWorld,
body_id: u32,
out: *mut AliceVec3,
) -> u8 {
ffi_guard(0, || {
let (w, o) = match (world.as_ref(), out.as_mut()) {
(Some(w), Some(o)) => (w, o),
_ => return 0,
};
match w.bodies.get(body_id as usize) {
Some(b) => {
*o = AliceVec3::from_vec3fix(b.velocity);
1
}
None => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_set_velocity(
world: *mut PhysicsWorld,
body_id: u32,
velocity: AliceVec3,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => match w.bodies.get_mut(body_id as usize) {
Some(b) => {
b.velocity = velocity.to_vec3fix();
1
}
None => 0,
},
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_get_rotation(
world: *const PhysicsWorld,
body_id: u32,
out: *mut AliceQuat,
) -> u8 {
ffi_guard(0, || {
let (w, o) = match (world.as_ref(), out.as_mut()) {
(Some(w), Some(o)) => (w, o),
_ => return 0,
};
match w.bodies.get(body_id as usize) {
Some(b) => {
*o = AliceQuat::from_quatfix(b.rotation);
1
}
None => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_set_restitution(
world: *mut PhysicsWorld,
body_id: u32,
restitution: f64,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => match w.bodies.get_mut(body_id as usize) {
Some(b) => {
b.restitution = Fix128::from_f64(restitution);
1
}
None => 0,
},
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_set_friction(
world: *mut PhysicsWorld,
body_id: u32,
friction: f64,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => match w.bodies.get_mut(body_id as usize) {
Some(b) => {
b.friction = Fix128::from_f64(friction);
1
}
None => 0,
},
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_apply_impulse(
world: *mut PhysicsWorld,
body_id: u32,
impulse: AliceVec3,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => match w.bodies.get_mut(body_id as usize) {
Some(b) => {
b.apply_impulse(impulse.to_vec3fix());
1
}
None => 0,
},
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_apply_impulse_at(
world: *mut PhysicsWorld,
body_id: u32,
impulse: AliceVec3,
point: AliceVec3,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => match w.bodies.get_mut(body_id as usize) {
Some(b) => {
b.apply_impulse_at(impulse.to_vec3fix(), point.to_vec3fix());
1
}
None => 0,
},
None => 0,
})
}
#[no_mangle]
pub extern "C" fn alice_physics_config_default() -> AlicePhysicsConfig {
ffi_guard(
AlicePhysicsConfig {
substeps: 0,
iterations: 0,
gravity_x: 0.0,
gravity_y: 0.0,
gravity_z: 0.0,
damping: 0.0,
},
|| {
let cfg = SolverConfig::default();
AlicePhysicsConfig {
substeps: cfg.substeps as u32,
iterations: cfg.iterations as u32,
gravity_x: cfg.gravity.x.to_f64(),
gravity_y: cfg.gravity.y.to_f64(),
gravity_z: cfg.gravity.z.to_f64(),
damping: cfg.damping.to_f64(),
}
},
)
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_set_gravity(
world: *mut PhysicsWorld,
x: f64,
y: f64,
z: f64,
) {
ffi_guard((), || {
if let Some(w) = world.as_mut() {
w.config.gravity = Vec3Fix::new(
Fix128::from_f64(x),
Fix128::from_f64(y),
Fix128::from_f64(z),
);
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_set_substeps(world: *mut PhysicsWorld, substeps: u32) {
ffi_guard((), || {
if let Some(w) = world.as_mut() {
w.config.substeps = substeps as usize;
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_state_serialize(
world: *const PhysicsWorld,
out_len: *mut u32,
) -> *mut u8 {
ffi_guard(std::ptr::null_mut(), || {
let w = match world.as_ref() {
Some(w) => w,
None => {
if let Some(len) = out_len.as_mut() {
*len = 0;
}
return std::ptr::null_mut();
}
};
let state = w.serialize_state();
let len = state.len();
if let Some(out) = out_len.as_mut() {
*out = len as u32;
}
let boxed = state.into_boxed_slice();
Box::into_raw(boxed) as *mut u8
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_state_deserialize(
world: *mut PhysicsWorld,
data: *const u8,
len: u32,
) -> u8 {
ffi_guard(0, || {
let w = match world.as_mut() {
Some(w) => w,
None => return 0,
};
if data.is_null() || len == 0 {
return 0;
}
let slice = std::slice::from_raw_parts(data, len as usize);
w.deserialize_state(slice) as u8
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_state_free(data: *mut u8, len: u32) {
ffi_guard((), || {
if !data.is_null() && len > 0 {
let slice = std::slice::from_raw_parts_mut(data, len as usize);
drop(Box::from_raw(slice as *mut [u8]));
}
})
}
#[no_mangle]
pub extern "C" fn alice_physics_version() -> *const std::os::raw::c_char {
ffi_guard(std::ptr::null(), || {
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
VERSION.as_ptr().cast()
})
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AlicePhysicsShape {
pub kind: u32,
pub a: f64,
pub b: f64,
pub c: f64,
}
impl AlicePhysicsShape {
fn to_shape(self) -> Option<crate::shape::Shape> {
binding_api::shape(self.kind, self.a, self.b, self.c)
}
}
fn vec3_arg(v: &AliceVec3) -> Option<Vec3Fix> {
binding_api::vec3(v.x, v.y, v.z)
}
fn index_result(r: Option<usize>) -> u32 {
r.and_then(|i| u32::try_from(i).ok()).unwrap_or(u32::MAX)
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_set_collision_radius(
world: *mut PhysicsWorld,
body_id: u32,
radius: f64,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => u8::from(binding_api::set_collision_radius(
w,
body_id as usize,
radius,
)),
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_clear_collision_radius(
world: *mut PhysicsWorld,
body_id: u32,
) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => u8::from(binding_api::clear_collision_radius(w, body_id as usize)),
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_add_shaped(
world: *mut PhysicsWorld,
shape: AlicePhysicsShape,
density: f64,
position: AliceVec3,
) -> u32 {
ffi_guard(u32::MAX, || {
let Some(w) = world.as_mut() else {
return u32::MAX;
};
let (Some(s), Some(p)) = (shape.to_shape(), vec3_arg(&position)) else {
return u32::MAX;
};
index_result(binding_api::add_shaped_body(w, s, density, p))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_set_shape(
world: *mut PhysicsWorld,
body_id: u32,
shape: AlicePhysicsShape,
) -> u8 {
ffi_guard(0, || {
let (Some(w), Some(s)) = (world.as_mut(), shape.to_shape()) else {
return 0;
};
u8::from(binding_api::set_body_shape(w, body_id as usize, s))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_static_add_plane(
world: *mut PhysicsWorld,
normal: AliceVec3,
offset: f64,
) -> u32 {
ffi_guard(u32::MAX, || {
let (Some(w), Some(n)) = (world.as_mut(), vec3_arg(&normal)) else {
return u32::MAX;
};
index_result(binding_api::add_static_plane(w, n, offset))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_static_add_heightfield(
world: *mut PhysicsWorld,
heights: *const f64,
width: u32,
depth: u32,
spacing: f64,
origin: AliceVec3,
) -> u32 {
ffi_guard(u32::MAX, || {
let (Some(w), Some(o)) = (world.as_mut(), vec3_arg(&origin)) else {
return u32::MAX;
};
let Some(count) = (width as usize).checked_mul(depth as usize) else {
return u32::MAX;
};
if heights.is_null() || count == 0 {
return u32::MAX;
}
let hs = std::slice::from_raw_parts(heights, count);
index_result(binding_api::add_static_heightfield(
w, hs, width, depth, spacing, o,
))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_static_add_trimesh(
world: *mut PhysicsWorld,
vertices: *const f64,
vertex_count: u32,
indices: *const u32,
index_count: u32,
) -> u32 {
ffi_guard(u32::MAX, || {
let Some(w) = world.as_mut() else {
return u32::MAX;
};
if vertices.is_null() || indices.is_null() || vertex_count == 0 || index_count == 0 {
return u32::MAX;
}
let Some(coords) = (vertex_count as usize).checked_mul(3) else {
return u32::MAX;
};
let vs = std::slice::from_raw_parts(vertices, coords);
let is = std::slice::from_raw_parts(indices, index_count as usize);
index_result(binding_api::add_static_trimesh(w, vs, is))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_static_remove(world: *mut PhysicsWorld, index: u32) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => u8::from(binding_api::remove_static_collider(w, index as usize)),
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_static_count(world: *const PhysicsWorld) -> u32 {
ffi_guard(0, || match world.as_ref() {
Some(w) => u32::try_from(w.static_collider_count()).unwrap_or(u32::MAX),
None => 0,
})
}
unsafe fn add_joint_ffi(world: *mut PhysicsWorld, make: impl FnOnce() -> Option<Joint>) -> u32 {
ffi_guard(u32::MAX, || {
let Some(w) = world.as_mut() else {
return u32::MAX;
};
match make() {
Some(j) => index_result(binding_api::add_joint(w, j)),
None => u32::MAX,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_joint_add_ball(
world: *mut PhysicsWorld,
body_a: u32,
body_b: u32,
anchor_a: AliceVec3,
anchor_b: AliceVec3,
) -> u32 {
add_joint_ffi(world, || {
Some(Joint::Ball(BallJoint::new(
body_a as usize,
body_b as usize,
vec3_arg(&anchor_a)?,
vec3_arg(&anchor_b)?,
)))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_joint_add_hinge(
world: *mut PhysicsWorld,
body_a: u32,
body_b: u32,
anchor_a: AliceVec3,
anchor_b: AliceVec3,
axis_a: AliceVec3,
axis_b: AliceVec3,
) -> u32 {
add_joint_ffi(world, || {
Some(Joint::Hinge(HingeJoint::new(
body_a as usize,
body_b as usize,
vec3_arg(&anchor_a)?,
vec3_arg(&anchor_b)?,
vec3_arg(&axis_a)?.try_normalize()?,
vec3_arg(&axis_b)?.try_normalize()?,
)))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_joint_add_fixed(
world: *mut PhysicsWorld,
body_a: u32,
body_b: u32,
anchor_a: AliceVec3,
anchor_b: AliceVec3,
relative_rotation: AliceQuat,
) -> u32 {
add_joint_ffi(world, || {
let q = &relative_rotation;
Some(Joint::Fixed(FixedJoint::new(
body_a as usize,
body_b as usize,
vec3_arg(&anchor_a)?,
vec3_arg(&anchor_b)?,
binding_api::unit_quat(q.x, q.y, q.z, q.w)?,
)))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_joint_add_slider(
world: *mut PhysicsWorld,
body_a: u32,
body_b: u32,
axis: AliceVec3,
anchor_a: AliceVec3,
anchor_b: AliceVec3,
) -> u32 {
add_joint_ffi(world, || {
Some(Joint::Slider(SliderJoint::new(
body_a as usize,
body_b as usize,
vec3_arg(&axis)?.try_normalize()?,
vec3_arg(&anchor_a)?,
vec3_arg(&anchor_b)?,
)))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_joint_add_spring(
world: *mut PhysicsWorld,
body_a: u32,
body_b: u32,
anchor_a: AliceVec3,
anchor_b: AliceVec3,
rest_length: f64,
stiffness: f64,
damping: f64,
) -> u32 {
add_joint_ffi(world, || {
Some(Joint::Spring(SpringJoint::new(
body_a as usize,
body_b as usize,
vec3_arg(&anchor_a)?,
vec3_arg(&anchor_b)?,
binding_api::non_negative(rest_length)?,
binding_api::positive(stiffness)?,
binding_api::non_negative(damping)?,
)))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_joint_remove(world: *mut PhysicsWorld, index: u32) -> u8 {
ffi_guard(0, || match world.as_mut() {
Some(w) => u8::from(binding_api::remove_joint(w, index as usize)),
None => 0,
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_joint_count(world: *const PhysicsWorld) -> u32 {
ffi_guard(0, || match world.as_ref() {
Some(w) => u32::try_from(w.joint_count()).unwrap_or(u32::MAX),
None => 0,
})
}
pub const ALICE_PHYSICS_NO_BODY: u32 = u32::MAX;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AliceQueryHit {
pub t: f64,
pub point: AliceVec3,
pub normal: AliceVec3,
pub target_kind: u32,
pub target_index: u32,
pub body: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AliceQueryTarget {
pub kind: u32,
pub index: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AliceBodyObservation {
pub body_index: u32,
pub position: AliceVec3,
pub velocity: AliceVec3,
pub rotation: AliceQuat,
pub angular_velocity: AliceVec3,
pub sleeping: u8,
pub in_contact: u8,
}
fn u32_index(i: usize) -> u32 {
u32::try_from(i).unwrap_or(u32::MAX)
}
fn exclude_arg(exclude_body: u32) -> Option<usize> {
(exclude_body != ALICE_PHYSICS_NO_BODY).then_some(exclude_body as usize)
}
fn write_hit(hit: Option<binding_api::QueryHit>, out: &mut AliceQueryHit) -> u8 {
let Some(h) = hit else {
return 0;
};
let [px, py, pz] = h.point;
let [nx, ny, nz] = h.normal;
*out = AliceQueryHit {
t: h.t,
point: AliceVec3 {
x: px,
y: py,
z: pz,
},
normal: AliceVec3 {
x: nx,
y: ny,
z: nz,
},
target_kind: h.target.0,
target_index: u32_index(h.target.1),
body: h.body.map_or(ALICE_PHYSICS_NO_BODY, u32_index),
};
1
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_cast_ray(
world: *const PhysicsWorld,
origin: AliceVec3,
direction: AliceVec3,
max_t: f64,
exclude_body: u32,
out: *mut AliceQueryHit,
) -> u8 {
ffi_guard(0, || {
let (Some(w), Some(out)) = (world.as_ref(), out.as_mut()) else {
return 0;
};
let (Some(o), Some(d), Some(m), Some(f)) = (
vec3_arg(&origin),
vec3_arg(&direction),
binding_api::finite(max_t),
binding_api::query_filter(w, exclude_arg(exclude_body)),
) else {
return 0;
};
write_hit(binding_api::cast_ray(w, o, d, m, &f), out)
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_cast_sphere(
world: *const PhysicsWorld,
center: AliceVec3,
radius: f64,
direction: AliceVec3,
max_t: f64,
exclude_body: u32,
out: *mut AliceQueryHit,
) -> u8 {
ffi_guard(0, || {
let (Some(w), Some(out)) = (world.as_ref(), out.as_mut()) else {
return 0;
};
let (Some(c), Some(r), Some(d), Some(m), Some(f)) = (
vec3_arg(¢er),
binding_api::finite(radius),
vec3_arg(&direction),
binding_api::finite(max_t),
binding_api::query_filter(w, exclude_arg(exclude_body)),
) else {
return 0;
};
write_hit(binding_api::cast_sphere(w, c, r, d, m, &f), out)
})
}
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn alice_physics_world_cast_capsule(
world: *const PhysicsWorld,
a: AliceVec3,
b: AliceVec3,
radius: f64,
direction: AliceVec3,
max_t: f64,
exclude_body: u32,
out: *mut AliceQueryHit,
) -> u8 {
ffi_guard(0, || {
let (Some(w), Some(out)) = (world.as_ref(), out.as_mut()) else {
return 0;
};
let (Some(a), Some(b), Some(r), Some(d), Some(m), Some(f)) = (
vec3_arg(&a),
vec3_arg(&b),
binding_api::finite(radius),
vec3_arg(&direction),
binding_api::finite(max_t),
binding_api::query_filter(w, exclude_arg(exclude_body)),
) else {
return 0;
};
write_hit(binding_api::cast_capsule(w, a, b, r, d, m, &f), out)
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_world_overlap_sphere(
world: *const PhysicsWorld,
center: AliceVec3,
radius: f64,
exclude_body: u32,
out: *mut AliceQueryTarget,
capacity: u32,
) -> u32 {
ffi_guard(u32::MAX, || {
let Some(w) = world.as_ref() else {
return u32::MAX;
};
if out.is_null() && capacity > 0 {
return u32::MAX;
}
let (Some(c), Some(r), Some(f)) = (
vec3_arg(¢er),
binding_api::finite(radius),
binding_api::query_filter(w, exclude_arg(exclude_body)),
) else {
return u32::MAX;
};
let found = binding_api::overlap_sphere(w, c, r, &f);
let n = found.len().min(capacity as usize);
if n > 0 {
let buf = std::slice::from_raw_parts_mut(out, n);
for (slot, &(kind, index)) in buf.iter_mut().zip(&found) {
*slot = AliceQueryTarget {
kind,
index: u32_index(index),
};
}
}
u32::try_from(found.len()).map_or(u32::MAX - 1, |n| n.min(u32::MAX - 1))
})
}
#[no_mangle]
pub unsafe extern "C" fn alice_physics_body_observe(
world: *const PhysicsWorld,
body_id: u32,
out: *mut AliceBodyObservation,
) -> u8 {
ffi_guard(0, || {
let (Some(w), Some(out)) = (world.as_ref(), out.as_mut()) else {
return 0;
};
let Some(o) = binding_api::observe_body(w, body_id as usize) else {
return 0;
};
let v = |[x, y, z]: [f64; 3]| AliceVec3 { x, y, z };
let [qx, qy, qz, qw] = o.rotation;
*out = AliceBodyObservation {
body_index: u32_index(o.body_index),
position: v(o.position),
velocity: v(o.velocity),
rotation: AliceQuat {
x: qx,
y: qy,
z: qz,
w: qw,
},
angular_velocity: v(o.angular_velocity),
sleeping: u8::from(o.sleeping),
in_contact: u8::from(o.in_contact),
};
1
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_string_matches_cargo_pkg_version() {
let s = unsafe { std::ffi::CStr::from_ptr(alice_physics_version()) };
assert_eq!(s.to_str().unwrap(), env!("CARGO_PKG_VERSION"));
}
#[test]
fn test_vec3_conversion_roundtrip() {
let original = Vec3Fix::new(
Fix128::from_f64(1.5),
Fix128::from_f64(-2.25),
Fix128::from_f64(3.75),
);
let c = AliceVec3::from_vec3fix(original);
let back = c.to_vec3fix();
assert_eq!(original.x.hi, back.x.hi);
assert_eq!(original.y.hi, back.y.hi);
assert_eq!(original.z.hi, back.z.hi);
}
#[test]
fn test_world_create_destroy() {
unsafe {
let world = alice_physics_world_create();
assert!(!world.is_null());
assert_eq!(alice_physics_world_body_count(world), 0);
alice_physics_world_destroy(world);
}
}
#[test]
fn test_world_destroy_null() {
unsafe {
alice_physics_world_destroy(std::ptr::null_mut());
}
}
#[test]
fn test_world_create_with_config() {
unsafe {
let config = alice_physics_config_default();
let world = alice_physics_world_create_with_config(config);
assert!(!world.is_null());
alice_physics_world_destroy(world);
}
}
#[test]
fn test_body_add_and_get() {
unsafe {
let world = alice_physics_world_create();
let pos = AliceVec3 {
x: 1.0,
y: 2.0,
z: 3.0,
};
let id = alice_physics_body_add_dynamic(world, pos, 1.0);
assert_eq!(id, 0);
assert_eq!(alice_physics_world_body_count(world), 1);
let mut out = AliceVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
let ok = alice_physics_body_get_position(world, id, &mut out);
assert_eq!(ok, 1);
assert!((out.x - 1.0).abs() < 1e-10);
assert!((out.y - 2.0).abs() < 1e-10);
assert!((out.z - 3.0).abs() < 1e-10);
alice_physics_world_destroy(world);
}
}
#[test]
fn test_body_get_invalid_id() {
unsafe {
let world = alice_physics_world_create();
let mut out = AliceVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
let ok = alice_physics_body_get_position(world, 999, &mut out);
assert_eq!(ok, 0);
alice_physics_world_destroy(world);
}
}
#[test]
fn test_null_world_safety() {
unsafe {
let null: *mut PhysicsWorld = std::ptr::null_mut();
assert_eq!(alice_physics_world_body_count(null), 0);
assert_eq!(alice_physics_world_step(null, 0.016), 0);
assert_eq!(alice_physics_world_step_n(null, 0.016, 10), 0);
let pos = AliceVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
assert_eq!(alice_physics_body_add_dynamic(null, pos, 1.0), u32::MAX);
}
}
#[test]
fn test_step_and_gravity() {
unsafe {
let world = alice_physics_world_create();
let pos = AliceVec3 {
x: 0.0,
y: 10.0,
z: 0.0,
};
let id = alice_physics_body_add_dynamic(world, pos, 1.0);
for _ in 0..60 {
alice_physics_world_step(world, 1.0 / 60.0);
}
let mut out = AliceVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
alice_physics_body_get_position(world, id, &mut out);
assert!(out.y < 10.0, "Body should fall under gravity, y={}", out.y);
alice_physics_world_destroy(world);
}
}
#[test]
fn test_batch_positions() {
unsafe {
let world = alice_physics_world_create();
let p1 = AliceVec3 {
x: 1.0,
y: 2.0,
z: 3.0,
};
let p2 = AliceVec3 {
x: 4.0,
y: 5.0,
z: 6.0,
};
alice_physics_body_add_static(world, p1);
alice_physics_body_add_static(world, p2);
let mut buf = [0.0f64; 6];
let ok = alice_physics_world_get_positions_batch(world, buf.as_mut_ptr(), 6);
assert_eq!(ok, 1);
assert!((buf[0] - 1.0).abs() < 1e-10);
assert!((buf[1] - 2.0).abs() < 1e-10);
assert!((buf[2] - 3.0).abs() < 1e-10);
assert!((buf[3] - 4.0).abs() < 1e-10);
assert!((buf[4] - 5.0).abs() < 1e-10);
assert!((buf[5] - 6.0).abs() < 1e-10);
let fail = alice_physics_world_get_positions_batch(world, buf.as_mut_ptr(), 3);
assert_eq!(fail, 0);
alice_physics_world_destroy(world);
}
}
#[test]
fn test_state_serialization_ffi() {
unsafe {
let world = alice_physics_world_create();
let pos = AliceVec3 {
x: 0.0,
y: 10.0,
z: 0.0,
};
alice_physics_body_add_dynamic(world, pos, 1.0);
let mut len: u32 = 0;
let data = alice_physics_state_serialize(world, &mut len);
assert!(!data.is_null());
assert!(len > 0);
let ok = alice_physics_state_deserialize(world, data, len);
assert_eq!(ok, 1);
alice_physics_state_free(data, len);
alice_physics_world_destroy(world);
}
}
#[test]
fn test_impulse_application() {
unsafe {
let world = alice_physics_world_create();
alice_physics_world_set_gravity(world, 0.0, 0.0, 0.0);
let pos = AliceVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
let id = alice_physics_body_add_dynamic(world, pos, 1.0);
let impulse = AliceVec3 {
x: 10.0,
y: 0.0,
z: 0.0,
};
alice_physics_body_apply_impulse(world, id, impulse);
alice_physics_world_step(world, 1.0 / 60.0);
let mut out = AliceVec3 {
x: 0.0,
y: 0.0,
z: 0.0,
};
alice_physics_body_get_position(world, id, &mut out);
assert!(out.x > 0.0, "Impulse should move body right, x={}", out.x);
alice_physics_world_destroy(world);
}
}
#[test]
fn test_body_info() {
unsafe {
let world = alice_physics_world_create();
let pos = AliceVec3 {
x: 1.0,
y: 2.0,
z: 3.0,
};
let id = alice_physics_body_add_sensor(world, pos);
let mut info = std::mem::zeroed::<AliceBodyInfo>();
let ok = alice_physics_body_get_info(world, id, &mut info);
assert_eq!(ok, 1);
assert_eq!(info.is_sensor, 1);
assert!((info.position.x - 1.0).abs() < 1e-10);
alice_physics_world_destroy(world);
}
}
#[test]
fn ffi_contract_gravity_fall_deterministic() {
unsafe {
let world = alice_physics_world_create();
let pos = AliceVec3 {
x: 0.0,
y: 10.0,
z: 0.0,
};
let mass = 1.0_f64;
let id = alice_physics_body_add_dynamic(world, pos, mass);
for _ in 0..60 {
let _ = alice_physics_world_step(world, 1.0 / 60.0);
}
let mut info = std::mem::zeroed::<AliceBodyInfo>();
let ok = alice_physics_body_get_info(world, id, &mut info);
assert_eq!(ok, 1);
assert!(
info.position.y < 10.0,
"body must fall under gravity, got y={}",
info.position.y
);
assert!(
info.position.y > -20.0,
"final y out of expected range, got y={}",
info.position.y
);
assert!(
info.position.x.abs() < 1e-9,
"no horizontal drift expected, got x={}",
info.position.x
);
assert!(
info.position.z.abs() < 1e-9,
"no horizontal drift expected, got z={}",
info.position.z
);
let mut raw = std::mem::zeroed::<AliceVec3Fix128Raw>();
let ok_raw = alice_physics_body_get_position_fix128_raw(world, id, &mut raw);
assert_eq!(ok_raw, 1, "raw fix128 getter must succeed");
let world2 = alice_physics_world_create();
let pos2 = AliceVec3 {
x: 0.0,
y: 10.0,
z: 0.0,
};
let id2 = alice_physics_body_add_dynamic(world2, pos2, mass);
for _ in 0..60 {
let _ = alice_physics_world_step(world2, 1.0 / 60.0);
}
let mut raw2 = std::mem::zeroed::<AliceVec3Fix128Raw>();
let ok_raw2 = alice_physics_body_get_position_fix128_raw(world2, id2, &mut raw2);
assert_eq!(
ok_raw2, 1,
"raw fix128 getter must succeed on the replay world"
);
alice_physics_world_destroy(world2);
assert_eq!(raw.x.hi, raw2.x.hi, "byte-for-byte determinism (x.hi)");
assert_eq!(raw.x.lo, raw2.x.lo, "byte-for-byte determinism (x.lo)");
assert_eq!(raw.y.hi, raw2.y.hi, "byte-for-byte determinism (y.hi)");
assert_eq!(raw.y.lo, raw2.y.lo, "byte-for-byte determinism (y.lo)");
assert_eq!(raw.z.hi, raw2.z.hi, "byte-for-byte determinism (z.hi)");
assert_eq!(raw.z.lo, raw2.z.lo, "byte-for-byte determinism (z.lo)");
const GOLDEN: [(i64, u64); 3] = [(0, 0), (5, 15_853_912_786_096_175_825), (0, 0)];
let y = raw.y.hi as f64 + raw.y.lo as f64 / (1u128 << 64) as f64;
assert!((y - 5.859_4).abs() < 1e-3, "FFI free fall y = {y}");
assert_eq!(
[
(raw.x.hi, raw.x.lo),
(raw.y.hi, raw.y.lo),
(raw.z.hi, raw.z.lo)
],
GOLDEN,
"FFI free-fall golden raw Fix128 pair drifted"
);
alice_physics_world_destroy(world);
}
}
#[test]
fn ffi_guard_turns_panic_into_sentinel_and_message() {
guard::clear_last_error();
let v = ffi_guard(u32::MAX, || {
if true {
panic!("boom {}", 42);
}
7
});
assert_eq!(v, u32::MAX);
let msg = guard::take_last_error().expect("message recorded");
assert!(msg.contains("boom 42"), "{msg}");
assert!(guard::take_last_error().is_none(), "take clears the slot");
assert_eq!(ffi_guard(0u8, || 1u8), 1);
assert!(guard::take_last_error().is_none());
}
#[test]
fn last_error_ffi_round_trips_through_c_string() {
guard::clear_last_error();
assert!(alice_physics_last_error().is_null());
guard::set_last_error("alice-physics FFI panic: index 7 out of range");
let p = alice_physics_last_error();
assert!(!p.is_null());
let text = unsafe { std::ffi::CStr::from_ptr(p) }
.to_str()
.expect("utf-8")
.to_owned();
assert_eq!(text, "alice-physics FFI panic: index 7 out of range");
unsafe { alice_physics_string_free(p) };
assert!(alice_physics_last_error().is_null());
guard::set_last_error("x");
alice_physics_clear_last_error();
assert!(alice_physics_last_error().is_null());
unsafe { alice_physics_string_free(std::ptr::null_mut()) };
}
#[test]
fn hostile_inputs_return_sentinels_without_unwinding() {
let null = std::ptr::null_mut::<PhysicsWorld>();
unsafe {
assert_eq!(alice_physics_world_step(null, 1.0 / 60.0), 0);
assert_eq!(alice_physics_world_step_n(null, 1.0 / 60.0, 5), 0);
assert_eq!(alice_physics_world_body_count(null), 0);
assert_eq!(
alice_physics_body_add_static(
null,
AliceVec3 {
x: 0.0,
y: 0.0,
z: 0.0
}
),
u32::MAX
);
alice_physics_world_set_gravity(null, 0.0, -1.0, 0.0);
alice_physics_world_set_substeps(null, 3);
alice_physics_world_destroy(null);
}
let world = alice_physics_world_create();
unsafe {
alice_physics_world_set_substeps(world, 0);
alice_physics_body_add_dynamic(
world,
AliceVec3 {
x: 0.0,
y: 5.0,
z: 0.0,
},
1.0,
);
assert_eq!(alice_physics_world_step(world, 1.0 / 60.0), 1);
assert_eq!(alice_physics_world_step(world, f64::NAN), 1);
assert_eq!(alice_physics_world_step(world, f64::INFINITY), 1);
let junk = [0xFFu8; 13];
assert_eq!(alice_physics_state_deserialize(world, junk.as_ptr(), 13), 0);
alice_physics_world_destroy(world);
}
}
fn v(x: f64, y: f64, z: f64) -> AliceVec3 {
AliceVec3 { x, y, z }
}
fn fx(x: f64) -> Fix128 {
Fix128::from_f64(x)
}
fn vf(x: f64, y: f64, z: f64) -> Vec3Fix {
Vec3Fix::new(fx(x), fx(y), fx(z))
}
unsafe fn twin_worlds() -> (*mut PhysicsWorld, PhysicsWorld) {
let world = alice_physics_world_create();
alice_physics_body_add_static(world, v(0.0, 0.0, 0.0));
alice_physics_body_add_dynamic(world, v(2.0, 0.0, 0.0), 1.0);
let mut rust = PhysicsWorld::new(SolverConfig::default());
rust.add_body(RigidBody::new_static(Vec3Fix::ZERO));
rust.add_body(RigidBody::new_dynamic(vf(2.0, 0.0, 0.0), Fix128::ONE));
(world, rust)
}
fn same_state(a: &PhysicsWorld, b: &PhysicsWorld) {
assert_eq!(a.bodies.len(), b.bodies.len());
for (x, y) in a.bodies.iter().zip(&b.bodies) {
assert_eq!(
(
x.position,
x.velocity,
x.rotation,
x.inv_mass,
x.inv_inertia
),
(
y.position,
y.velocity,
y.rotation,
y.inv_mass,
y.inv_inertia
)
);
}
assert_eq!(a.joints, b.joints);
assert_eq!(a.static_collider_count(), b.static_collider_count());
}
#[test]
fn binding_calls_match_the_rust_api_bit_for_bit() {
unsafe {
let (world, mut rust) = twin_worlds();
let w = &mut *world;
assert_eq!(alice_physics_body_set_collision_radius(world, 1, 0.5), 1);
rust.set_body_collision_radius(1, fx(0.5));
let cube = AlicePhysicsShape {
kind: 0,
a: 0.5,
b: 0.5,
c: 0.5,
};
let id = alice_physics_body_add_shaped(world, cube, 2.0, v(5.0, 3.0, 0.0));
assert_eq!(id, 2);
let rust_cube = crate::shape::Shape::Box {
half_extents: vf(0.5, 0.5, 0.5),
};
rust.add_shaped_body(&rust_cube, fx(2.0), vf(5.0, 3.0, 0.0))
.unwrap();
let cyl = AlicePhysicsShape {
kind: 1,
a: 0.25,
b: 1.0,
c: 0.0,
};
assert_eq!(alice_physics_body_set_shape(world, 1, cyl), 1);
let rust_cyl = crate::shape::Shape::Cylinder {
radius: fx(0.25),
half_height: Fix128::ONE,
};
assert!(rust.set_body_shape(1, &rust_cyl));
assert_eq!(
alice_physics_static_add_plane(world, v(0.0, 2.0, 0.0), -1.0),
0
);
rust.add_static_collider(crate::static_collider::StaticCollider::Plane(
crate::plane_collider::PlaneCollider::new(Vec3Fix::UNIT_Y, -Fix128::ONE),
));
assert_eq!(
alice_physics_joint_add_ball(world, 0, 1, v(0.0, 0.0, 0.0), v(-2.0, 0.0, 0.0)),
0
);
rust.add_joint(Joint::Ball(BallJoint::new(
0,
1,
Vec3Fix::ZERO,
vf(-2.0, 0.0, 0.0),
)));
assert_eq!(
alice_physics_joint_add_spring(
world,
1,
2,
v(0.0, 0.0, 0.0),
v(0.0, 0.0, 0.0),
3.0,
50.0,
0.5
),
1
);
rust.add_joint(Joint::Spring(SpringJoint::new(
1,
2,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
fx(3.0),
fx(50.0),
fx(0.5),
)));
assert_eq!(alice_physics_joint_count(world), 2);
assert_eq!(alice_physics_static_count(world), 1);
let dt = Fix128::from_ratio(1, 60);
for _ in 0..30 {
assert_eq!(alice_physics_world_step(world, 1.0 / 60.0), 1);
rust.step(dt);
}
same_state(w, &rust);
alice_physics_world_destroy(world);
}
}
#[test]
fn joint_and_static_constructors_store_what_was_passed() {
unsafe {
let (world, _) = twin_worlds();
alice_physics_body_add_dynamic(world, v(0.0, 5.0, 0.0), 1.0);
let h = alice_physics_joint_add_hinge(
world,
0,
1,
v(0.0, 0.0, 0.0),
v(1.0, 0.0, 0.0),
v(0.0, 0.0, 3.0),
v(0.0, 0.0, 1.0),
);
let f = alice_physics_joint_add_fixed(
world,
0,
2,
v(0.0, 0.0, 0.0),
v(0.0, 0.0, 0.0),
AliceQuat {
x: 0.0,
y: 0.0,
z: 0.0,
w: 2.0,
},
);
let s = alice_physics_joint_add_slider(
world,
1,
2,
v(2.0, 0.0, 0.0),
v(0.0, 0.0, 0.0),
v(0.0, 0.0, 0.0),
);
assert_eq!((h, f, s), (0, 1, 2));
let w = &*world;
match w.joints[0] {
Joint::Hinge(j) => {
assert_eq!((j.body_a, j.body_b), (0, 1));
assert_eq!(j.local_axis_a, Vec3Fix::UNIT_Z);
assert_eq!(j.local_anchor_b, vf(1.0, 0.0, 0.0));
}
other => panic!("expected a hinge, got {other:?}"),
}
match w.joints[1] {
Joint::Fixed(j) => assert_eq!(j.relative_rotation, QuatFix::IDENTITY),
other => panic!("expected a fixed joint, got {other:?}"),
}
match w.joints[2] {
Joint::Slider(j) => assert_eq!(j.local_axis, Vec3Fix::UNIT_X),
other => panic!("expected a slider, got {other:?}"),
}
assert_eq!(alice_physics_joint_remove(world, 0), 1);
assert_eq!(alice_physics_joint_count(world), 2);
let w = &*world;
assert!(
matches!(w.joints[0], Joint::Slider(_)),
"the last joint moves into the hole"
);
let heights = [0.0, 0.5, 1.0, 1.5, 2.0, 2.5];
assert_eq!(
alice_physics_static_add_heightfield(
world,
heights.as_ptr(),
3,
2,
1.0,
v(0.0, 0.0, 0.0)
),
0
);
let verts = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0];
let idx = [0u32, 1, 2];
assert_eq!(
alice_physics_static_add_trimesh(world, verts.as_ptr(), 3, idx.as_ptr(), 3),
1
);
assert_eq!(alice_physics_static_count(world), 2);
assert_eq!(alice_physics_static_remove(world, 0), 1);
assert_eq!(alice_physics_static_count(world), 1);
alice_physics_world_destroy(world);
}
}
#[test]
fn binding_calls_refuse_bad_arguments_without_touching_the_world() {
unsafe {
let (world, _) = twin_worlds();
alice_physics_clear_last_error();
let nan = f64::NAN;
let null: *mut PhysicsWorld = std::ptr::null_mut();
let unit = AlicePhysicsShape {
kind: 0,
a: 1.0,
b: 1.0,
c: 1.0,
};
let q1 = AliceQuat {
x: 0.0,
y: 0.0,
z: 0.0,
w: 1.0,
};
let o = v(0.0, 0.0, 0.0);
for r in [0.0, -1.0, nan, f64::INFINITY] {
assert_eq!(
alice_physics_body_set_collision_radius(world, 1, r),
0,
"radius {r}"
);
}
assert_eq!(alice_physics_body_set_collision_radius(world, 9, 1.0), 0);
assert_eq!(alice_physics_body_set_collision_radius(null, 0, 1.0), 0);
assert_eq!(alice_physics_body_clear_collision_radius(world, 9), 0);
let bad_shapes = [
AlicePhysicsShape { kind: 6, ..unit },
AlicePhysicsShape { a: 0.0, ..unit },
AlicePhysicsShape { c: nan, ..unit },
AlicePhysicsShape {
kind: 5,
a: 1.0,
b: 1.0,
c: 0.0,
},
];
for s in bad_shapes {
assert_eq!(alice_physics_body_add_shaped(world, s, 1.0, o), u32::MAX);
assert_eq!(alice_physics_body_set_shape(world, 1, s), 0);
}
assert_eq!(alice_physics_body_add_shaped(world, unit, 0.0, o), u32::MAX);
assert_eq!(
alice_physics_body_add_shaped(world, unit, 1.0, v(nan, 0.0, 0.0)),
u32::MAX
);
assert_eq!(alice_physics_body_set_shape(world, 9, unit), 0);
assert_eq!(alice_physics_static_add_plane(world, o, 0.0), u32::MAX);
assert_eq!(
alice_physics_static_add_plane(world, v(0.0, 1.0, 0.0), nan),
u32::MAX
);
let hs = [0.0; 4];
assert_eq!(
alice_physics_static_add_heightfield(world, hs.as_ptr(), 1, 4, 1.0, o),
u32::MAX
);
assert_eq!(
alice_physics_static_add_heightfield(world, hs.as_ptr(), 2, 2, 0.0, o),
u32::MAX
);
assert_eq!(
alice_physics_static_add_heightfield(world, std::ptr::null(), 2, 2, 1.0, o),
u32::MAX
);
let vs = [0.0; 9];
assert_eq!(
alice_physics_static_add_trimesh(world, vs.as_ptr(), 3, [0u32, 1, 3].as_ptr(), 3),
u32::MAX
);
assert_eq!(
alice_physics_static_add_trimesh(world, vs.as_ptr(), 3, [0u32, 1].as_ptr(), 2),
u32::MAX
);
assert_eq!(
alice_physics_static_add_trimesh(world, vs.as_ptr(), 0, [0u32, 1, 2].as_ptr(), 3),
u32::MAX
);
assert_eq!(alice_physics_static_remove(world, 0), 0);
assert_eq!(
alice_physics_joint_add_ball(world, 1, 1, o, o),
u32::MAX,
"self joint"
);
assert_eq!(
alice_physics_joint_add_ball(world, 0, 9, o, o),
u32::MAX,
"unknown body"
);
assert_eq!(
alice_physics_joint_add_ball(world, 0, 1, v(nan, 0.0, 0.0), o),
u32::MAX
);
assert_eq!(
alice_physics_joint_add_hinge(world, 0, 1, o, o, o, v(0.0, 0.0, 1.0)),
u32::MAX,
"zero axis"
);
assert_eq!(
alice_physics_joint_add_fixed(world, 0, 1, o, o, AliceQuat { w: 0.0, ..q1 }),
u32::MAX,
"zero rotation"
);
assert_eq!(
alice_physics_joint_add_slider(world, 0, 1, o, o, o),
u32::MAX,
"zero axis"
);
assert_eq!(
alice_physics_joint_add_spring(world, 0, 1, o, o, 1.0, 0.0, 0.0),
u32::MAX,
"stiffness 0"
);
assert_eq!(
alice_physics_joint_add_spring(world, 0, 1, o, o, -1.0, 1.0, 0.0),
u32::MAX,
"rest < 0"
);
assert_eq!(alice_physics_joint_add_ball(null, 0, 1, o, o), u32::MAX);
assert_eq!(alice_physics_joint_remove(world, 0), 0);
assert_eq!(alice_physics_joint_count(null), 0);
assert_eq!(alice_physics_static_count(null), 0);
let w = &*world;
assert_eq!(w.bodies.len(), 2);
assert!(w.joints.is_empty());
assert_eq!(w.static_collider_count(), 0);
assert!(
alice_physics_last_error().is_null(),
"a refusal must not come from a caught panic"
);
alice_physics_world_destroy(world);
}
}
unsafe fn query_twins() -> (*mut PhysicsWorld, PhysicsWorld) {
let world = alice_physics_world_create();
assert_eq!(alice_physics_body_add_static(world, v(0.0, 0.0, 10.0)), 0);
assert_eq!(
alice_physics_body_add_dynamic(world, v(0.0, 0.0, 20.0), 1.0),
1
);
assert_eq!(alice_physics_body_set_collision_radius(world, 0, 1.0), 1);
assert_eq!(alice_physics_body_set_collision_radius(world, 1, 1.0), 1);
assert_eq!(
alice_physics_static_add_plane(world, v(0.0, 1.0, 0.0), -2.0),
0
);
let mut rust = PhysicsWorld::new(SolverConfig::default());
rust.add_body(RigidBody::new_static(vf(0.0, 0.0, 10.0)));
rust.add_body(RigidBody::new_dynamic(vf(0.0, 0.0, 20.0), Fix128::ONE));
rust.set_body_collision_radius(0, Fix128::ONE);
rust.set_body_collision_radius(1, Fix128::ONE);
rust.add_static_collider(crate::static_collider::StaticCollider::Plane(
crate::plane_collider::PlaneCollider::new(Vec3Fix::UNIT_Y, fx(-2.0)),
));
(world, rust)
}
fn no_hit() -> AliceQueryHit {
AliceQueryHit {
t: -1.0,
point: v(0.0, 0.0, 0.0),
normal: v(0.0, 0.0, 0.0),
target_kind: 7,
target_index: 7,
body: 7,
}
}
fn same_hit(c: &AliceQueryHit, t: Fix128, p: Vec3Fix, n: Vec3Fix, target: (u32, u32, u32)) {
assert_eq!(c.t.to_bits(), t.to_f64().to_bits(), "t");
assert_eq!(c.point, AliceVec3::from_vec3fix(p), "point");
assert_eq!(c.normal, AliceVec3::from_vec3fix(n), "normal");
assert_eq!((c.target_kind, c.target_index, c.body), target, "target");
}
fn close(c: AliceVec3, x: f64, y: f64, z: f64) {
let d = (c.x - x).abs().max((c.y - y).abs()).max((c.z - z).abs());
assert!(d < 1e-12, "{c:?} vs ({x}, {y}, {z})");
}
#[test]
fn query_calls_match_the_rust_api_and_the_closed_form() {
unsafe {
let (world, rust) = query_twins();
let f = crate::shape_raycast::RayFilter::default();
let o = v(0.0, 0.0, 0.0);
let pz = v(0.0, 0.0, 1.0);
let none = ALICE_PHYSICS_NO_BODY;
let mut hit = no_hit();
assert_eq!(
alice_physics_world_cast_ray(world, o, pz, 100.0, none, &mut hit),
1
);
let r = rust
.cast_ray(Vec3Fix::ZERO, Vec3Fix::UNIT_Z, fx(100.0), &f)
.unwrap();
same_hit(&hit, r.t, r.point, r.normal, (0, 0, 0));
assert!((hit.t - 9.0).abs() < 1e-12, "t = {}", hit.t);
close(hit.point, 0.0, 0.0, 9.0);
close(hit.normal, 0.0, 0.0, -1.0);
let off = v(0.25, 0.5, 0.0);
assert_eq!(
alice_physics_world_cast_ray(world, off, pz, 100.0, none, &mut hit),
1
);
let r = rust
.cast_ray(vf(0.25, 0.5, 0.0), Vec3Fix::UNIT_Z, fx(100.0), &f)
.unwrap();
same_hit(&hit, r.t, r.point, r.normal, (0, 0, 0));
let tc = 10.0 - 0.6875f64.sqrt();
assert!((hit.t - tc).abs() < 1e-12, "t = {}", hit.t);
close(hit.point, 0.25, 0.5, tc);
close(hit.normal, 0.25, 0.5, tc - 10.0);
assert_eq!(
alice_physics_world_cast_ray(world, o, pz, 100.0, 0, &mut hit),
1
);
let r = rust
.cast_ray(
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
fx(100.0),
&f.excluding_body(0),
)
.unwrap();
same_hit(&hit, r.t, r.point, r.normal, (0, 1, 1));
assert!((hit.t - 19.0).abs() < 1e-12, "t = {}", hit.t);
assert_eq!(
alice_physics_world_cast_ray(world, o, v(0.0, -3.0, 0.0), 10.0, none, &mut hit),
1
);
let r = rust
.cast_ray(Vec3Fix::ZERO, vf(0.0, -3.0, 0.0), fx(10.0), &f)
.unwrap();
same_hit(&hit, r.t, r.point, r.normal, (1, 0, none));
assert!((hit.t - 2.0).abs() < 1e-12, "t = {}", hit.t);
close(hit.point, 0.0, -2.0, 0.0);
close(hit.normal, 0.0, 1.0, 0.0);
let mut miss = no_hit();
assert_eq!(
alice_physics_world_cast_ray(world, o, v(1.0, 0.0, 0.0), 100.0, none, &mut miss),
0
);
assert!(rust
.cast_ray(Vec3Fix::ZERO, Vec3Fix::UNIT_X, fx(100.0), &f)
.is_none());
assert_eq!(miss.target_kind, 7);
assert_eq!(
alice_physics_world_cast_sphere(world, o, 0.5, pz, 100.0, none, &mut hit),
1
);
let r = rust
.cast_sphere(Vec3Fix::ZERO, fx(0.5), Vec3Fix::UNIT_Z, fx(100.0), &f)
.unwrap();
same_hit(&hit, r.t, r.point, r.normal, (0, 0, 0));
assert!((hit.t - 8.5).abs() < 1e-12, "t = {}", hit.t);
close(hit.point, 0.0, 0.0, 9.0);
close(hit.normal, 0.0, 0.0, -1.0);
let (a, b) = (v(-1.0, 0.0, 0.0), v(1.0, 0.0, 0.0));
assert_eq!(
alice_physics_world_cast_capsule(world, a, b, 0.5, pz, 100.0, none, &mut hit),
1
);
let r = rust
.cast_capsule(
vf(-1.0, 0.0, 0.0),
vf(1.0, 0.0, 0.0),
fx(0.5),
Vec3Fix::UNIT_Z,
fx(100.0),
&f,
)
.unwrap();
same_hit(&hit, r.t, r.point, r.normal, (0, 0, 0));
assert!((hit.t - 8.5).abs() < 1e-12, "t = {}", hit.t);
close(hit.point, 0.0, 0.0, 9.0);
let mut out = [AliceQueryTarget { kind: 7, index: 7 }; 4];
let n = alice_physics_world_overlap_sphere(
world,
v(0.0, -1.2, 10.0),
1.0,
none,
out.as_mut_ptr(),
4,
);
let r = rust.overlap_sphere(vf(0.0, -1.2, 10.0), Fix128::ONE, &f);
use crate::shape_raycast::RayTarget;
assert_eq!(r, vec![RayTarget::Body(0), RayTarget::StaticCollider(0)]);
assert_eq!(n, 2);
assert_eq!(
out,
[
AliceQueryTarget { kind: 0, index: 0 },
AliceQueryTarget { kind: 1, index: 0 },
AliceQueryTarget { kind: 7, index: 7 },
AliceQueryTarget { kind: 7, index: 7 },
]
);
let n = alice_physics_world_overlap_sphere(
world,
v(0.0, -1.2, 10.0),
1.0,
0,
out.as_mut_ptr(),
4,
);
assert_eq!(n, 1);
assert_eq!(out[0], AliceQueryTarget { kind: 1, index: 0 });
alice_physics_world_destroy(world);
}
}
#[test]
fn query_structs_have_the_header_layout() {
assert_eq!(std::mem::size_of::<AliceQueryHit>(), 72);
assert_eq!(std::mem::offset_of!(AliceQueryHit, body), 64);
assert_eq!(std::mem::size_of::<AliceQueryTarget>(), 8);
assert_eq!(std::mem::size_of::<AliceBodyObservation>(), 120);
assert_eq!(std::mem::offset_of!(AliceBodyObservation, in_contact), 113);
}
#[test]
fn overlap_sphere_counts_past_a_small_buffer() {
unsafe {
let (world, _) = query_twins();
let c = v(0.0, -1.2, 10.0);
let none = ALICE_PHYSICS_NO_BODY;
let mut out = [AliceQueryTarget { kind: 7, index: 7 }; 2];
assert_eq!(
alice_physics_world_overlap_sphere(world, c, 1.0, none, out.as_mut_ptr(), 1),
2
);
assert_eq!(out[0], AliceQueryTarget { kind: 0, index: 0 });
assert_eq!(
out[1],
AliceQueryTarget { kind: 7, index: 7 },
"past capacity"
);
assert_eq!(
alice_physics_world_overlap_sphere(world, c, 1.0, none, std::ptr::null_mut(), 0),
2
);
alice_physics_world_destroy(world);
}
}
#[test]
fn observe_body_matches_the_rust_api() {
unsafe {
let (world, mut rust) = query_twins();
assert_eq!(
alice_physics_body_set_velocity(world, 1, v(1.0, 2.0, 3.0)),
1
);
rust.bodies[1].velocity = vf(1.0, 2.0, 3.0);
let mut o = std::mem::zeroed::<AliceBodyObservation>();
assert_eq!(alice_physics_body_observe(world, 1, &mut o), 1);
assert_eq!(o.body_index, 1);
assert_eq!(o.position, v(0.0, 0.0, 20.0));
assert_eq!(o.velocity, v(1.0, 2.0, 3.0));
assert_eq!(
(o.rotation.x, o.rotation.y, o.rotation.z, o.rotation.w),
(0.0, 0.0, 0.0, 1.0)
);
assert_eq!(o.angular_velocity, v(0.0, 0.0, 0.0));
assert_eq!((o.sleeping, o.in_contact), (0, 0));
assert_eq!(alice_physics_world_step(world, 1.0 / 60.0), 1);
rust.step(fx(1.0 / 60.0));
for i in 0..2u32 {
assert_eq!(alice_physics_body_observe(world, i, &mut o), 1);
let r = rust.observe_body(i as usize).unwrap();
assert_eq!(o.body_index, i);
assert_eq!(o.position, AliceVec3::from_vec3fix(r.position));
assert_eq!(o.velocity, AliceVec3::from_vec3fix(r.velocity));
assert_eq!(o.rotation, AliceQuat::from_quatfix(r.rotation));
assert_eq!(
o.angular_velocity,
AliceVec3::from_vec3fix(r.angular_velocity)
);
assert_eq!(
(o.sleeping != 0, o.in_contact != 0),
(r.sleeping, r.in_contact)
);
}
alice_physics_world_destroy(world);
}
}
#[test]
fn query_calls_refuse_degenerate_arguments() {
unsafe {
let (world, _) = query_twins();
alice_physics_clear_last_error();
let null: *const PhysicsWorld = std::ptr::null();
let none = ALICE_PHYSICS_NO_BODY;
let o = v(0.0, 0.0, 0.0);
let pz = v(0.0, 0.0, 1.0);
let zero = v(0.0, 0.0, 0.0);
let nan = f64::NAN;
let mut hit = no_hit();
let ray = |w, o, d, m, e, h: *mut AliceQueryHit| {
alice_physics_world_cast_ray(w, o, d, m, e, h)
};
assert_eq!(ray(world, o, zero, 100.0, none, &mut hit), 0, "zero dir");
assert_eq!(ray(world, o, pz, 0.0, none, &mut hit), 0, "max_t 0");
assert_eq!(ray(world, o, pz, -1.0, none, &mut hit), 0, "max_t < 0");
assert_eq!(ray(world, o, pz, nan, none, &mut hit), 0, "max_t nan");
assert_eq!(ray(world, v(nan, 0.0, 0.0), pz, 100.0, none, &mut hit), 0);
assert_eq!(ray(world, o, pz, 100.0, 2, &mut hit), 0, "exclude 2");
assert_eq!(ray(null, o, pz, 100.0, none, &mut hit), 0, "null world");
assert_eq!(ray(world, o, pz, 100.0, none, std::ptr::null_mut()), 0);
assert_eq!(
alice_physics_world_cast_sphere(world, o, -0.5, pz, 100.0, none, &mut hit),
0,
"negative radius"
);
assert_eq!(
alice_physics_world_cast_sphere(world, o, nan, pz, 100.0, none, &mut hit),
0
);
assert_eq!(
alice_physics_world_cast_sphere(world, o, 0.5, zero, 100.0, none, &mut hit),
0
);
assert_eq!(
alice_physics_world_cast_sphere(null, o, 0.5, pz, 100.0, none, &mut hit),
0
);
let (a, b) = (v(-1.0, 0.0, 0.0), v(1.0, 0.0, 0.0));
assert_eq!(
alice_physics_world_cast_capsule(world, a, b, -0.5, pz, 100.0, none, &mut hit),
0,
"negative radius"
);
assert_eq!(
alice_physics_world_cast_capsule(world, a, b, 0.5, zero, 100.0, none, &mut hit),
0
);
assert_eq!(
alice_physics_world_cast_capsule(world, a, b, 0.5, pz, 100.0, 9, &mut hit),
0
);
assert_eq!(
alice_physics_world_cast_capsule(null, a, b, 0.5, pz, 100.0, none, &mut hit),
0
);
assert_eq!(hit.target_kind, 7, "output untouched");
let mut out = [AliceQueryTarget { kind: 7, index: 7 }; 2];
let c = v(0.0, -1.2, 10.0);
let ov = |w, c, r, e, p: *mut AliceQueryTarget, n| {
alice_physics_world_overlap_sphere(w, c, r, e, p, n)
};
assert_eq!(ov(world, c, -1.0, none, out.as_mut_ptr(), 2), 0, "r < 0");
assert_eq!(ov(world, c, nan, none, out.as_mut_ptr(), 2), u32::MAX);
assert_eq!(ov(world, c, 1.0, 5, out.as_mut_ptr(), 2), u32::MAX);
assert_eq!(ov(null, c, 1.0, none, out.as_mut_ptr(), 2), u32::MAX);
assert_eq!(ov(world, c, 1.0, none, std::ptr::null_mut(), 2), u32::MAX);
assert_eq!(out[0].kind, 7, "output untouched");
let mut obs = std::mem::zeroed::<AliceBodyObservation>();
obs.body_index = 7;
assert_eq!(alice_physics_body_observe(world, 2, &mut obs), 0);
assert_eq!(alice_physics_body_observe(null, 0, &mut obs), 0);
assert_eq!(
alice_physics_body_observe(world, 0, std::ptr::null_mut()),
0
);
assert_eq!(obs.body_index, 7, "output untouched");
let empty = alice_physics_world_create();
assert_eq!(ray(empty, o, pz, 100.0, none, &mut hit), 0);
assert_eq!(ov(empty, o, 1.0, none, out.as_mut_ptr(), 2), 0);
assert_eq!(alice_physics_body_observe(empty, 0, &mut obs), 0);
assert_eq!(ray(empty, o, pz, 100.0, 0, &mut hit), 0, "exclude in empty");
alice_physics_world_destroy(empty);
assert!(
alice_physics_last_error().is_null(),
"a refusal must not come from a caught panic"
);
alice_physics_world_destroy(world);
}
}
}