use crate::{
calculate_zero_angle_with_conditions, run_monte_carlo_with_direction_std_dev,
AtmosphericConditions, BallisticInputs, DragModel, MonteCarloParams, TrajectorySolver,
WindConditions,
};
use std::os::raw::{c_char, c_double, c_int};
use std::ptr;
pub const MIN_FFI_STEP_SIZE_MS: c_double = 0.1;
#[repr(C)]
pub struct FFIBallisticInputs {
pub muzzle_velocity: c_double, pub muzzle_angle: c_double, pub bc_value: c_double, pub bullet_mass: c_double, pub bullet_diameter: c_double, pub bc_type: c_int, pub sight_height: c_double, pub target_distance: c_double, pub temperature: c_double, pub twist_rate: c_double, pub is_twist_right: c_int, pub shooting_angle: c_double, pub altitude: c_double, pub latitude: c_double, pub azimuth_angle: c_double, pub use_rk4: c_int, pub use_adaptive_rk45: c_int, pub enable_wind_shear: c_int, pub enable_trajectory_sampling: c_int, pub sample_interval: c_double, pub enable_pitch_damping: c_int, pub enable_precession_nutation: c_int, pub enable_spin_drift: c_int, pub enable_magnus: c_int, pub enable_coriolis: c_int, pub shot_azimuth: c_double,
pub cant_angle: c_double,
}
#[repr(C)]
pub struct FFIWindConditions {
pub speed: c_double, pub direction: c_double,
pub vertical_speed: c_double,
}
#[repr(C)]
pub struct FFIAtmosphericConditions {
pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
#[repr(C)]
pub struct FFITrajectorySample {
pub distance: c_double, pub time: c_double, pub velocity_mps: c_double, pub energy_joules: c_double, pub drop_meters: c_double, pub windage_meters: c_double, pub mach: c_double, pub spin_rate_rps: c_double, }
#[repr(C)]
pub struct FFITrajectoryPoint {
pub time: c_double,
pub position_x: c_double,
pub position_y: c_double,
pub position_z: c_double,
pub velocity_magnitude: c_double,
pub kinetic_energy: c_double,
}
#[repr(C)]
pub struct FFITrajectoryResult {
pub max_range: c_double,
pub max_height: c_double,
pub time_of_flight: c_double,
pub impact_velocity: c_double,
pub impact_energy: c_double,
pub points: *mut FFITrajectoryPoint,
pub point_count: c_int,
pub sampled_points: *mut FFITrajectorySample,
pub sampled_point_count: c_int,
pub min_pitch_damping: c_double, pub transonic_mach: c_double, pub final_pitch_angle: c_double, pub final_yaw_angle: c_double, pub max_yaw_angle: c_double, pub max_precession_angle: c_double, }
#[repr(C)]
pub struct FFIMonteCarloParams {
pub num_simulations: c_int,
pub velocity_std_dev: c_double,
pub angle_std_dev: c_double,
pub bc_std_dev: c_double,
pub wind_speed_std_dev: c_double,
pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
#[repr(C)]
pub struct FFIMonteCarloResults {
pub ranges: *mut c_double,
pub impact_velocities: *mut c_double,
pub impact_positions_x: *mut c_double,
pub impact_positions_y: *mut c_double,
pub impact_positions_z: *mut c_double,
pub num_results: c_int,
pub mean_range: c_double,
pub std_dev_range: c_double,
pub mean_impact_velocity: c_double,
pub std_dev_impact_velocity: c_double,
pub hit_probability: c_double, }
fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
let mut ballistic_inputs = BallisticInputs::default();
ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
ballistic_inputs.shot_azimuth = inputs.shot_azimuth;
ballistic_inputs.cant_angle = inputs.cant_angle;
ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
ballistic_inputs.bc_value = inputs.bc_value;
ballistic_inputs.bullet_mass = inputs.bullet_mass;
ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
ballistic_inputs.bc_type = match inputs.bc_type {
1 => DragModel::G7,
2 => DragModel::G2,
3 => DragModel::G5,
4 => DragModel::G6,
5 => DragModel::G8,
6 => DragModel::GI,
7 => DragModel::GS,
_ => DragModel::G1,
};
ballistic_inputs.sight_height = inputs.sight_height;
ballistic_inputs.target_distance = inputs.target_distance;
ballistic_inputs.temperature = inputs.temperature;
ballistic_inputs.twist_rate = inputs.twist_rate;
ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
ballistic_inputs.shooting_angle = inputs.shooting_angle;
ballistic_inputs.altitude = inputs.altitude;
if !inputs.latitude.is_nan() {
ballistic_inputs.latitude = Some(inputs.latitude);
}
ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
ballistic_inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
ballistic_inputs.bullet_length = {
let est = crate::stability::estimate_bullet_length_m(
ballistic_inputs.bullet_diameter,
ballistic_inputs.bullet_mass,
);
if est > 0.0 {
est
} else {
ballistic_inputs.bullet_diameter * 4.5
}
};
ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
ballistic_inputs.sample_interval = inputs.sample_interval;
ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
ballistic_inputs.enable_advanced_effects =
inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
ballistic_inputs
}
unsafe fn drag_table_from_raw(
mach: *const c_double,
cd: *const c_double,
len: c_int,
) -> Result<crate::drag::DragTable, ()> {
if mach.is_null() || cd.is_null() || len < 2 {
return Err(());
}
let len = len as usize;
let mach = unsafe { std::slice::from_raw_parts(mach, len) }.to_vec();
let cd = unsafe { std::slice::from_raw_parts(cd, len) }.to_vec();
crate::drag::DragTable::try_new(mach, cd).map_err(|_| ())
}
unsafe fn calculate_trajectory_impl(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
max_range: c_double,
step_size: c_double,
custom_drag_table: Option<crate::drag::DragTable>,
) -> *mut FFITrajectoryResult {
if inputs.is_null() {
return ptr::null_mut();
}
if !step_size.is_finite() || step_size < MIN_FFI_STEP_SIZE_MS {
return ptr::null_mut();
}
let inputs = unsafe { &*inputs };
let mut ballistic_inputs = convert_inputs(inputs);
ballistic_inputs.custom_drag_table = custom_drag_table;
let twist_rate_in = ballistic_inputs.twist_rate;
let wind_conditions = if wind.is_null() {
WindConditions::default()
} else {
let wind = unsafe { &*wind };
WindConditions {
speed: wind.speed,
direction: wind.direction,
vertical_speed: wind.vertical_speed,
}
};
let atmospheric_conditions = if atmosphere.is_null() {
AtmosphericConditions::default()
} else {
let atmo = unsafe { &*atmosphere };
AtmosphericConditions {
temperature: atmo.temperature,
pressure: atmo.pressure,
humidity: atmo.humidity,
altitude: atmo.altitude,
}
};
let (sample_temp_c, sample_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
atmospheric_conditions.temperature,
atmospheric_conditions.pressure,
atmospheric_conditions.altitude,
);
let (_, sample_speed_of_sound) = crate::atmosphere::calculate_atmosphere(
atmospheric_conditions.altitude,
Some(sample_temp_c),
Some(sample_pressure_hpa),
atmospheric_conditions.humidity,
);
let mut solver =
TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
solver.set_max_range(max_range);
solver.set_time_step(step_size / 1000.0);
match solver.solve() {
Ok(result) => {
let point_count = result.points.len();
let points = if point_count > 0 {
let mut ffi_points = Vec::with_capacity(point_count);
for point in result.points.iter() {
ffi_points.push(FFITrajectoryPoint {
time: point.time,
position_x: point.position[0],
position_y: point.position[1],
position_z: point.position[2],
velocity_magnitude: point.velocity_magnitude,
kinetic_energy: point.kinetic_energy,
});
}
let points_ptr = ffi_points.as_mut_ptr();
std::mem::forget(ffi_points); points_ptr
} else {
ptr::null_mut()
};
let (sampled_points, sampled_point_count) =
if let Some(ref samples) = result.sampled_points {
let mut ffi_samples = Vec::with_capacity(samples.len());
for sample in samples {
ffi_samples.push(FFITrajectorySample {
distance: sample.distance_m,
time: sample.time_s,
velocity_mps: sample.velocity_mps,
energy_joules: sample.energy_j,
drop_meters: sample.drop_m,
windage_meters: sample.wind_drift_m,
mach: if sample_speed_of_sound > 0.0 {
sample.velocity_mps / sample_speed_of_sound
} else {
0.0
},
spin_rate_rps: if twist_rate_in > 0.0 {
sample.velocity_mps / (twist_rate_in * 0.0254)
} else {
0.0
},
});
}
let count = ffi_samples.len() as c_int;
let samples_ptr = ffi_samples.as_mut_ptr();
std::mem::forget(ffi_samples);
(samples_ptr, count)
} else {
(ptr::null_mut(), 0)
};
let (final_pitch, final_yaw, max_yaw, max_prec) =
if let Some(ref angular) = result.angular_state {
(
angular.pitch_angle,
angular.yaw_angle,
result.max_yaw_angle.unwrap_or(std::f64::NAN),
result.max_precession_angle.unwrap_or(std::f64::NAN),
)
} else {
(std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
};
let ffi_result = Box::new(FFITrajectoryResult {
max_range: result.max_range,
max_height: result.max_height,
time_of_flight: result.time_of_flight,
impact_velocity: result.impact_velocity,
impact_energy: result.impact_energy,
points,
point_count: point_count as c_int,
sampled_points,
sampled_point_count,
min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
final_pitch_angle: final_pitch,
final_yaw_angle: final_yaw,
max_yaw_angle: max_yaw,
max_precession_angle: max_prec,
});
Box::into_raw(ffi_result)
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_calculate_trajectory(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
max_range: c_double,
step_size: c_double,
) -> *mut FFITrajectoryResult {
unsafe { calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, None) }
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
max_range: c_double,
step_size: c_double,
drag_mach: *const c_double,
drag_cd: *const c_double,
drag_table_len: c_int,
) -> *mut FFITrajectoryResult {
let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
Ok(t) => t,
Err(()) => return ptr::null_mut(),
};
unsafe {
calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, Some(table))
}
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
if !result.is_null() {
unsafe {
let result = Box::from_raw(result);
if !result.points.is_null() && result.point_count > 0 {
let points = Vec::from_raw_parts(
result.points,
result.point_count as usize,
result.point_count as usize,
);
drop(points);
}
if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
let samples = Vec::from_raw_parts(
result.sampled_points,
result.sampled_point_count as usize,
result.sampled_point_count as usize,
);
drop(samples);
}
drop(result);
}
}
}
unsafe fn calculate_zero_angle_impl(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
zero_distance: c_double,
custom_drag_table: Option<crate::drag::DragTable>,
) -> c_double {
if inputs.is_null() {
return f64::NAN;
}
let inputs = unsafe { &*inputs };
let mut ballistic_inputs = convert_inputs(inputs);
ballistic_inputs.custom_drag_table = custom_drag_table;
let wind_conditions = if wind.is_null() {
WindConditions::default()
} else {
let wind = unsafe { &*wind };
WindConditions {
speed: wind.speed,
direction: wind.direction,
vertical_speed: wind.vertical_speed,
}
};
let atmospheric_conditions = if atmosphere.is_null() {
AtmosphericConditions::default()
} else {
let atmo = unsafe { &*atmosphere };
AtmosphericConditions {
temperature: atmo.temperature,
pressure: atmo.pressure,
humidity: atmo.humidity,
altitude: atmo.altitude,
}
};
let target_height = ballistic_inputs.sight_height;
match calculate_zero_angle_with_conditions(
ballistic_inputs,
zero_distance,
target_height,
wind_conditions,
atmospheric_conditions,
) {
Ok(angle) => angle,
Err(_) => f64::NAN,
}
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_calculate_zero_angle(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
zero_distance: c_double,
) -> c_double {
unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, None) }
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table(
inputs: *const FFIBallisticInputs,
wind: *const FFIWindConditions,
atmosphere: *const FFIAtmosphericConditions,
zero_distance: c_double,
drag_mach: *const c_double,
drag_cd: *const c_double,
drag_table_len: c_int,
) -> c_double {
let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
Ok(t) => t,
Err(()) => return f64::NAN,
};
unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, Some(table)) }
}
#[no_mangle]
pub extern "C" fn ballistics_quick_trajectory(
muzzle_velocity: c_double,
bc: c_double,
sight_height: c_double,
zero_distance: c_double,
target_distance: c_double,
) -> c_double {
let mut inputs = BallisticInputs::default();
inputs.muzzle_velocity = muzzle_velocity;
inputs.bc_value = bc;
inputs.sight_height = sight_height;
inputs.target_distance = target_distance;
let wind = WindConditions::default();
let atmo = AtmosphericConditions::default();
let zero_angle = match calculate_zero_angle_with_conditions(
inputs.clone(),
zero_distance,
sight_height,
wind.clone(),
atmo.clone(),
) {
Ok(angle) => angle,
Err(_) => return f64::NAN,
};
inputs.muzzle_angle = zero_angle;
let mut solver = TrajectorySolver::new(inputs, wind, atmo);
solver.set_max_range(target_distance * 1.1);
match solver.solve() {
Ok(result) => {
for point in result.points {
if point.position[0] >= target_distance {
return sight_height - point.position[1];
}
}
f64::NAN
}
Err(_) => f64::NAN,
}
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_monte_carlo(
inputs: *const FFIBallisticInputs,
atmosphere: *const FFIAtmosphericConditions,
params: *const FFIMonteCarloParams,
) -> *mut FFIMonteCarloResults {
unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, 0.0) }
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_monte_carlo_with_direction_std_dev(
inputs: *const FFIBallisticInputs,
atmosphere: *const FFIAtmosphericConditions,
params: *const FFIMonteCarloParams,
wind_direction_std_dev: c_double,
) -> *mut FFIMonteCarloResults {
unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, wind_direction_std_dev) }
}
unsafe fn ballistics_monte_carlo_impl(
inputs: *const FFIBallisticInputs,
atmosphere: *const FFIAtmosphericConditions,
params: *const FFIMonteCarloParams,
wind_direction_std_dev: f64,
) -> *mut FFIMonteCarloResults {
if inputs.is_null() || params.is_null() {
return ptr::null_mut();
}
let inputs = unsafe { &*inputs };
let params = unsafe { &*params };
const MAX_SIMULATIONS: c_int = 1_000_000;
if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
return ptr::null_mut();
}
let mut ballistic_inputs = convert_inputs(inputs);
ballistic_inputs.muzzle_height = 1.5;
ballistic_inputs.ground_threshold = 0.0;
if !atmosphere.is_null() {
let atmo = unsafe { &*atmosphere };
ballistic_inputs.temperature = atmo.temperature;
ballistic_inputs.pressure = atmo.pressure;
ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
ballistic_inputs.altitude = atmo.altitude;
}
let mc_params = MonteCarloParams {
num_simulations: params.num_simulations as usize,
velocity_std_dev: params.velocity_std_dev,
angle_std_dev: params.angle_std_dev,
bc_std_dev: params.bc_std_dev,
wind_speed_std_dev: params.wind_speed_std_dev,
target_distance: if params.target_distance.is_nan() {
None
} else {
Some(params.target_distance)
},
base_wind_speed: params.base_wind_speed,
base_wind_direction: params.base_wind_direction,
azimuth_std_dev: params.azimuth_std_dev,
};
match run_monte_carlo_with_direction_std_dev(
ballistic_inputs,
mc_params,
wind_direction_std_dev,
) {
Ok(results) => {
let num_results = results.ranges.len() as c_int;
let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
let variance_range: f64 = results
.ranges
.iter()
.map(|r| (r - mean_range).powi(2))
.sum::<f64>()
/ num_results as f64;
let std_dev_range = variance_range.sqrt();
let mean_velocity: f64 =
results.impact_velocities.iter().sum::<f64>() / num_results as f64;
let variance_velocity: f64 = results
.impact_velocities
.iter()
.map(|v| (v - mean_velocity).powi(2))
.sum::<f64>()
/ num_results as f64;
let std_dev_velocity = variance_velocity.sqrt();
let hit_probability = if params.target_distance.is_nan() {
0.0
} else {
results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
};
let ranges_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, &range) in results.ranges.iter().enumerate() {
*ptr.add(i) = range;
}
ptr
};
let velocities_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, &vel) in results.impact_velocities.iter().enumerate() {
*ptr.add(i) = vel;
}
ptr
};
let pos_x_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, pos) in results.impact_positions.iter().enumerate() {
*ptr.add(i) = pos.x;
}
ptr
};
let pos_y_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, pos) in results.impact_positions.iter().enumerate() {
*ptr.add(i) = pos.y;
}
ptr
};
let pos_z_ptr = unsafe {
let ptr = std::alloc::alloc(
std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
) as *mut c_double;
for (i, pos) in results.impact_positions.iter().enumerate() {
*ptr.add(i) = pos.z;
}
ptr
};
let result = Box::new(FFIMonteCarloResults {
ranges: ranges_ptr,
impact_velocities: velocities_ptr,
impact_positions_x: pos_x_ptr,
impact_positions_y: pos_y_ptr,
impact_positions_z: pos_z_ptr,
num_results,
mean_range,
std_dev_range,
mean_impact_velocity: mean_velocity,
std_dev_impact_velocity: std_dev_velocity,
hit_probability,
});
Box::into_raw(result)
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
if results.is_null() {
return;
}
unsafe {
let results = Box::from_raw(results);
let num = results.num_results as usize;
if !results.ranges.is_null() {
std::alloc::dealloc(
results.ranges as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_velocities.is_null() {
std::alloc::dealloc(
results.impact_velocities as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_positions_x.is_null() {
std::alloc::dealloc(
results.impact_positions_x as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_positions_y.is_null() {
std::alloc::dealloc(
results.impact_positions_y as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
if !results.impact_positions_z.is_null() {
std::alloc::dealloc(
results.impact_positions_z as *mut u8,
std::alloc::Layout::array::<c_double>(num).unwrap(),
);
}
}
}
#[no_mangle]
pub extern "C" fn ballistics_get_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_trajectory_inputs() -> FFIBallisticInputs {
FFIBallisticInputs {
muzzle_velocity: 800.0,
muzzle_angle: 0.0,
bc_value: 0.5,
bullet_mass: 0.01,
bullet_diameter: 0.00762,
bc_type: 0,
sight_height: 0.05,
target_distance: 1.0,
temperature: 15.0,
twist_rate: 12.0,
is_twist_right: 1,
shooting_angle: 0.0,
altitude: 0.0,
latitude: f64::NAN,
azimuth_angle: 0.0,
use_rk4: 1,
use_adaptive_rk45: 0,
enable_wind_shear: 0,
enable_trajectory_sampling: 0,
sample_interval: 10.0,
enable_pitch_damping: 0,
enable_precession_nutation: 0,
enable_spin_drift: 0,
enable_magnus: 0,
enable_coriolis: 0,
shot_azimuth: 0.0,
cant_angle: 0.0,
}
}
#[allow(dead_code)]
#[repr(C)]
struct LegacyFFIMonteCarloParams {
num_simulations: c_int,
velocity_std_dev: c_double,
angle_std_dev: c_double,
bc_std_dev: c_double,
wind_speed_std_dev: c_double,
target_distance: c_double,
base_wind_speed: c_double,
base_wind_direction: c_double,
azimuth_std_dev: c_double,
}
#[test]
fn monte_carlo_params_legacy_abi_size_is_unchanged() {
assert_eq!(
std::mem::size_of::<FFIMonteCarloParams>(),
std::mem::size_of::<LegacyFFIMonteCarloParams>()
);
assert_eq!(
std::mem::align_of::<FFIMonteCarloParams>(),
std::mem::align_of::<LegacyFFIMonteCarloParams>()
);
}
#[test]
fn null_pointer_contracts_return_sentinels_and_free_safely() {
unsafe {
assert!(ballistics_calculate_trajectory(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
1_000.0,
1.0,
)
.is_null());
assert!(ballistics_calculate_zero_angle(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
100.0,
)
.is_nan());
assert!(ballistics_calculate_trajectory_with_drag_table(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
1_000.0,
1.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
DECK_MACH.len() as c_int,
)
.is_null());
assert!(ballistics_calculate_zero_angle_with_drag_table(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
100.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
DECK_MACH.len() as c_int,
)
.is_nan());
assert!(
ballistics_monte_carlo(std::ptr::null(), std::ptr::null(), std::ptr::null(),)
.is_null()
);
assert!(ballistics_monte_carlo_with_direction_std_dev(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
0.1,
)
.is_null());
ballistics_free_trajectory_result(std::ptr::null_mut());
ballistics_free_monte_carlo_results(std::ptr::null_mut());
}
}
#[test]
fn mba1283_ffi_enforces_step_floor_for_every_solver_mode() {
for (mode, use_rk4, use_adaptive_rk45) in [("Euler", 0, 0), ("RK4", 1, 0), ("RK45", 1, 1)] {
for step_size in [
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
-1.0,
-0.0,
0.0,
0.001,
MIN_FFI_STEP_SIZE_MS - 0.001,
] {
let mut inputs = valid_trajectory_inputs();
inputs.use_rk4 = use_rk4;
inputs.use_adaptive_rk45 = use_adaptive_rk45;
let result = unsafe {
ballistics_calculate_trajectory(
&inputs,
std::ptr::null(),
std::ptr::null(),
0.01,
step_size,
)
};
assert!(
result.is_null(),
"{mode} step_size={step_size:?} bypassed the FFI floor"
);
}
let mut inputs = valid_trajectory_inputs();
inputs.use_rk4 = use_rk4;
inputs.use_adaptive_rk45 = use_adaptive_rk45;
let result = unsafe {
ballistics_calculate_trajectory(
&inputs,
std::ptr::null(),
std::ptr::null(),
0.01,
MIN_FFI_STEP_SIZE_MS,
)
};
assert!(
!result.is_null(),
"the documented minimum step must remain usable in {mode}"
);
unsafe {
assert!((*result).point_count >= 0);
assert!((*result).point_count as usize <= crate::MAX_TRAJECTORY_POINTS);
ballistics_free_trajectory_result(result);
}
}
}
const DECK_MACH: [f64; 4] = [0.5, 1.0, 2.0, 3.0];
const DECK_CD_LOW: [f64; 4] = [0.05, 0.08, 0.06, 0.05];
#[test]
fn trajectory_with_drag_table_applies_the_deck() {
let inputs = valid_trajectory_inputs();
unsafe {
let plain = ballistics_calculate_trajectory(
&inputs,
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
);
let decked = ballistics_calculate_trajectory_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
DECK_MACH.len() as c_int,
);
assert!(!plain.is_null() && !decked.is_null());
assert!(
(*decked).impact_velocity > (*plain).impact_velocity + 1.0,
"deck did not change the solve: plain={} decked={}",
(*plain).impact_velocity,
(*decked).impact_velocity
);
ballistics_free_trajectory_result(plain);
ballistics_free_trajectory_result(decked);
}
}
#[test]
fn trajectory_with_drag_table_rejects_invalid_decks() {
let inputs = valid_trajectory_inputs();
let descending = [3.0, 2.0, 1.0, 0.5];
let negative_cd = [0.05, -0.08, 0.06, 0.05];
unsafe {
assert!(ballistics_calculate_trajectory_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
std::ptr::null(),
DECK_CD_LOW.as_ptr(),
4,
)
.is_null());
assert!(ballistics_calculate_trajectory_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
DECK_MACH.as_ptr(),
std::ptr::null(),
4,
)
.is_null());
assert!(ballistics_calculate_trajectory_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
1,
)
.is_null());
assert!(ballistics_calculate_trajectory_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
descending.as_ptr(),
DECK_CD_LOW.as_ptr(),
4,
)
.is_null());
assert!(ballistics_calculate_trajectory_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
DECK_MACH.as_ptr(),
negative_cd.as_ptr(),
4,
)
.is_null());
assert!(ballistics_calculate_trajectory_with_drag_table(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
300.0,
1.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
4,
)
.is_null());
}
}
#[test]
fn zero_angle_with_drag_table_applies_the_deck() {
let inputs = valid_trajectory_inputs();
unsafe {
let plain =
ballistics_calculate_zero_angle(&inputs, std::ptr::null(), std::ptr::null(), 100.0);
let decked = ballistics_calculate_zero_angle_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
100.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
DECK_MACH.len() as c_int,
);
assert!(plain.is_finite() && decked.is_finite());
assert!(
(plain - decked).abs() > 1e-6,
"deck did not change the zero: plain={plain} decked={decked}"
);
}
}
#[test]
fn zero_angle_with_drag_table_rejects_invalid_decks() {
let inputs = valid_trajectory_inputs();
let descending = [3.0, 2.0, 1.0, 0.5];
unsafe {
assert!(ballistics_calculate_zero_angle_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
100.0,
std::ptr::null(),
DECK_CD_LOW.as_ptr(),
4,
)
.is_nan());
assert!(ballistics_calculate_zero_angle_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
100.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
0,
)
.is_nan());
assert!(ballistics_calculate_zero_angle_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
100.0,
descending.as_ptr(),
DECK_CD_LOW.as_ptr(),
4,
)
.is_nan());
assert!(ballistics_calculate_zero_angle_with_drag_table(
std::ptr::null(),
std::ptr::null(),
std::ptr::null(),
100.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
4,
)
.is_nan());
}
}
#[test]
fn zero_then_fly_with_same_deck_is_consistent() {
let mut inputs = valid_trajectory_inputs();
unsafe {
let angle = ballistics_calculate_zero_angle_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
100.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
DECK_MACH.len() as c_int,
);
assert!(angle.is_finite());
inputs.muzzle_angle = angle;
let result = ballistics_calculate_trajectory_with_drag_table(
&inputs,
std::ptr::null(),
std::ptr::null(),
150.0,
1.0,
DECK_MACH.as_ptr(),
DECK_CD_LOW.as_ptr(),
DECK_MACH.len() as c_int,
);
assert!(!result.is_null());
let zero_distance = 100.0;
let pts = std::slice::from_raw_parts((*result).points, (*result).point_count as usize);
let bracket = pts
.windows(2)
.find(|w| w[0].position_x <= zero_distance && w[1].position_x >= zero_distance)
.expect("trajectory brackets the zero distance");
let (lo, hi) = (&bracket[0], &bracket[1]);
let y_at_zero = if hi.position_x > lo.position_x {
let t = (zero_distance - lo.position_x) / (hi.position_x - lo.position_x);
lo.position_y + t * (hi.position_y - lo.position_y)
} else {
lo.position_y
};
assert!(
(y_at_zero - inputs.sight_height).abs() < 0.002,
"zeroed flight missed the line of sight at 100 m: y={} (sight_height={})",
y_at_zero,
inputs.sight_height
);
ballistics_free_trajectory_result(result);
}
}
#[test]
fn ffi_cant_angle_deflects_laterally() {
let mut level = valid_trajectory_inputs();
level.muzzle_angle = 0.003;
let mut canted = valid_trajectory_inputs();
canted.muzzle_angle = 0.003;
canted.cant_angle = 10f64.to_radians();
unsafe {
let a = ballistics_calculate_trajectory(&level, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
let b = ballistics_calculate_trajectory(&canted, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
assert!(!a.is_null() && !b.is_null());
let za = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_z;
let zb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_z;
assert!(zb > za + 0.005, "FFI cant must deflect right: level={za} canted={zb}");
ballistics_free_trajectory_result(a);
ballistics_free_trajectory_result(b);
}
}
#[test]
fn ffi_vertical_wind_raises_trajectory() {
let inputs = valid_trajectory_inputs();
let no_wind = FFIWindConditions {
speed: 0.0,
direction: 0.0,
vertical_speed: 0.0,
};
let updraft = FFIWindConditions {
speed: 0.0,
direction: 0.0,
vertical_speed: 5.0,
};
unsafe {
let a = ballistics_calculate_trajectory(&inputs, &no_wind, std::ptr::null(), 400.0, 1.0);
let b = ballistics_calculate_trajectory(&inputs, &updraft, std::ptr::null(), 400.0, 1.0);
assert!(!a.is_null() && !b.is_null());
let ya = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_y;
let yb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_y;
assert!(yb > ya + 0.01, "FFI updraft must raise the trajectory: no_wind={ya} updraft={yb}");
ballistics_free_trajectory_result(a);
ballistics_free_trajectory_result(b);
}
}
}