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 = inputs.bullet_diameter * 4.5; ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
174 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
175 ballistic_inputs.sample_interval = inputs.sample_interval;
176 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
177 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
178 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
179 ballistic_inputs.enable_advanced_effects =
180 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
181 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
183 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
184
185 ballistic_inputs
186}
187
188#[no_mangle]
190pub extern "C" fn ballistics_calculate_trajectory(
191 inputs: *const FFIBallisticInputs,
192 wind: *const FFIWindConditions,
193 atmosphere: *const FFIAtmosphericConditions,
194 max_range: c_double,
195 step_size: c_double,
196) -> *mut FFITrajectoryResult {
197 if inputs.is_null() {
198 return ptr::null_mut();
199 }
200
201 let inputs = unsafe { &*inputs };
202 let ballistic_inputs = convert_inputs(inputs);
203
204 let wind_conditions = if wind.is_null() {
205 WindConditions::default()
206 } else {
207 let wind = unsafe { &*wind };
208 WindConditions {
209 speed: wind.speed,
210 direction: wind.direction,
211 }
212 };
213
214 let atmospheric_conditions = if atmosphere.is_null() {
215 AtmosphericConditions::default()
216 } else {
217 let atmo = unsafe { &*atmosphere };
218 AtmosphericConditions {
219 temperature: atmo.temperature,
220 pressure: atmo.pressure,
221 humidity: atmo.humidity,
222 altitude: atmo.altitude,
223 }
224 };
225
226 let mut solver =
228 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
229
230 solver.set_max_range(max_range);
232 solver.set_time_step(step_size / 1000.0); match solver.solve() {
235 Ok(result) => {
236 let point_count = result.points.len();
238 let points = if point_count > 0 {
239 let mut ffi_points = Vec::with_capacity(point_count);
240 for (i, point) in result.points.iter().enumerate() {
241 ffi_points.push(FFITrajectoryPoint {
242 time: point.time,
243 position_x: point.position[0],
244 position_y: point.position[1],
245 position_z: point.position[2],
246 velocity_magnitude: point.velocity_magnitude,
247 kinetic_energy: point.kinetic_energy,
248 });
249
250 #[cfg(debug_assertions)]
254 if i == 0 || i == result.points.len() - 1 || i % 100 == 0 {
255 eprintln!(
256 "FFI point {}: lateral={:.2}m, vertical={:.2}m, downrange={:.2}m",
257 i, point.position[2], point.position[1], point.position[0]
258 );
259 }
260 }
261 let points_ptr = ffi_points.as_mut_ptr();
262 std::mem::forget(ffi_points); points_ptr
264 } else {
265 ptr::null_mut()
266 };
267
268 let (sampled_points, sampled_point_count) =
270 if let Some(ref samples) = result.sampled_points {
271 let mut ffi_samples = Vec::with_capacity(samples.len());
272 for sample in samples {
273 ffi_samples.push(FFITrajectorySample {
274 distance: sample.distance_m,
275 time: sample.time_s,
276 velocity_mps: sample.velocity_mps,
277 energy_joules: sample.energy_j,
278 drop_meters: sample.drop_m,
279 windage_meters: sample.wind_drift_m,
280 mach: 0.0, spin_rate_rps: 0.0, });
283 }
284 let count = ffi_samples.len() as c_int;
285 let samples_ptr = ffi_samples.as_mut_ptr();
286 std::mem::forget(ffi_samples);
287 (samples_ptr, count)
288 } else {
289 (ptr::null_mut(), 0)
290 };
291
292 let (final_pitch, final_yaw, max_yaw, max_prec) =
294 if let Some(ref angular) = result.angular_state {
295 (
296 angular.pitch_angle,
297 angular.yaw_angle,
298 result.max_yaw_angle.unwrap_or(std::f64::NAN),
299 result.max_precession_angle.unwrap_or(std::f64::NAN),
300 )
301 } else {
302 (std::f64::NAN, std::f64::NAN, std::f64::NAN, std::f64::NAN)
303 };
304
305 let ffi_result = Box::new(FFITrajectoryResult {
307 max_range: result.max_range,
308 max_height: result.max_height,
309 time_of_flight: result.time_of_flight,
310 impact_velocity: result.impact_velocity,
311 impact_energy: result.impact_energy,
312 points,
313 point_count: point_count as c_int,
314 sampled_points,
315 sampled_point_count,
316 min_pitch_damping: result.min_pitch_damping.unwrap_or(std::f64::NAN),
317 transonic_mach: result.transonic_mach.unwrap_or(std::f64::NAN),
318 final_pitch_angle: final_pitch,
319 final_yaw_angle: final_yaw,
320 max_yaw_angle: max_yaw,
321 max_precession_angle: max_prec,
322 });
323
324 Box::into_raw(ffi_result)
325 }
326 Err(_) => ptr::null_mut(),
327 }
328}
329
330#[no_mangle]
332pub extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
333 if !result.is_null() {
334 unsafe {
335 let result = Box::from_raw(result);
336 if !result.points.is_null() && result.point_count > 0 {
337 let points = Vec::from_raw_parts(
338 result.points,
339 result.point_count as usize,
340 result.point_count as usize,
341 );
342 drop(points);
343 }
344 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
345 let samples = Vec::from_raw_parts(
346 result.sampled_points,
347 result.sampled_point_count as usize,
348 result.sampled_point_count as usize,
349 );
350 drop(samples);
351 }
352 drop(result);
353 }
354 }
355}
356
357#[no_mangle]
359pub extern "C" fn ballistics_calculate_zero_angle(
360 inputs: *const FFIBallisticInputs,
361 wind: *const FFIWindConditions,
362 atmosphere: *const FFIAtmosphericConditions,
363 zero_distance: c_double,
364) -> c_double {
365 if inputs.is_null() {
366 return f64::NAN;
367 }
368
369 let inputs = unsafe { &*inputs };
370 let ballistic_inputs = convert_inputs(inputs);
371
372 let wind_conditions = if wind.is_null() {
373 WindConditions::default()
374 } else {
375 let wind = unsafe { &*wind };
376 WindConditions {
377 speed: wind.speed,
378 direction: wind.direction,
379 }
380 };
381
382 let atmospheric_conditions = if atmosphere.is_null() {
383 AtmosphericConditions::default()
384 } else {
385 let atmo = unsafe { &*atmosphere };
386 AtmosphericConditions {
387 temperature: atmo.temperature,
388 pressure: atmo.pressure,
389 humidity: atmo.humidity,
390 altitude: atmo.altitude,
391 }
392 };
393
394 let target_height = ballistic_inputs.sight_height;
397
398 #[cfg(debug_assertions)]
399 {
400 eprintln!("FFI: Calculating zero angle for:");
401 eprintln!(" Zero distance: {} m", zero_distance);
402 eprintln!(" Target height: {} m", target_height);
403 eprintln!(" Sight height: {} m", ballistic_inputs.sight_height);
404 eprintln!(" Wind speed: {} m/s", wind_conditions.speed);
405 eprintln!(" Temperature: {} C", atmospheric_conditions.temperature);
406 }
407
408 match calculate_zero_angle_with_conditions(
409 ballistic_inputs,
410 zero_distance,
411 target_height,
412 wind_conditions,
413 atmospheric_conditions,
414 ) {
415 Ok(angle) => {
416 #[cfg(debug_assertions)]
417 eprintln!(
418 " Calculated angle: {} rad ({} deg)",
419 angle,
420 angle * 180.0 / std::f64::consts::PI
421 );
422 angle
423 }
424 Err(e) => {
425 #[cfg(debug_assertions)]
426 eprintln!(" Error: {:?}", e);
427 f64::NAN
428 }
429 }
430}
431
432#[no_mangle]
434pub extern "C" fn ballistics_quick_trajectory(
435 muzzle_velocity: c_double,
436 bc: c_double,
437 sight_height: c_double,
438 zero_distance: c_double,
439 target_distance: c_double,
440) -> c_double {
441 let mut inputs = BallisticInputs::default();
445 inputs.muzzle_velocity = muzzle_velocity;
446 inputs.bc_value = bc;
447 inputs.sight_height = sight_height;
448 inputs.target_distance = target_distance;
449
450 let wind = WindConditions::default();
451 let atmo = AtmosphericConditions::default();
452
453 let zero_angle = match calculate_zero_angle_with_conditions(
455 inputs.clone(),
456 zero_distance,
457 sight_height,
458 wind.clone(),
459 atmo.clone(),
460 ) {
461 Ok(angle) => angle,
462 Err(_) => return f64::NAN,
463 };
464
465 inputs.muzzle_angle = zero_angle;
467
468 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
469 solver.set_max_range(target_distance * 1.1);
470
471 match solver.solve() {
472 Ok(result) => {
473 for point in result.points {
475 if point.position[0] >= target_distance {
476 return -point.position[1]; }
478 }
479 f64::NAN
480 }
481 Err(_) => f64::NAN,
482 }
483}
484
485#[no_mangle]
487pub extern "C" fn ballistics_monte_carlo(
488 inputs: *const FFIBallisticInputs,
489 _atmosphere: *const FFIAtmosphericConditions,
490 params: *const FFIMonteCarloParams,
491) -> *mut FFIMonteCarloResults {
492 if inputs.is_null() || params.is_null() {
493 return ptr::null_mut();
494 }
495
496 let inputs = unsafe { &*inputs };
497 let params = unsafe { &*params };
498
499 const MAX_SIMULATIONS: c_int = 1_000_000;
505 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
506 return ptr::null_mut();
507 }
508
509 let ballistic_inputs = convert_inputs(inputs);
511
512 let mc_params = MonteCarloParams {
517 num_simulations: params.num_simulations as usize,
518 velocity_std_dev: params.velocity_std_dev,
519 angle_std_dev: params.angle_std_dev,
520 bc_std_dev: params.bc_std_dev,
521 wind_speed_std_dev: params.wind_speed_std_dev,
522 target_distance: if params.target_distance.is_nan() {
523 None
524 } else {
525 Some(params.target_distance)
526 },
527 base_wind_speed: params.base_wind_speed,
528 base_wind_direction: params.base_wind_direction,
529 azimuth_std_dev: params.azimuth_std_dev,
530 };
531
532 match run_monte_carlo(ballistic_inputs, mc_params) {
534 Ok(results) => {
535 let num_results = results.ranges.len() as c_int;
536
537 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
539 let variance_range: f64 = results
540 .ranges
541 .iter()
542 .map(|r| (r - mean_range).powi(2))
543 .sum::<f64>()
544 / num_results as f64;
545 let std_dev_range = variance_range.sqrt();
546
547 let mean_velocity: f64 =
548 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
549 let variance_velocity: f64 = results
550 .impact_velocities
551 .iter()
552 .map(|v| (v - mean_velocity).powi(2))
553 .sum::<f64>()
554 / num_results as f64;
555 let std_dev_velocity = variance_velocity.sqrt();
556
557 let hit_probability = if params.target_distance.is_nan() {
559 0.0
560 } else {
561 let target = params.target_distance;
562 let hit_radius = 0.3; let hits = results
564 .impact_positions
565 .iter()
566 .filter(|pos| {
567 let distance = (pos.x.powi(2) + pos.y.powi(2)).sqrt();
568 distance < target && pos.norm() < hit_radius
569 })
570 .count();
571 hits as f64 / num_results as f64
572 };
573
574 let ranges_ptr = unsafe {
576 let ptr = std::alloc::alloc(
577 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
578 ) as *mut c_double;
579 for (i, &range) in results.ranges.iter().enumerate() {
580 *ptr.add(i) = range;
581 }
582 ptr
583 };
584
585 let velocities_ptr = unsafe {
586 let ptr = std::alloc::alloc(
587 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
588 ) as *mut c_double;
589 for (i, &vel) in results.impact_velocities.iter().enumerate() {
590 *ptr.add(i) = vel;
591 }
592 ptr
593 };
594
595 let pos_x_ptr = unsafe {
596 let ptr = std::alloc::alloc(
597 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
598 ) as *mut c_double;
599 for (i, pos) in results.impact_positions.iter().enumerate() {
600 *ptr.add(i) = pos.x;
601 }
602 ptr
603 };
604
605 let pos_y_ptr = unsafe {
606 let ptr = std::alloc::alloc(
607 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
608 ) as *mut c_double;
609 for (i, pos) in results.impact_positions.iter().enumerate() {
610 *ptr.add(i) = pos.y;
611 }
612 ptr
613 };
614
615 let pos_z_ptr = unsafe {
616 let ptr = std::alloc::alloc(
617 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
618 ) as *mut c_double;
619 for (i, pos) in results.impact_positions.iter().enumerate() {
620 *ptr.add(i) = pos.z;
621 }
622 ptr
623 };
624
625 let result = Box::new(FFIMonteCarloResults {
627 ranges: ranges_ptr,
628 impact_velocities: velocities_ptr,
629 impact_positions_x: pos_x_ptr,
630 impact_positions_y: pos_y_ptr,
631 impact_positions_z: pos_z_ptr,
632 num_results,
633 mean_range,
634 std_dev_range,
635 mean_impact_velocity: mean_velocity,
636 std_dev_impact_velocity: std_dev_velocity,
637 hit_probability,
638 });
639
640 Box::into_raw(result)
641 }
642 Err(_) => ptr::null_mut(),
643 }
644}
645
646#[no_mangle]
648pub extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
649 if results.is_null() {
650 return;
651 }
652
653 unsafe {
654 let results = Box::from_raw(results);
655 let num = results.num_results as usize;
656
657 if !results.ranges.is_null() {
659 std::alloc::dealloc(
660 results.ranges as *mut u8,
661 std::alloc::Layout::array::<c_double>(num).unwrap(),
662 );
663 }
664
665 if !results.impact_velocities.is_null() {
666 std::alloc::dealloc(
667 results.impact_velocities as *mut u8,
668 std::alloc::Layout::array::<c_double>(num).unwrap(),
669 );
670 }
671
672 if !results.impact_positions_x.is_null() {
673 std::alloc::dealloc(
674 results.impact_positions_x as *mut u8,
675 std::alloc::Layout::array::<c_double>(num).unwrap(),
676 );
677 }
678
679 if !results.impact_positions_y.is_null() {
680 std::alloc::dealloc(
681 results.impact_positions_y as *mut u8,
682 std::alloc::Layout::array::<c_double>(num).unwrap(),
683 );
684 }
685
686 if !results.impact_positions_z.is_null() {
687 std::alloc::dealloc(
688 results.impact_positions_z as *mut u8,
689 std::alloc::Layout::array::<c_double>(num).unwrap(),
690 );
691 }
692
693 }
695}
696
697#[no_mangle]
699pub extern "C" fn ballistics_get_version() -> *const c_char {
700 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
704}