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, }
40
41#[repr(C)]
42pub struct FFIWindConditions {
43 pub speed: c_double, pub direction: c_double, }
46
47#[repr(C)]
48pub struct FFIAtmosphericConditions {
49 pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
54
55#[repr(C)]
56pub struct FFITrajectorySample {
57 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, }
66
67#[repr(C)]
68pub struct FFITrajectoryPoint {
69 pub time: c_double,
70 pub position_x: c_double,
71 pub position_y: c_double,
72 pub position_z: c_double,
73 pub velocity_magnitude: c_double,
74 pub kinetic_energy: c_double,
75}
76
77#[repr(C)]
78pub struct FFITrajectoryResult {
79 pub max_range: c_double,
80 pub max_height: c_double,
81 pub time_of_flight: c_double,
82 pub impact_velocity: c_double,
83 pub impact_energy: c_double,
84 pub points: *mut FFITrajectoryPoint,
85 pub point_count: c_int,
86 pub sampled_points: *mut FFITrajectorySample,
87 pub sampled_point_count: c_int,
88 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, }
95
96#[repr(C)]
98pub struct FFIMonteCarloParams {
99 pub num_simulations: c_int,
100 pub velocity_std_dev: c_double,
101 pub angle_std_dev: c_double,
102 pub bc_std_dev: c_double,
103 pub wind_speed_std_dev: c_double,
104 pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
109
110#[repr(C)]
112pub struct FFIMonteCarloResults {
113 pub ranges: *mut c_double,
114 pub impact_velocities: *mut c_double,
115 pub impact_positions_x: *mut c_double,
116 pub impact_positions_y: *mut c_double,
117 pub impact_positions_z: *mut c_double,
118 pub num_results: c_int,
119 pub mean_range: c_double,
120 pub std_dev_range: c_double,
121 pub mean_impact_velocity: c_double,
122 pub std_dev_impact_velocity: c_double,
123 pub hit_probability: c_double, }
125
126fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
128 let mut ballistic_inputs = BallisticInputs::default();
129
130 ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
131 ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
132 ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
133 ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
134 ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
135 ballistic_inputs.bc_value = inputs.bc_value;
136 ballistic_inputs.bullet_mass = inputs.bullet_mass;
137 ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
138 ballistic_inputs.bc_type = match inputs.bc_type {
139 1 => DragModel::G7,
140 2 => DragModel::G2,
141 3 => DragModel::G5,
142 4 => DragModel::G6,
143 5 => DragModel::G8,
144 6 => DragModel::GI,
145 7 => DragModel::GS,
146 _ => DragModel::G1,
147 };
148 ballistic_inputs.sight_height = inputs.sight_height;
149 ballistic_inputs.target_distance = inputs.target_distance;
150 ballistic_inputs.temperature = inputs.temperature;
151 ballistic_inputs.twist_rate = inputs.twist_rate;
152 ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
153 ballistic_inputs.shooting_angle = inputs.shooting_angle;
154 ballistic_inputs.altitude = inputs.altitude;
155
156 if !inputs.latitude.is_nan() {
157 ballistic_inputs.latitude = Some(inputs.latitude);
158 }
159
160 ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
162 ballistic_inputs.weight_grains = inputs.bullet_mass / 0.00006479891;
163 ballistic_inputs.bullet_length = inputs.bullet_diameter * 4.5; ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
167 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
168 ballistic_inputs.sample_interval = inputs.sample_interval;
169 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
170 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
171 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
172 ballistic_inputs.enable_advanced_effects =
173 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
174 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
176 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
177
178 ballistic_inputs
179}
180
181#[no_mangle]
183pub extern "C" fn ballistics_calculate_trajectory(
184 inputs: *const FFIBallisticInputs,
185 wind: *const FFIWindConditions,
186 atmosphere: *const FFIAtmosphericConditions,
187 max_range: c_double,
188 step_size: c_double,
189) -> *mut FFITrajectoryResult {
190 if inputs.is_null() {
191 return ptr::null_mut();
192 }
193
194 let inputs = unsafe { &*inputs };
195 let ballistic_inputs = convert_inputs(inputs);
196
197 let wind_conditions = if wind.is_null() {
198 WindConditions::default()
199 } else {
200 let wind = unsafe { &*wind };
201 WindConditions {
202 speed: wind.speed,
203 direction: wind.direction,
204 }
205 };
206
207 let atmospheric_conditions = if atmosphere.is_null() {
208 AtmosphericConditions::default()
209 } else {
210 let atmo = unsafe { &*atmosphere };
211 AtmosphericConditions {
212 temperature: atmo.temperature,
213 pressure: atmo.pressure,
214 humidity: atmo.humidity,
215 altitude: atmo.altitude,
216 }
217 };
218
219 let mut solver =
221 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
222
223 solver.set_max_range(max_range);
225 solver.set_time_step(step_size / 1000.0); match solver.solve() {
228 Ok(result) => {
229 let point_count = result.points.len();
231 let points = if point_count > 0 {
232 let mut ffi_points = Vec::with_capacity(point_count);
233 for (i, point) in result.points.iter().enumerate() {
234 ffi_points.push(FFITrajectoryPoint {
235 time: point.time,
236 position_x: point.position[0],
237 position_y: point.position[1],
238 position_z: point.position[2],
239 velocity_magnitude: point.velocity_magnitude,
240 kinetic_energy: point.kinetic_energy,
241 });
242
243 #[cfg(debug_assertions)]
247 if i == 0 || i == result.points.len() - 1 || i % 100 == 0 {
248 eprintln!(
249 "FFI point {}: lateral={:.2}m, vertical={:.2}m, downrange={:.2}m",
250 i, point.position[2], point.position[1], point.position[0]
251 );
252 }
253 }
254 let points_ptr = ffi_points.as_mut_ptr();
255 std::mem::forget(ffi_points); points_ptr
257 } else {
258 ptr::null_mut()
259 };
260
261 let (sampled_points, sampled_point_count) =
263 if let Some(ref samples) = result.sampled_points {
264 let mut ffi_samples = Vec::with_capacity(samples.len());
265 for sample in samples {
266 ffi_samples.push(FFITrajectorySample {
267 distance: sample.distance_m,
268 time: sample.time_s,
269 velocity_mps: sample.velocity_mps,
270 energy_joules: sample.energy_j,
271 drop_meters: sample.drop_m,
272 windage_meters: sample.wind_drift_m,
273 mach: 0.0, spin_rate_rps: 0.0, });
276 }
277 let count = ffi_samples.len() as c_int;
278 let samples_ptr = ffi_samples.as_mut_ptr();
279 std::mem::forget(ffi_samples);
280 (samples_ptr, count)
281 } else {
282 (ptr::null_mut(), 0)
283 };
284
285 let (final_pitch, final_yaw, max_yaw, max_prec) =
287 if let Some(ref angular) = result.angular_state {
288 (
289 angular.pitch_angle,
290 angular.yaw_angle,
291 result.max_yaw_angle.unwrap_or(std::f64::NAN),
292 result.max_precession_angle.unwrap_or(std::f64::NAN),
293 )
294 } else {
295 (std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
296 };
297
298 let ffi_result = Box::new(FFITrajectoryResult {
300 max_range: result.max_range,
301 max_height: result.max_height,
302 time_of_flight: result.time_of_flight,
303 impact_velocity: result.impact_velocity,
304 impact_energy: result.impact_energy,
305 points,
306 point_count: point_count as c_int,
307 sampled_points,
308 sampled_point_count,
309 min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
310 transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
311 final_pitch_angle: final_pitch,
312 final_yaw_angle: final_yaw,
313 max_yaw_angle: max_yaw,
314 max_precession_angle: max_prec,
315 });
316
317 Box::into_raw(ffi_result)
318 }
319 Err(_) => ptr::null_mut(),
320 }
321}
322
323#[no_mangle]
325pub extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
326 if !result.is_null() {
327 unsafe {
328 let result = Box::from_raw(result);
329 if !result.points.is_null() && result.point_count > 0 {
330 let points = Vec::from_raw_parts(
331 result.points,
332 result.point_count as usize,
333 result.point_count as usize,
334 );
335 drop(points);
336 }
337 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
338 let samples = Vec::from_raw_parts(
339 result.sampled_points,
340 result.sampled_point_count as usize,
341 result.sampled_point_count as usize,
342 );
343 drop(samples);
344 }
345 drop(result);
346 }
347 }
348}
349
350#[no_mangle]
352pub extern "C" fn ballistics_calculate_zero_angle(
353 inputs: *const FFIBallisticInputs,
354 wind: *const FFIWindConditions,
355 atmosphere: *const FFIAtmosphericConditions,
356 zero_distance: c_double,
357) -> c_double {
358 if inputs.is_null() {
359 return f64::NAN;
360 }
361
362 let inputs = unsafe { &*inputs };
363 let ballistic_inputs = convert_inputs(inputs);
364
365 let wind_conditions = if wind.is_null() {
366 WindConditions::default()
367 } else {
368 let wind = unsafe { &*wind };
369 WindConditions {
370 speed: wind.speed,
371 direction: wind.direction,
372 }
373 };
374
375 let atmospheric_conditions = if atmosphere.is_null() {
376 AtmosphericConditions::default()
377 } else {
378 let atmo = unsafe { &*atmosphere };
379 AtmosphericConditions {
380 temperature: atmo.temperature,
381 pressure: atmo.pressure,
382 humidity: atmo.humidity,
383 altitude: atmo.altitude,
384 }
385 };
386
387 let target_height = ballistic_inputs.sight_height;
390
391 #[cfg(debug_assertions)]
392 {
393 eprintln!("FFI: Calculating zero angle for:");
394 eprintln!(" Zero distance: {} m", zero_distance);
395 eprintln!(" Target height: {} m", target_height);
396 eprintln!(" Sight height: {} m", ballistic_inputs.sight_height);
397 eprintln!(" Wind speed: {} m/s", wind_conditions.speed);
398 eprintln!(" Temperature: {} C", atmospheric_conditions.temperature);
399 }
400
401 match calculate_zero_angle_with_conditions(
402 ballistic_inputs,
403 zero_distance,
404 target_height,
405 wind_conditions,
406 atmospheric_conditions,
407 ) {
408 Ok(angle) => {
409 #[cfg(debug_assertions)]
410 eprintln!(
411 " Calculated angle: {} rad ({} deg)",
412 angle,
413 angle * 180.0 / std::f64::consts::PI
414 );
415 angle
416 }
417 Err(e) => {
418 #[cfg(debug_assertions)]
419 eprintln!(" Error: {:?}", e);
420 f64::NAN
421 }
422 }
423}
424
425#[no_mangle]
427pub extern "C" fn ballistics_quick_trajectory(
428 muzzle_velocity: c_double,
429 bc: c_double,
430 sight_height: c_double,
431 zero_distance: c_double,
432 target_distance: c_double,
433) -> c_double {
434 let mut inputs = BallisticInputs::default();
438 inputs.muzzle_velocity = muzzle_velocity;
439 inputs.bc_value = bc;
440 inputs.sight_height = sight_height;
441 inputs.target_distance = target_distance;
442
443 let wind = WindConditions::default();
444 let atmo = AtmosphericConditions::default();
445
446 let zero_angle = match calculate_zero_angle_with_conditions(
448 inputs.clone(),
449 zero_distance,
450 sight_height,
451 wind.clone(),
452 atmo.clone(),
453 ) {
454 Ok(angle) => angle,
455 Err(_) => return f64::NAN,
456 };
457
458 inputs.muzzle_angle = zero_angle;
460
461 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
462 solver.set_max_range(target_distance * 1.1);
463
464 match solver.solve() {
465 Ok(result) => {
466 for point in result.points {
468 if point.position[0] >= target_distance {
469 return -point.position[1]; }
471 }
472 f64::NAN
473 }
474 Err(_) => f64::NAN,
475 }
476}
477
478#[no_mangle]
480pub extern "C" fn ballistics_monte_carlo(
481 inputs: *const FFIBallisticInputs,
482 _atmosphere: *const FFIAtmosphericConditions,
483 params: *const FFIMonteCarloParams,
484) -> *mut FFIMonteCarloResults {
485 if inputs.is_null() || params.is_null() {
486 return ptr::null_mut();
487 }
488
489 let inputs = unsafe { &*inputs };
490 let params = unsafe { &*params };
491
492 const MAX_SIMULATIONS: c_int = 1_000_000;
498 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
499 return ptr::null_mut();
500 }
501
502 let ballistic_inputs = convert_inputs(inputs);
504
505 let mc_params = MonteCarloParams {
510 num_simulations: params.num_simulations as usize,
511 velocity_std_dev: params.velocity_std_dev,
512 angle_std_dev: params.angle_std_dev,
513 bc_std_dev: params.bc_std_dev,
514 wind_speed_std_dev: params.wind_speed_std_dev,
515 target_distance: if params.target_distance.is_nan() {
516 None
517 } else {
518 Some(params.target_distance)
519 },
520 base_wind_speed: params.base_wind_speed,
521 base_wind_direction: params.base_wind_direction,
522 azimuth_std_dev: params.azimuth_std_dev,
523 };
524
525 match run_monte_carlo(ballistic_inputs, mc_params) {
527 Ok(results) => {
528 let num_results = results.ranges.len() as c_int;
529
530 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
532 let variance_range: f64 = results
533 .ranges
534 .iter()
535 .map(|r| (r - mean_range).powi(2))
536 .sum::<f64>()
537 / num_results as f64;
538 let std_dev_range = variance_range.sqrt();
539
540 let mean_velocity: f64 =
541 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
542 let variance_velocity: f64 = results
543 .impact_velocities
544 .iter()
545 .map(|v| (v - mean_velocity).powi(2))
546 .sum::<f64>()
547 / num_results as f64;
548 let std_dev_velocity = variance_velocity.sqrt();
549
550 let hit_probability = if params.target_distance.is_nan() {
552 0.0
553 } else {
554 let target = params.target_distance;
555 let hit_radius = 0.3; let hits = results
557 .impact_positions
558 .iter()
559 .filter(|pos| {
560 let distance = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
561 distance < target && pos.norm() < hit_radius
562 })
563 .count();
564 hits as f64 / num_results as f64
565 };
566
567 let ranges_ptr = unsafe {
569 let ptr = std::alloc::alloc(
570 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
571 ) as *mut c_double;
572 for (i, &range) in results.ranges.iter().enumerate() {
573 *ptr.add(i) = range;
574 }
575 ptr
576 };
577
578 let velocities_ptr = unsafe {
579 let ptr = std::alloc::alloc(
580 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
581 ) as *mut c_double;
582 for (i, &vel) in results.impact_velocities.iter().enumerate() {
583 *ptr.add(i) = vel;
584 }
585 ptr
586 };
587
588 let pos_x_ptr = unsafe {
589 let ptr = std::alloc::alloc(
590 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
591 ) as *mut c_double;
592 for (i, pos) in results.impact_positions.iter().enumerate() {
593 *ptr.add(i) = pos.x;
594 }
595 ptr
596 };
597
598 let pos_y_ptr = unsafe {
599 let ptr = std::alloc::alloc(
600 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
601 ) as *mut c_double;
602 for (i, pos) in results.impact_positions.iter().enumerate() {
603 *ptr.add(i) = pos.y;
604 }
605 ptr
606 };
607
608 let pos_z_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, pos) in results.impact_positions.iter().enumerate() {
613 *ptr.add(i) = pos.z;
614 }
615 ptr
616 };
617
618 let result = Box::new(FFIMonteCarloResults {
620 ranges: ranges_ptr,
621 impact_velocities: velocities_ptr,
622 impact_positions_x: pos_x_ptr,
623 impact_positions_y: pos_y_ptr,
624 impact_positions_z: pos_z_ptr,
625 num_results,
626 mean_range,
627 std_dev_range,
628 mean_impact_velocity: mean_velocity,
629 std_dev_impact_velocity: std_dev_velocity,
630 hit_probability,
631 });
632
633 Box::into_raw(result)
634 }
635 Err(_) => ptr::null_mut(),
636 }
637}
638
639#[no_mangle]
641pub extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
642 if results.is_null() {
643 return;
644 }
645
646 unsafe {
647 let results = Box::from_raw(results);
648 let num = results.num_results as usize;
649
650 if !results.ranges.is_null() {
652 std::alloc::dealloc(
653 results.ranges as *mut u8,
654 std::alloc::Layout::array::<c_double>(num).unwrap(),
655 );
656 }
657
658 if !results.impact_velocities.is_null() {
659 std::alloc::dealloc(
660 results.impact_velocities as *mut u8,
661 std::alloc::Layout::array::<c_double>(num).unwrap(),
662 );
663 }
664
665 if !results.impact_positions_x.is_null() {
666 std::alloc::dealloc(
667 results.impact_positions_x as *mut u8,
668 std::alloc::Layout::array::<c_double>(num).unwrap(),
669 );
670 }
671
672 if !results.impact_positions_y.is_null() {
673 std::alloc::dealloc(
674 results.impact_positions_y as *mut u8,
675 std::alloc::Layout::array::<c_double>(num).unwrap(),
676 );
677 }
678
679 if !results.impact_positions_z.is_null() {
680 std::alloc::dealloc(
681 results.impact_positions_z as *mut u8,
682 std::alloc::Layout::array::<c_double>(num).unwrap(),
683 );
684 }
685
686 }
688}
689
690#[no_mangle]
692pub extern "C" fn ballistics_get_version() -> *const c_char {
693 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
697}