mod control;
mod id;
mod motorcycle;
mod readout;
mod settings;
mod tracked;
use std::marker::PhantomData;
use std::ops::Range;
use oxijolt_sys::*;
use crate::body::with_locked_body;
use crate::constraint::constraint_base;
use crate::math::is_unit;
use crate::owned::{JoltObject, Owned};
use crate::{AllowedDofs, BodyError, BodyId, MotionType, PhysicsWorld, Vec3, VehicleError};
pub use control::{DriverInput, VehicleMut};
pub use id::{
AnyVehicleId, Motorcycle, TrackedVehicle, VehicleId, VehicleKind, VehicleType, WheeledVehicle,
};
pub use motorcycle::MotorcycleLean;
pub use readout::{VehicleRef, WheelContact, WheelState};
use settings::{BuiltSettings, WheelGeometry};
pub use settings::{
MotorcycleSettings, TrackedVehicleSettings, TrackedWheelSettings, VehicleAntiRollBar,
VehicleCollisionTester, VehicleDifferentialSettings, VehicleEngineSettings,
VehicleTrackSettings, VehicleTransmissionSettings, WheelSettings, WheeledVehicleSettings,
};
pub use tracked::{TrackSide, TrackState, TrackedDriverInput};
impl JoltObject for JPH_VehicleConstraint {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_Constraint_Destroy(JPH_VehicleConstraint_AsConstraint(ptr)) };
}
}
pub(crate) struct VehicleEntry {
constraint: Owned<JPH_VehicleConstraint>,
pub(crate) body: BodyId,
pub(crate) collision_tester: VehicleCollisionTester,
wheels: Vec<WheelGeometry>,
kind: VehicleType,
tracks: Option<[Range<u32>; 2]>,
}
fn gravity_override(entry: &VehicleEntry) -> Option<Vec3> {
let constraint = entry.constraint.as_ptr();
unsafe {
JPH_VehicleConstraint_IsGravityOverridden(constraint).then(|| {
let mut value = Vec3::ZERO.to_jph();
JPH_VehicleConstraint_GetGravityOverride(constraint, &mut value);
Vec3::from_jph(value)
})
}
}
fn engine_and_transmission(
entry: &VehicleEntry,
) -> (*const JPH_VehicleEngine, *const JPH_VehicleTransmission) {
let controller = unsafe { JPH_VehicleConstraint_GetController(entry.constraint.as_ptr()) };
match entry.kind {
VehicleType::Wheeled | VehicleType::Motorcycle => unsafe {
let controller: *const JPH_WheeledVehicleController = controller.cast();
(
JPH_WheeledVehicleController_GetEngine(controller),
JPH_WheeledVehicleController_GetTransmission(controller),
)
},
VehicleType::Tracked => unsafe {
let controller: *const JPH_TrackedVehicleController = controller.cast();
(
JPH_TrackedVehicleController_GetEngine(controller),
JPH_TrackedVehicleController_GetTransmission(controller),
)
},
}
}
fn install_tester(entry: &mut VehicleEntry, tester: VehicleCollisionTester) {
let jolt_tester = tester.create(entry.body.to_raw());
unsafe {
JPH_VehicleConstraint_SetVehicleCollisionTester(
entry.constraint.as_ptr(),
jolt_tester.as_ptr(),
)
};
entry.collision_tester = tester;
}
impl PhysicsWorld {
pub fn create_wheeled_vehicle(
&mut self,
body: BodyId,
settings: &WheeledVehicleSettings,
) -> Result<VehicleId, VehicleError> {
settings.validate(self.object_layer_count)?;
self.check_chassis(body)?;
let id = self.attach_vehicle(body, settings.build())?;
Ok(VehicleId::new(id.raw, self.tag))
}
fn check_chassis(&self, body: BodyId) -> Result<(), VehicleError> {
self.check(body).map_err(VehicleError::Body)?;
if self.is_inner_body(body) {
return Err(VehicleError::Body(BodyError::OwnedByCharacter(body)));
}
if self.is_ragdoll_body(body) {
return Err(VehicleError::Body(BodyError::OwnedByRagdoll(body)));
}
if self.body(body).map_err(VehicleError::Body)?.is_soft_body() {
return Err(VehicleError::Body(BodyError::NotRigidBody(body)));
}
if self.body(body).map_err(VehicleError::Body)?.motion_type() != MotionType::Dynamic {
return Err(VehicleError::NotDynamic(body));
}
if self.body(body).map_err(VehicleError::Body)?.allowed_dofs() != AllowedDofs::ALL {
return Err(VehicleError::Body(BodyError::RestrictedDofs(body)));
}
if self.vehicle_bodies.contains_key(&body.to_raw()) {
return Err(VehicleError::AlreadyHasVehicle(body));
}
Ok(())
}
fn attach_vehicle(
&mut self,
body: BodyId,
built: BuiltSettings,
) -> Result<AnyVehicleId, VehicleError> {
let raw = self.next_vehicle_id;
if raw == u32::MAX {
return Err(VehicleError::TooManyVehicles);
}
let mut wheel_pointers: Vec<*mut JPH_WheelSettings> =
built.wheels.iter().map(|wheel| wheel.as_base()).collect();
let kind = built.controller.kind();
let jolt_settings = JPH_VehicleConstraintSettings {
base: constraint_base(),
up: built.frame.up.to_jph(),
forward: built.frame.forward.to_jph(),
maxPitchRollAngle: built.frame.max_pitch_roll_angle,
wheelsCount: wheel_pointers.len() as u32,
wheels: wheel_pointers.as_mut_ptr(),
antiRollBarsCount: built.anti_roll_bars.len() as u32,
antiRollBars: built.anti_roll_bars.as_ptr(),
controller: built.controller.as_base(),
};
let constraint = with_locked_body(self.body_lock_interface, body, |chassis| {
unsafe {
Owned::from_raw(JPH_VehicleConstraint_Create(
chassis.as_ptr(),
&jolt_settings,
))
}
})
.unwrap_or_else(|| unreachable!("`check_chassis` found the body and `&mut self` keeps it"))
.unwrap_or_else(|| unreachable!("joltc `new`s the constraint"));
let mut entry = VehicleEntry {
constraint,
body,
collision_tester: built.collision_tester,
wheels: built.geometry,
kind,
tracks: built.tracks,
};
install_tester(&mut entry, built.collision_tester);
self.note_structure_change();
unsafe {
let constraint = entry.constraint.as_ptr();
JPH_PhysicsSystem_AddConstraint(
self.system.as_ptr(),
JPH_VehicleConstraint_AsConstraint(constraint),
);
JPH_PhysicsSystem_AddStepListener(
self.system.as_ptr(),
JPH_VehicleConstraint_AsPhysicsStepListener(constraint),
);
}
self.vehicles.insert(raw, entry);
self.vehicle_bodies.insert(body.to_raw(), raw);
self.next_vehicle_id += 1;
Ok(AnyVehicleId {
raw,
world: self.tag,
kind,
})
}
fn vehicle_entry(&self, id: AnyVehicleId) -> Result<&VehicleEntry, VehicleError> {
if id.world != self.tag {
return Err(VehicleError::WrongWorld(id));
}
let entry = self
.vehicles
.get(&id.raw)
.ok_or(VehicleError::NotFound(id))?;
debug_assert_eq!(entry.kind, id.kind, "a vehicle id has its vehicle's kind");
Ok(entry)
}
pub fn remove_vehicle(&mut self, id: impl Into<AnyVehicleId>) -> Result<(), VehicleError> {
let id = id.into();
self.vehicle_entry(id)?;
self.note_structure_change();
let entry = self
.vehicles
.remove(&id.raw)
.unwrap_or_else(|| unreachable!("checked above"));
self.vehicle_bodies.remove(&entry.body.to_raw());
self.unregister_vehicle(entry);
Ok(())
}
fn unregister_vehicle(&mut self, entry: VehicleEntry) {
unsafe {
let constraint = entry.constraint.as_ptr();
JPH_PhysicsSystem_RemoveStepListener(
self.system.as_ptr(),
JPH_VehicleConstraint_AsPhysicsStepListener(constraint),
);
JPH_PhysicsSystem_RemoveConstraint(
self.system.as_ptr(),
JPH_VehicleConstraint_AsConstraint(constraint),
);
}
drop(entry);
}
pub(crate) fn remove_all_vehicles(&mut self) {
while let Some((_, entry)) = self.vehicles.pop_first() {
self.vehicle_bodies.remove(&entry.body.to_raw());
self.unregister_vehicle(entry);
}
}
pub fn vehicle<K: VehicleKind>(
&self,
id: VehicleId<K>,
) -> Result<VehicleRef<'_, K>, VehicleError> {
let entry = self.vehicle_entry(id.into())?;
Ok(VehicleRef {
world: self,
id,
entry,
})
}
pub fn vehicle_mut<K: VehicleKind>(
&mut self,
id: VehicleId<K>,
) -> Result<VehicleMut<'_, K>, VehicleError> {
self.vehicle_entry(id.into())?;
let object_layer_count = self.object_layer_count;
let entry = self
.vehicles
.get_mut(&id.raw)
.unwrap_or_else(|| unreachable!("checked above"));
Ok(VehicleMut {
entry,
object_layer_count,
kind: PhantomData,
})
}
pub fn vehicle_ids(&self) -> impl Iterator<Item = AnyVehicleId> + '_ {
self.vehicles.iter().map(|(&raw, entry)| AnyVehicleId {
raw,
world: self.tag,
kind: entry.kind,
})
}
pub fn vehicle_count(&self) -> u32 {
self.vehicles.len() as u32
}
pub fn contains_vehicle(&self, id: impl Into<AnyVehicleId>) -> bool {
self.vehicle_entry(id.into()).is_ok()
}
pub fn vehicle_of_body(&self, body: BodyId) -> Option<AnyVehicleId> {
if body.world != self.tag {
return None;
}
let raw = *self.vehicle_bodies.get(&body.to_raw())?;
let entry = self
.vehicles
.get(&raw)
.unwrap_or_else(|| unreachable!("every chassis names a vehicle"));
Some(AnyVehicleId {
raw,
world: self.tag,
kind: entry.kind,
})
}
pub(crate) fn is_vehicle_body(&self, body: BodyId) -> bool {
self.vehicle_of_body(body).is_some()
}
}
pub(crate) struct VehicleRebase {
raw: u32,
gravity: Option<Vec3>,
collision_tester: VehicleCollisionTester,
target_lean: Option<Vec3>,
}
impl PhysicsWorld {
pub(crate) fn rotated_vehicles(
&self,
rotate: impl Fn(Vec3) -> Vec3,
) -> Result<Vec<VehicleRebase>, &'static str> {
let mut rebased = Vec::with_capacity(self.vehicles.len());
for (&raw, entry) in &self.vehicles {
let gravity = gravity_override(entry).map(&rotate);
let collision_tester = match entry.collision_tester.up() {
Some(up) => {
let up = rotate(up).normalized_or_zero();
if !is_unit(up) {
return Err("rebase would give a vehicle tester an up that is not unit");
}
entry.collision_tester.with_up(up)
}
None => entry.collision_tester,
};
let target_lean = (entry.kind == VehicleType::Motorcycle).then(|| {
let controller =
unsafe { JPH_VehicleConstraint_GetController(entry.constraint.as_ptr()) };
rotate(motorcycle::target_lean(controller))
});
rebased.push(VehicleRebase {
raw,
gravity,
collision_tester,
target_lean,
});
}
Ok(rebased)
}
pub(crate) fn apply_vehicle_rebase(&mut self, rebased: Vec<VehicleRebase>) {
for vehicle in rebased {
let entry = self
.vehicles
.get_mut(&vehicle.raw)
.unwrap_or_else(|| unreachable!("computed from this world's vehicles"));
if let Some(gravity) = vehicle.gravity {
let gravity = gravity.to_jph();
unsafe {
JPH_VehicleConstraint_OverrideGravity(entry.constraint.as_ptr(), &gravity)
};
}
if vehicle.collision_tester != entry.collision_tester {
install_tester(entry, vehicle.collision_tester);
}
if let Some(target_lean) = vehicle.target_lean {
unsafe {
let controller = JPH_VehicleConstraint_GetController(entry.constraint.as_ptr());
motorcycle::set_target_lean(controller, target_lean);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::WorldSettings;
#[test]
fn listener_batching_matches_the_fleet_gate() {
let world = PhysicsWorld::new(WorldSettings::default()).unwrap();
let mut settings: JPH_PhysicsSettings = unsafe { std::mem::zeroed() };
unsafe { JPH_PhysicsSystem_GetPhysicsSettings(world.system.as_ptr(), &mut settings) };
assert_eq!(settings.stepListenersBatchSize, 8);
assert_eq!(settings.stepListenerBatchesPerJob, 1);
}
}