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