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