//! Core data types and error definitions. //! //! Contains the fundamental types shared across the entire application: //! car records, error types, and composite key definitions. use serde::Deserialize; /// Custom error type for the Top Drives application. /// /// Provides a unified error handling strategy across all modules, /// replacing the mix of `Result<(), String>`, `std::io::Result`, and /// silent `eprintln!` + early return patterns. #[derive(Debug, thiserror::Error)] pub enum AppError { /// An I/O error occurred (file not found, permission denied, etc.) #[error("I/O error: {0}")] Io(#[from] std::io::Error), /// A network error occurred during data fetching #[error("Network error: {0}")] Network(String), /// A JSON parsing error in a specific file #[error("Parse error in {file}: {reason}")] Parse { /// The file that failed to parse file: String, /// The reason for the parse failure reason: String, }, /// A required data file is missing #[error("Missing data file: {0}")] MissingFile(String), /// An internal runtime error (e.g., semaphore closed, task join failure) #[error("Internal error: {0}")] Internal(String), } /// Core car information structure containing basic car metadata. /// Used throughout the application to represent car entities. #[derive(Debug, Clone, Deserialize)] pub struct Car { /// Unique car identifier used in the records database pub rid: String, /// Car manufacturer/brand name pub brand: String, /// Car model name (without brand) #[serde(rename = "onlyName")] pub only_name: String, /// Car model year pub year: u16, /// Rarity level (10-113, affects performance brackets) pub rq: u8, /// Whether this car is only available as a prize car #[serde(default)] pub prize: bool, /// Game-specific unique identifier for the car pub guid: String, } /// Represents the best performing car record for a specific track, RQ, and condition. /// Contains all necessary information to compare car performance. #[derive(Debug, Clone)] pub struct BestCarRecord { /// Car identifier from the records database pub car_rid: String, /// Car manufacturer/brand name pub brand: String, /// Car model name (without brand) pub only_name: String, /// Car model year pub year: u16, /// Rarity level of this specific car configuration pub rq: u8, /// Original tune configuration string from JSON (e.g., "111", "221", "233") pub config: String, /// Normalized config mapped to the closest final build ("323", "233", or "332"). /// Final builds are permutations of digits summing to 8. Non-standard configs /// are normalized by repeatedly incrementing the smallest digit until the sum is 8. pub normalized_config: String, /// Track time in seconds (lower is better) pub time: f64, /// Whether the current user owns this car pub owned: bool, /// Whether this car is only available as a prize car pub prize: bool, /// Game-specific unique identifier for the car pub guid: String, } /// Type alias for the composite key used to identify unique track conditions. /// Consists of: (Track ID, RQ Level, Surface Condition) pub type TripletKey = (String, u8, String); /// Normalizes a tune configuration to the closest "final build". /// /// The three final builds are "323", "233", and "332" (all permutations of digits /// summing to 8). For any non-standard config, this function repeatedly increments /// the smallest digit until the digit sum reaches 8, producing one of the three /// final builds. /// /// Expects bare digit strings (e.g., `"233"`, `"111"`). Version prefixes (e.g., /// `"v2c_233"`) are stripped at read time by the parsing layer before reaching /// this function. /// /// If the config cannot be parsed (wrong length, non-digit characters, or digit /// sum exceeding 8), the original config string is returned unchanged. /// /// # Examples /// /// ``` /// use top_drives::normalize_to_final_build; /// assert_eq!(normalize_to_final_build("323"), "323"); /// assert_eq!(normalize_to_final_build("111"), "332"); /// assert_eq!(normalize_to_final_build("221"), "332"); /// assert_eq!(normalize_to_final_build("122"), "332"); /// assert_eq!(normalize_to_final_build("113"), "323"); /// assert_eq!(normalize_to_final_build("233"), "233"); /// assert_eq!(normalize_to_final_build("332"), "332"); /// assert_eq!(normalize_to_final_build("879"), "879"); // maxed: returned as-is /// ``` pub fn normalize_to_final_build(config: &str) -> String { let bytes = config.as_bytes(); if bytes.len() != 3 || !bytes.iter().all(|&b| b.is_ascii_digit()) { return config.to_string(); } let mut digits = [0u8; 3]; for (i, &b) in bytes.iter().enumerate() { digits[i] = b - b'0'; } let sum: i32 = digits.iter().map(|&d| d as i32).sum(); if sum >= 8 { // Already a final build (sum == 8) or maxed (sum > 8) — return as-is. return config.to_string(); } let mut deficit = 8 - sum; while deficit > 0 { // Find the position of the smallest digit (first occurrence on tie) let min_pos = (0..3).min_by_key(|&i| digits[i]).unwrap(); digits[min_pos] += 1; deficit -= 1; } digits.iter().map(|&d| (b'0' + d) as char).collect() }