1use std::collections::HashMap;
41use std::fs::File;
42use std::io::{BufReader, Read};
43use std::path::{Path, PathBuf};
44
45const MAGIC: &[u8; 4] = b"BC5D";
47
48const SUPPORTED_VERSION: u32 = 2;
50
51#[derive(Debug)]
53pub struct Bc5dTable {
54 caliber: f32,
56 data: Vec<f32>,
58 weight_bins: Vec<f32>,
60 bc_bins: Vec<f32>,
62 muzzle_vel_bins: Vec<f32>,
64 current_vel_bins: Vec<f32>,
66 num_drag_types: usize,
68 version: u32,
70 api_version: String,
72 timestamp: u64,
74}
75
76#[derive(Debug, Default)]
78pub struct Bc5dTableManager {
79 table_dir: Option<PathBuf>,
81 tables: HashMap<i32, Bc5dTable>,
83}
84
85#[derive(Debug)]
87pub enum Bc5dError {
88 IoError(std::io::Error),
89 InvalidMagic,
90 UnsupportedVersion(u32),
91 ChecksumMismatch { expected: u32, actual: u32 },
92 InvalidDimensions,
93 TableNotFound(f64),
94 NoTableDirectory,
95}
96
97impl std::fmt::Display for Bc5dError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Bc5dError::IoError(e) => write!(f, "IO error: {}", e),
101 Bc5dError::InvalidMagic => write!(f, "Invalid file magic (expected 'BC5D')"),
102 Bc5dError::UnsupportedVersion(v) => write!(f, "Unsupported table version: {}", v),
103 Bc5dError::ChecksumMismatch { expected, actual } => {
104 write!(f, "Checksum mismatch: expected {:08x}, got {:08x}", expected, actual)
105 }
106 Bc5dError::InvalidDimensions => write!(f, "Invalid table dimensions"),
107 Bc5dError::TableNotFound(cal) => write!(f, "No BC5D table found for caliber {:.3}", cal),
108 Bc5dError::NoTableDirectory => write!(f, "No BC table directory configured"),
109 }
110 }
111}
112
113impl std::error::Error for Bc5dError {}
114
115impl From<std::io::Error> for Bc5dError {
116 fn from(e: std::io::Error) -> Self {
117 Bc5dError::IoError(e)
118 }
119}
120
121impl Bc5dTable {
122 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Bc5dError> {
124 let file = File::open(&path)?;
125 let mut reader = BufReader::new(file);
126
127 let mut magic = [0u8; 4];
129 reader.read_exact(&mut magic)?;
130 if &magic != MAGIC {
131 return Err(Bc5dError::InvalidMagic);
132 }
133
134 let version = read_u32(&mut reader)?;
136 if version != SUPPORTED_VERSION {
137 return Err(Bc5dError::UnsupportedVersion(version));
138 }
139
140 let caliber = read_f32(&mut reader)?;
141 let _flags = read_u32(&mut reader)?;
142 let _padding = read_u32(&mut reader)?;
143
144 let dim_weight = read_u32(&mut reader)? as usize;
145 let dim_bc = read_u32(&mut reader)? as usize;
146 let dim_muzzle_vel = read_u32(&mut reader)? as usize;
147 let dim_current_vel = read_u32(&mut reader)? as usize;
148 let dim_drag_types = read_u32(&mut reader)? as usize;
149
150 let timestamp = read_u64(&mut reader)?;
151 let stored_checksum = read_u32(&mut reader)?;
152
153 let mut api_version_bytes = [0u8; 16];
155 reader.read_exact(&mut api_version_bytes)?;
156 let api_version = String::from_utf8_lossy(&api_version_bytes)
157 .trim_end_matches('\0')
158 .to_string();
159
160 let mut reserved = [0u8; 12];
162 reader.read_exact(&mut reserved)?;
163
164 if dim_weight == 0 || dim_bc == 0 || dim_muzzle_vel == 0 || dim_current_vel == 0 || dim_drag_types == 0 {
166 return Err(Bc5dError::InvalidDimensions);
167 }
168
169 let weight_bins = read_f32_array(&mut reader, dim_weight)?;
171 let bc_bins = read_f32_array(&mut reader, dim_bc)?;
172 let muzzle_vel_bins = read_f32_array(&mut reader, dim_muzzle_vel)?;
173 let current_vel_bins = read_f32_array(&mut reader, dim_current_vel)?;
174
175 const MAX_TOTAL_CELLS: usize = 64_000_000; let total_cells = dim_drag_types
180 .checked_mul(dim_weight)
181 .and_then(|x| x.checked_mul(dim_bc))
182 .and_then(|x| x.checked_mul(dim_muzzle_vel))
183 .and_then(|x| x.checked_mul(dim_current_vel))
184 .filter(|&n| n <= MAX_TOTAL_CELLS)
185 .ok_or(Bc5dError::InvalidDimensions)?;
186 let data = read_f32_array(&mut reader, total_cells)?;
187
188 let mut checksum_data = Vec::new();
190 for &v in &weight_bins {
191 checksum_data.extend_from_slice(&v.to_le_bytes());
192 }
193 for &v in &bc_bins {
194 checksum_data.extend_from_slice(&v.to_le_bytes());
195 }
196 for &v in &muzzle_vel_bins {
197 checksum_data.extend_from_slice(&v.to_le_bytes());
198 }
199 for &v in ¤t_vel_bins {
200 checksum_data.extend_from_slice(&v.to_le_bytes());
201 }
202 for &v in &data {
203 checksum_data.extend_from_slice(&v.to_le_bytes());
204 }
205
206 let calculated_checksum = crc32_ieee(&checksum_data);
207 if calculated_checksum != stored_checksum {
208 return Err(Bc5dError::ChecksumMismatch {
209 expected: stored_checksum,
210 actual: calculated_checksum,
211 });
212 }
213
214 Ok(Bc5dTable {
215 caliber,
216 data,
217 weight_bins,
218 bc_bins,
219 muzzle_vel_bins,
220 current_vel_bins,
221 num_drag_types: dim_drag_types,
222 version,
223 api_version,
224 timestamp,
225 })
226 }
227
228 pub fn lookup(
241 &self,
242 weight_grains: f64,
243 base_bc: f64,
244 muzzle_velocity: f64,
245 current_velocity: f64,
246 drag_type: &str,
247 ) -> f64 {
248 let drag_idx = if drag_type.eq_ignore_ascii_case("G7") { 1 } else { 0 };
250
251 let drag_idx = drag_idx.min(self.num_drag_types - 1);
253
254 let (weight_idx, weight_w) = self.interp_idx(weight_grains as f32, &self.weight_bins);
256 let (bc_idx, bc_w) = self.interp_idx(base_bc as f32, &self.bc_bins);
257 let (muzzle_idx, muzzle_w) = self.interp_idx(muzzle_velocity as f32, &self.muzzle_vel_bins);
258 let (current_idx, current_w) = self.interp_idx(current_velocity as f32, &self.current_vel_bins);
259
260 let mut result = 0.0f64;
262
263 for dw in 0..2 {
264 for db in 0..2 {
265 for dm in 0..2 {
266 for dc in 0..2 {
267 let weight = (if dw == 0 { 1.0 - weight_w } else { weight_w })
269 * (if db == 0 { 1.0 - bc_w } else { bc_w })
270 * (if dm == 0 { 1.0 - muzzle_w } else { muzzle_w })
271 * (if dc == 0 { 1.0 - current_w } else { current_w });
272
273 let wi = (weight_idx + dw).min(self.weight_bins.len() - 1);
275 let bi = (bc_idx + db).min(self.bc_bins.len() - 1);
276 let mi = (muzzle_idx + dm).min(self.muzzle_vel_bins.len() - 1);
277 let ci = (current_idx + dc).min(self.current_vel_bins.len() - 1);
278
279 let idx = self.flat_index(drag_idx, wi, bi, mi, ci);
281 result += weight * self.data[idx] as f64;
282 }
283 }
284 }
285 }
286
287 result.max(0.5).min(1.5)
289 }
290
291 pub fn get_effective_bc(
295 &self,
296 weight_grains: f64,
297 base_bc: f64,
298 muzzle_velocity: f64,
299 current_velocity: f64,
300 drag_type: &str,
301 ) -> f64 {
302 let correction = self.lookup(weight_grains, base_bc, muzzle_velocity, current_velocity, drag_type);
303 base_bc * correction
304 }
305
306 fn interp_idx(&self, value: f32, bins: &[f32]) -> (usize, f64) {
308 if bins.is_empty() {
309 return (0, 0.0);
310 }
311
312 if value <= bins[0] {
314 return (0, 0.0);
315 }
316 if value >= bins[bins.len() - 1] {
317 return (bins.len().saturating_sub(2), 1.0);
318 }
319
320 let idx = match bins.binary_search_by(|probe| {
322 probe.partial_cmp(&value).unwrap_or(std::cmp::Ordering::Equal)
323 }) {
324 Ok(i) => i.saturating_sub(1).min(bins.len() - 2),
325 Err(i) => i.saturating_sub(1).min(bins.len() - 2),
326 };
327
328 let low = bins[idx];
330 let high = bins[idx + 1];
331 let weight = if high > low {
332 ((value - low) / (high - low)) as f64
333 } else {
334 0.0
335 };
336
337 (idx, weight)
338 }
339
340 fn flat_index(&self, drag_idx: usize, weight_idx: usize, bc_idx: usize, muzzle_idx: usize, current_idx: usize) -> usize {
342 let n_weight = self.weight_bins.len();
343 let n_bc = self.bc_bins.len();
344 let n_muzzle = self.muzzle_vel_bins.len();
345 let n_current = self.current_vel_bins.len();
346
347 drag_idx * (n_weight * n_bc * n_muzzle * n_current)
348 + weight_idx * (n_bc * n_muzzle * n_current)
349 + bc_idx * (n_muzzle * n_current)
350 + muzzle_idx * n_current
351 + current_idx
352 }
353
354 pub fn caliber(&self) -> f32 {
356 self.caliber
357 }
358
359 pub fn version(&self) -> u32 {
361 self.version
362 }
363
364 pub fn api_version(&self) -> &str {
366 &self.api_version
367 }
368
369 pub fn timestamp(&self) -> u64 {
371 self.timestamp
372 }
373
374 pub fn total_cells(&self) -> usize {
376 self.data.len()
377 }
378
379 pub fn dimensions_str(&self) -> String {
381 format!(
382 "{}x{}x{}x{}x{} (weight x bc x muzzle_vel x current_vel x drag_types)",
383 self.weight_bins.len(),
384 self.bc_bins.len(),
385 self.muzzle_vel_bins.len(),
386 self.current_vel_bins.len(),
387 self.num_drag_types
388 )
389 }
390
391 pub fn weight_range(&self) -> (f32, f32) {
393 (*self.weight_bins.first().unwrap_or(&0.0), *self.weight_bins.last().unwrap_or(&0.0))
394 }
395
396 pub fn velocity_range(&self) -> (f32, f32) {
398 (*self.current_vel_bins.first().unwrap_or(&0.0), *self.current_vel_bins.last().unwrap_or(&0.0))
399 }
400}
401
402impl Bc5dTableManager {
403 pub fn new<P: AsRef<Path>>(table_dir: P) -> Self {
405 Bc5dTableManager {
406 table_dir: Some(table_dir.as_ref().to_path_buf()),
407 tables: HashMap::new(),
408 }
409 }
410
411 pub fn empty() -> Self {
413 Bc5dTableManager {
414 table_dir: None,
415 tables: HashMap::new(),
416 }
417 }
418
419 pub fn get_table(&mut self, caliber: f64) -> Result<&Bc5dTable, Bc5dError> {
423 let caliber_key = caliber_to_key(caliber);
424
425 if self.tables.contains_key(&caliber_key) {
427 return Ok(self.tables.get(&caliber_key).unwrap());
428 }
429
430 let table_dir = self.table_dir.as_ref().ok_or(Bc5dError::NoTableDirectory)?;
432 let table_path = find_table_file(table_dir, caliber)?;
433 let table = Bc5dTable::load(&table_path)?;
434 self.tables.insert(caliber_key, table);
435 Ok(self.tables.get(&caliber_key).unwrap())
436 }
437
438 pub fn lookup(
440 &mut self,
441 caliber: f64,
442 weight_grains: f64,
443 base_bc: f64,
444 muzzle_velocity: f64,
445 current_velocity: f64,
446 drag_type: &str,
447 ) -> Result<f64, Bc5dError> {
448 let table = self.get_table(caliber)?;
449 Ok(table.lookup(weight_grains, base_bc, muzzle_velocity, current_velocity, drag_type))
450 }
451
452 pub fn get_effective_bc(
454 &mut self,
455 caliber: f64,
456 weight_grains: f64,
457 base_bc: f64,
458 muzzle_velocity: f64,
459 current_velocity: f64,
460 drag_type: &str,
461 ) -> Result<f64, Bc5dError> {
462 let table = self.get_table(caliber)?;
463 Ok(table.get_effective_bc(weight_grains, base_bc, muzzle_velocity, current_velocity, drag_type))
464 }
465
466 pub fn has_table(&self, caliber: f64) -> bool {
468 if let Some(ref table_dir) = self.table_dir {
469 find_table_file(table_dir, caliber).is_ok()
470 } else {
471 false
472 }
473 }
474
475 pub fn available_calibers(&self) -> Vec<f64> {
477 let mut calibers = Vec::new();
478 if let Some(ref table_dir) = self.table_dir {
479 if let Ok(entries) = std::fs::read_dir(table_dir) {
480 for entry in entries.flatten() {
481 let path = entry.path();
482 if let Some(ext) = path.extension() {
483 if ext == "bin" {
484 if let Some(stem) = path.file_stem() {
485 let name = stem.to_string_lossy();
486 if name.starts_with("bc5d_") {
487 if let Ok(cal_int) = name[5..].parse::<i32>() {
489 calibers.push(cal_int as f64 / 1000.0);
490 }
491 }
492 }
493 }
494 }
495 }
496 }
497 }
498 calibers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
499 calibers
500 }
501}
502
503fn caliber_to_key(caliber: f64) -> i32 {
505 (caliber * 1000.0).round() as i32
506}
507
508fn find_table_file(table_dir: &Path, caliber: f64) -> Result<PathBuf, Bc5dError> {
510 let caliber_int = (caliber * 1000.0).round() as i32;
511 let filename = format!("bc5d_{}.bin", caliber_int);
512 let path = table_dir.join(&filename);
513
514 if path.exists() {
515 return Ok(path);
516 }
517
518 let variations = [
520 format!("bc5d_{:03}.bin", caliber_int),
521 format!("bc5d_0{}.bin", caliber_int),
522 ];
523
524 for var in &variations {
525 let var_path = table_dir.join(var);
526 if var_path.exists() {
527 return Ok(var_path);
528 }
529 }
530
531 Err(Bc5dError::TableNotFound(caliber))
532}
533
534fn read_u32<R: Read>(reader: &mut R) -> Result<u32, std::io::Error> {
537 let mut buf = [0u8; 4];
538 reader.read_exact(&mut buf)?;
539 Ok(u32::from_le_bytes(buf))
540}
541
542fn read_u64<R: Read>(reader: &mut R) -> Result<u64, std::io::Error> {
543 let mut buf = [0u8; 8];
544 reader.read_exact(&mut buf)?;
545 Ok(u64::from_le_bytes(buf))
546}
547
548fn read_f32<R: Read>(reader: &mut R) -> Result<f32, std::io::Error> {
549 let mut buf = [0u8; 4];
550 reader.read_exact(&mut buf)?;
551 Ok(f32::from_le_bytes(buf))
552}
553
554fn read_f32_array<R: Read>(reader: &mut R, count: usize) -> Result<Vec<f32>, std::io::Error> {
555 const MAX_ELEMS: usize = 64_000_000; if count > MAX_ELEMS {
559 return Err(std::io::Error::new(
560 std::io::ErrorKind::InvalidData,
561 "f32 array length too large",
562 ));
563 }
564 let byte_len = count.checked_mul(4).ok_or_else(|| {
565 std::io::Error::new(std::io::ErrorKind::InvalidData, "f32 array length overflow")
566 })?;
567 let mut data = vec![0f32; count];
568 let mut buf = vec![0u8; byte_len];
569 reader.read_exact(&mut buf)?;
570
571 for (i, chunk) in buf.chunks_exact(4).enumerate() {
572 data[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
573 }
574
575 Ok(data)
576}
577
578pub(crate) fn crc32_ieee(data: &[u8]) -> u32 {
580 const TABLE: [u32; 256] = make_crc32_table();
581 let mut crc = 0xFFFFFFFFu32;
582 for &byte in data {
583 let idx = ((crc ^ byte as u32) & 0xFF) as usize;
584 crc = (crc >> 8) ^ TABLE[idx];
585 }
586 !crc
587}
588
589const fn make_crc32_table() -> [u32; 256] {
590 const POLY: u32 = 0xEDB88320;
591 let mut table = [0u32; 256];
592 let mut i = 0;
593 while i < 256 {
594 let mut crc = i as u32;
595 let mut j = 0;
596 while j < 8 {
597 if crc & 1 != 0 {
598 crc = (crc >> 1) ^ POLY;
599 } else {
600 crc >>= 1;
601 }
602 j += 1;
603 }
604 table[i] = crc;
605 i += 1;
606 }
607 table
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 fn create_test_table() -> Bc5dTable {
615 let weight_bins = vec![100.0, 150.0, 200.0];
617 let bc_bins = vec![0.3, 0.4, 0.5];
618 let muzzle_vel_bins = vec![2500.0, 3000.0];
619 let current_vel_bins = vec![1000.0, 2000.0, 3000.0];
620 let num_drag_types = 2;
621
622 let total = num_drag_types * weight_bins.len() * bc_bins.len() * muzzle_vel_bins.len() * current_vel_bins.len();
624 let mut data = vec![1.0f32; total];
625
626 data[0] = 0.95; data[total - 1] = 1.05; Bc5dTable {
633 caliber: 0.308,
634 data,
635 weight_bins,
636 bc_bins,
637 muzzle_vel_bins,
638 current_vel_bins,
639 num_drag_types,
640 version: 2,
641 api_version: "test".to_string(),
642 timestamp: 0,
643 }
644 }
645
646 #[test]
647 fn test_interp_idx_in_range() {
648 let table = create_test_table();
649
650 let (idx, weight) = table.interp_idx(125.0, &table.weight_bins);
652 assert_eq!(idx, 0);
653 assert!((weight - 0.5).abs() < 0.01);
654
655 let (idx, weight) = table.interp_idx(150.0, &table.weight_bins);
657 assert_eq!(idx, 0);
658 assert!((weight - 1.0).abs() < 0.01);
659 }
660
661 #[test]
662 fn test_interp_idx_out_of_range() {
663 let table = create_test_table();
664
665 let (idx, weight) = table.interp_idx(50.0, &table.weight_bins);
667 assert_eq!(idx, 0);
668 assert_eq!(weight, 0.0);
669
670 let (idx, weight) = table.interp_idx(250.0, &table.weight_bins);
672 assert_eq!(idx, 1); assert_eq!(weight, 1.0);
674 }
675
676 #[test]
677 fn test_lookup_returns_valid_range() {
678 let table = create_test_table();
679
680 let correction = table.lookup(150.0, 0.4, 2750.0, 2000.0, "G1");
681 assert!(correction >= 0.5 && correction <= 1.5);
682
683 let correction = table.lookup(150.0, 0.4, 2750.0, 2000.0, "G7");
684 assert!(correction >= 0.5 && correction <= 1.5);
685 }
686
687 #[test]
688 fn test_effective_bc() {
689 let table = create_test_table();
690
691 let base_bc = 0.4;
692 let effective = table.get_effective_bc(150.0, base_bc, 2750.0, 2000.0, "G1");
693
694 assert!(effective >= base_bc * 0.5 && effective <= base_bc * 1.5);
696 }
697
698 #[test]
699 fn test_caliber_to_key() {
700 assert_eq!(caliber_to_key(0.308), 308);
701 assert_eq!(caliber_to_key(0.224), 224);
702 assert_eq!(caliber_to_key(0.338), 338);
703 }
704
705 #[test]
706 fn test_table_metadata() {
707 let table = create_test_table();
708 assert!((table.caliber() - 0.308).abs() < 0.001);
709 assert_eq!(table.version(), 2);
710 assert_eq!(table.api_version(), "test");
711 }
712
713 #[test]
714 fn test_crc32() {
715 let data = b"123456789";
717 let crc = crc32_ieee(data);
718 assert_eq!(crc, 0xCBF43926);
719 }
720}