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