1use crate::{
4 calculate_zero_angle_with_conditions, run_monte_carlo, AtmosphericConditions, BallisticInputs,
5 DragModel, MonteCarloParams, TrajectorySolver, WindConditions,
6};
7use std::os::raw::{c_char, c_double, c_int};
8use std::ptr;
9
10#[repr(C)]
13pub struct FFIBallisticInputs {
14 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,
43}
44
45#[repr(C)]
46pub struct FFIWindConditions {
47 pub speed: c_double, pub direction: c_double,
51}
52
53#[repr(C)]
54pub struct FFIAtmosphericConditions {
55 pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
60
61#[repr(C)]
62pub struct FFITrajectorySample {
63 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, }
72
73#[repr(C)]
74pub struct FFITrajectoryPoint {
75 pub time: c_double,
76 pub position_x: c_double,
77 pub position_y: c_double,
78 pub position_z: c_double,
79 pub velocity_magnitude: c_double,
80 pub kinetic_energy: c_double,
81}
82
83#[repr(C)]
84pub struct FFITrajectoryResult {
85 pub max_range: c_double,
86 pub max_height: c_double,
87 pub time_of_flight: c_double,
88 pub impact_velocity: c_double,
89 pub impact_energy: c_double,
90 pub points: *mut FFITrajectoryPoint,
91 pub point_count: c_int,
92 pub sampled_points: *mut FFITrajectorySample,
93 pub sampled_point_count: c_int,
94 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, }
101
102#[repr(C)]
104pub struct FFIMonteCarloParams {
105 pub num_simulations: c_int,
106 pub velocity_std_dev: c_double,
107 pub angle_std_dev: c_double,
108 pub bc_std_dev: c_double,
109 pub wind_speed_std_dev: c_double,
110 pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
115
116#[repr(C)]
118pub struct FFIMonteCarloResults {
119 pub ranges: *mut c_double,
120 pub impact_velocities: *mut c_double,
121 pub impact_positions_x: *mut c_double,
122 pub impact_positions_y: *mut c_double,
123 pub impact_positions_z: *mut c_double,
124 pub num_results: c_int,
125 pub mean_range: c_double,
126 pub std_dev_range: c_double,
127 pub mean_impact_velocity: c_double,
128 pub std_dev_impact_velocity: c_double,
129 pub hit_probability: c_double, }
131
132fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
134 let mut ballistic_inputs = BallisticInputs::default();
135
136 ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
137 ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
138 ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
139 ballistic_inputs.shot_azimuth = inputs.shot_azimuth;
140 ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
141 ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
142 ballistic_inputs.bc_value = inputs.bc_value;
143 ballistic_inputs.bullet_mass = inputs.bullet_mass;
144 ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
145 ballistic_inputs.bc_type = match inputs.bc_type {
146 1 => DragModel::G7,
147 2 => DragModel::G2,
148 3 => DragModel::G5,
149 4 => DragModel::G6,
150 5 => DragModel::G8,
151 6 => DragModel::GI,
152 7 => DragModel::GS,
153 _ => DragModel::G1,
154 };
155 ballistic_inputs.sight_height = inputs.sight_height;
156 ballistic_inputs.target_distance = inputs.target_distance;
157 ballistic_inputs.temperature = inputs.temperature;
158 ballistic_inputs.twist_rate = inputs.twist_rate;
159 ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
160 ballistic_inputs.shooting_angle = inputs.shooting_angle;
161 ballistic_inputs.altitude = inputs.altitude;
162
163 if !inputs.latitude.is_nan() {
164 ballistic_inputs.latitude = Some(inputs.latitude);
165 }
166
167 ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
169 ballistic_inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
170 ballistic_inputs.bullet_length = {
173 let est = crate::stability::estimate_bullet_length_m(
174 ballistic_inputs.bullet_diameter,
175 ballistic_inputs.bullet_mass,
176 );
177 if est > 0.0 {
178 est
179 } else {
180 ballistic_inputs.bullet_diameter * 4.5
181 }
182 };
183
184 ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
186 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
187 ballistic_inputs.sample_interval = inputs.sample_interval;
188 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
189 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
190 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
191 ballistic_inputs.enable_advanced_effects =
192 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
193 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
195 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
196
197 ballistic_inputs
198}
199
200#[no_mangle]
202pub extern "C" fn ballistics_calculate_trajectory(
203 inputs: *const FFIBallisticInputs,
204 wind: *const FFIWindConditions,
205 atmosphere: *const FFIAtmosphericConditions,
206 max_range: c_double,
207 step_size: c_double,
208) -> *mut FFITrajectoryResult {
209 if inputs.is_null() {
210 return ptr::null_mut();
211 }
212
213 let inputs = unsafe { &*inputs };
214 let ballistic_inputs = convert_inputs(inputs);
215 let twist_rate_in = ballistic_inputs.twist_rate;
216
217 let wind_conditions = if wind.is_null() {
218 WindConditions::default()
219 } else {
220 let wind = unsafe { &*wind };
221 WindConditions {
222 speed: wind.speed,
223 direction: wind.direction,
224 }
225 };
226
227 let atmospheric_conditions = if atmosphere.is_null() {
228 AtmosphericConditions::default()
229 } else {
230 let atmo = unsafe { &*atmosphere };
231 AtmosphericConditions {
232 temperature: atmo.temperature,
233 pressure: atmo.pressure,
234 humidity: atmo.humidity,
235 altitude: atmo.altitude,
236 }
237 };
238
239 let (sample_temp_c, sample_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
241 atmospheric_conditions.temperature,
242 atmospheric_conditions.pressure,
243 atmospheric_conditions.altitude,
244 );
245 let (_, sample_speed_of_sound) = crate::atmosphere::calculate_atmosphere(
246 atmospheric_conditions.altitude,
247 Some(sample_temp_c),
248 Some(sample_pressure_hpa),
249 atmospheric_conditions.humidity,
250 );
251
252 let mut solver =
253 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
254
255 solver.set_max_range(max_range);
257 solver.set_time_step(step_size / 1000.0); match solver.solve() {
260 Ok(result) => {
261 let point_count = result.points.len();
263 let points = if point_count > 0 {
264 let mut ffi_points = Vec::with_capacity(point_count);
265 for (i, point) in result.points.iter().enumerate() {
266 ffi_points.push(FFITrajectoryPoint {
267 time: point.time,
268 position_x: point.position[0],
269 position_y: point.position[1],
270 position_z: point.position[2],
271 velocity_magnitude: point.velocity_magnitude,
272 kinetic_energy: point.kinetic_energy,
273 });
274
275 #[cfg(debug_assertions)]
279 if i == 0 || i == result.points.len() - 1 || i % 100 == 0 {
280 eprintln!(
281 "FFI point {}: lateral={:.2}m, vertical={:.2}m, downrange={:.2}m",
282 i, point.position[2], point.position[1], point.position[0]
283 );
284 }
285 }
286 let points_ptr = ffi_points.as_mut_ptr();
287 std::mem::forget(ffi_points); points_ptr
289 } else {
290 ptr::null_mut()
291 };
292
293 let (sampled_points, sampled_point_count) =
295 if let Some(ref samples) = result.sampled_points {
296 let mut ffi_samples = Vec::with_capacity(samples.len());
297 for sample in samples {
298 ffi_samples.push(FFITrajectorySample {
299 distance: sample.distance_m,
300 time: sample.time_s,
301 velocity_mps: sample.velocity_mps,
302 energy_joules: sample.energy_j,
303 drop_meters: sample.drop_m,
304 windage_meters: sample.wind_drift_m,
305 mach: if sample_speed_of_sound > 0.0 {
306 sample.velocity_mps / sample_speed_of_sound
307 } else {
308 0.0
309 },
310 spin_rate_rps: if twist_rate_in > 0.0 {
311 sample.velocity_mps / (twist_rate_in * 0.0254)
312 } else {
313 0.0
314 },
315 });
316 }
317 let count = ffi_samples.len() as c_int;
318 let samples_ptr = ffi_samples.as_mut_ptr();
319 std::mem::forget(ffi_samples);
320 (samples_ptr, count)
321 } else {
322 (ptr::null_mut(), 0)
323 };
324
325 let (final_pitch, final_yaw, max_yaw, max_prec) =
327 if let Some(ref angular) = result.angular_state {
328 (
329 angular.pitch_angle,
330 angular.yaw_angle,
331 result.max_yaw_angle.unwrap_or(std::f64::NAN),
332 result.max_precession_angle.unwrap_or(std::f64::NAN),
333 )
334 } else {
335 (std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
336 };
337
338 let ffi_result = Box::new(FFITrajectoryResult {
340 max_range: result.max_range,
341 max_height: result.max_height,
342 time_of_flight: result.time_of_flight,
343 impact_velocity: result.impact_velocity,
344 impact_energy: result.impact_energy,
345 points,
346 point_count: point_count as c_int,
347 sampled_points,
348 sampled_point_count,
349 min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
350 transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
351 final_pitch_angle: final_pitch,
352 final_yaw_angle: final_yaw,
353 max_yaw_angle: max_yaw,
354 max_precession_angle: max_prec,
355 });
356
357 Box::into_raw(ffi_result)
358 }
359 Err(_) => ptr::null_mut(),
360 }
361}
362
363#[no_mangle]
365pub extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
366 if !result.is_null() {
367 unsafe {
368 let result = Box::from_raw(result);
369 if !result.points.is_null() && result.point_count > 0 {
370 let points = Vec::from_raw_parts(
371 result.points,
372 result.point_count as usize,
373 result.point_count as usize,
374 );
375 drop(points);
376 }
377 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
378 let samples = Vec::from_raw_parts(
379 result.sampled_points,
380 result.sampled_point_count as usize,
381 result.sampled_point_count as usize,
382 );
383 drop(samples);
384 }
385 drop(result);
386 }
387 }
388}
389
390#[no_mangle]
392pub extern "C" fn ballistics_calculate_zero_angle(
393 inputs: *const FFIBallisticInputs,
394 wind: *const FFIWindConditions,
395 atmosphere: *const FFIAtmosphericConditions,
396 zero_distance: c_double,
397) -> c_double {
398 if inputs.is_null() {
399 return f64::NAN;
400 }
401
402 let inputs = unsafe { &*inputs };
403 let ballistic_inputs = convert_inputs(inputs);
404
405 let wind_conditions = if wind.is_null() {
406 WindConditions::default()
407 } else {
408 let wind = unsafe { &*wind };
409 WindConditions {
410 speed: wind.speed,
411 direction: wind.direction,
412 }
413 };
414
415 let atmospheric_conditions = if atmosphere.is_null() {
416 AtmosphericConditions::default()
417 } else {
418 let atmo = unsafe { &*atmosphere };
419 AtmosphericConditions {
420 temperature: atmo.temperature,
421 pressure: atmo.pressure,
422 humidity: atmo.humidity,
423 altitude: atmo.altitude,
424 }
425 };
426
427 let target_height = ballistic_inputs.sight_height;
430
431 #[cfg(debug_assertions)]
432 {
433 eprintln!("FFI: Calculating zero angle for:");
434 eprintln!(" Zero distance: {} m", zero_distance);
435 eprintln!(" Target height: {} m", target_height);
436 eprintln!(" Sight height: {} m", ballistic_inputs.sight_height);
437 eprintln!(" Wind speed: {} m/s", wind_conditions.speed);
438 eprintln!(" Temperature: {} C", atmospheric_conditions.temperature);
439 }
440
441 match calculate_zero_angle_with_conditions(
442 ballistic_inputs,
443 zero_distance,
444 target_height,
445 wind_conditions,
446 atmospheric_conditions,
447 ) {
448 Ok(angle) => {
449 #[cfg(debug_assertions)]
450 eprintln!(
451 " Calculated angle: {} rad ({} deg)",
452 angle,
453 angle * 180.0 / std::f64::consts::PI
454 );
455 angle
456 }
457 Err(e) => {
458 #[cfg(debug_assertions)]
459 eprintln!(" Error: {:?}", e);
460 f64::NAN
461 }
462 }
463}
464
465#[no_mangle]
467pub extern "C" fn ballistics_quick_trajectory(
468 muzzle_velocity: c_double,
469 bc: c_double,
470 sight_height: c_double,
471 zero_distance: c_double,
472 target_distance: c_double,
473) -> c_double {
474 let mut inputs = BallisticInputs::default();
478 inputs.muzzle_velocity = muzzle_velocity;
479 inputs.bc_value = bc;
480 inputs.sight_height = sight_height;
481 inputs.target_distance = target_distance;
482
483 let wind = WindConditions::default();
484 let atmo = AtmosphericConditions::default();
485
486 let zero_angle = match calculate_zero_angle_with_conditions(
488 inputs.clone(),
489 zero_distance,
490 sight_height,
491 wind.clone(),
492 atmo.clone(),
493 ) {
494 Ok(angle) => angle,
495 Err(_) => return f64::NAN,
496 };
497
498 inputs.muzzle_angle = zero_angle;
500
501 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
502 solver.set_max_range(target_distance * 1.1);
503
504 match solver.solve() {
505 Ok(result) => {
506 for point in result.points {
508 if point.position[0] >= target_distance {
509 return sight_height - point.position[1];
510 }
511 }
512 f64::NAN
513 }
514 Err(_) => f64::NAN,
515 }
516}
517
518#[no_mangle]
520pub extern "C" fn ballistics_monte_carlo(
521 inputs: *const FFIBallisticInputs,
522 atmosphere: *const FFIAtmosphericConditions,
523 params: *const FFIMonteCarloParams,
524) -> *mut FFIMonteCarloResults {
525 if inputs.is_null() || params.is_null() {
526 return ptr::null_mut();
527 }
528
529 let inputs = unsafe { &*inputs };
530 let params = unsafe { &*params };
531
532 const MAX_SIMULATIONS: c_int = 1_000_000;
538 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
539 return ptr::null_mut();
540 }
541
542 let mut ballistic_inputs = convert_inputs(inputs);
544 ballistic_inputs.muzzle_height = 1.5;
545 ballistic_inputs.ground_threshold = 0.0;
546 if !atmosphere.is_null() {
547 let atmo = unsafe { &*atmosphere };
548 ballistic_inputs.temperature = atmo.temperature;
549 ballistic_inputs.pressure = atmo.pressure;
550 ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
551 ballistic_inputs.altitude = atmo.altitude;
552 }
553
554 let mc_params = MonteCarloParams {
556 num_simulations: params.num_simulations as usize,
557 velocity_std_dev: params.velocity_std_dev,
558 angle_std_dev: params.angle_std_dev,
559 bc_std_dev: params.bc_std_dev,
560 wind_speed_std_dev: params.wind_speed_std_dev,
561 target_distance: if params.target_distance.is_nan() {
562 None
563 } else {
564 Some(params.target_distance)
565 },
566 base_wind_speed: params.base_wind_speed,
567 base_wind_direction: params.base_wind_direction,
568 azimuth_std_dev: params.azimuth_std_dev,
569 };
570
571 match run_monte_carlo(ballistic_inputs, mc_params) {
573 Ok(results) => {
574 let num_results = results.ranges.len() as c_int;
575
576 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
578 let variance_range: f64 = results
579 .ranges
580 .iter()
581 .map(|r| (r - mean_range).powi(2))
582 .sum::<f64>()
583 / num_results as f64;
584 let std_dev_range = variance_range.sqrt();
585
586 let mean_velocity: f64 =
587 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
588 let variance_velocity: f64 = results
589 .impact_velocities
590 .iter()
591 .map(|v| (v - mean_velocity).powi(2))
592 .sum::<f64>()
593 / num_results as f64;
594 let std_dev_velocity = variance_velocity.sqrt();
595
596 let hit_probability = if params.target_distance.is_nan() {
602 0.0
603 } else {
604 results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
605 };
606
607 let ranges_ptr = unsafe {
609 let ptr = std::alloc::alloc(
610 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
611 ) as *mut c_double;
612 for (i, &range) in results.ranges.iter().enumerate() {
613 *ptr.add(i) = range;
614 }
615 ptr
616 };
617
618 let velocities_ptr = unsafe {
619 let ptr = std::alloc::alloc(
620 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
621 ) as *mut c_double;
622 for (i, &vel) in results.impact_velocities.iter().enumerate() {
623 *ptr.add(i) = vel;
624 }
625 ptr
626 };
627
628 let pos_x_ptr = unsafe {
629 let ptr = std::alloc::alloc(
630 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
631 ) as *mut c_double;
632 for (i, pos) in results.impact_positions.iter().enumerate() {
633 *ptr.add(i) = pos.x;
634 }
635 ptr
636 };
637
638 let pos_y_ptr = unsafe {
639 let ptr = std::alloc::alloc(
640 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
641 ) as *mut c_double;
642 for (i, pos) in results.impact_positions.iter().enumerate() {
643 *ptr.add(i) = pos.y;
644 }
645 ptr
646 };
647
648 let pos_z_ptr = unsafe {
649 let ptr = std::alloc::alloc(
650 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
651 ) as *mut c_double;
652 for (i, pos) in results.impact_positions.iter().enumerate() {
653 *ptr.add(i) = pos.z;
654 }
655 ptr
656 };
657
658 let result = Box::new(FFIMonteCarloResults {
660 ranges: ranges_ptr,
661 impact_velocities: velocities_ptr,
662 impact_positions_x: pos_x_ptr,
663 impact_positions_y: pos_y_ptr,
664 impact_positions_z: pos_z_ptr,
665 num_results,
666 mean_range,
667 std_dev_range,
668 mean_impact_velocity: mean_velocity,
669 std_dev_impact_velocity: std_dev_velocity,
670 hit_probability,
671 });
672
673 Box::into_raw(result)
674 }
675 Err(_) => ptr::null_mut(),
676 }
677}
678
679#[no_mangle]
681pub extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
682 if results.is_null() {
683 return;
684 }
685
686 unsafe {
687 let results = Box::from_raw(results);
688 let num = results.num_results as usize;
689
690 if !results.ranges.is_null() {
692 std::alloc::dealloc(
693 results.ranges as *mut u8,
694 std::alloc::Layout::array::<c_double>(num).unwrap(),
695 );
696 }
697
698 if !results.impact_velocities.is_null() {
699 std::alloc::dealloc(
700 results.impact_velocities as *mut u8,
701 std::alloc::Layout::array::<c_double>(num).unwrap(),
702 );
703 }
704
705 if !results.impact_positions_x.is_null() {
706 std::alloc::dealloc(
707 results.impact_positions_x as *mut u8,
708 std::alloc::Layout::array::<c_double>(num).unwrap(),
709 );
710 }
711
712 if !results.impact_positions_y.is_null() {
713 std::alloc::dealloc(
714 results.impact_positions_y as *mut u8,
715 std::alloc::Layout::array::<c_double>(num).unwrap(),
716 );
717 }
718
719 if !results.impact_positions_z.is_null() {
720 std::alloc::dealloc(
721 results.impact_positions_z as *mut u8,
722 std::alloc::Layout::array::<c_double>(num).unwrap(),
723 );
724 }
725
726 }
728}
729
730#[no_mangle]
732pub extern "C" fn ballistics_get_version() -> *const c_char {
733 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
737}