top_drives/src/records.rs
Jan-Peter von Hunnius bf21d3d1f6 feat: add normalized config tracking and improve command
- Add normalize_to_final_build() to map tune configs to final builds (323/233/332)
- Add normalized_config field to BestCarRecord
- Change CarCounts.by_rid key from String to (String, String) for per-config tracking
- Change CarCounts.guid_to_rid to HashMap<String, HashSet<(String, String)>>
- Extract owned tune configs from my_cars.json (engineMajor/weightMajor/chassisMajor)
- Add improve command: owned cars not yet at final build, sorted by track count
- Strip version prefixes (v2c_/v1c_) from config keys at parse time
- Update dedup check in update_best_car to consider normalized_config
- Add atomic_write helper for safe CSV output
- Delete completed plans/code-review-v2.md
- Apply rustfmt formatting across codebase
2026-05-11 16:23:04 +02:00

238 lines
7.8 KiB
Rust

//! # Top Drives Records Analyzer
//!
//! A comprehensive Rust-based tool for analyzing car performance data in the Top Drives racing game.
//! This module provides the CLI entry point and delegates to submodules for parsing, output, and
//! command handling.
//!
//! ## Module Structure
//!
//! - [`commands`] — Command handlers (fetch, best, trash, wishlist, details, track)
//! - [`parsing`] — Core timing data parsing and car database loading
//! - [`output`] — CSV output utilities and sorting helpers
//!
//! ## Key Features
//!
//! - Fetches car and track data from GitHub repository
//! - Parses JSON timing files for performance analysis
//! - Identifies best performing cars across all tracks and conditions
//! - Compares user collection against optimal car lists
//! - Provides track-specific performance recommendations
//! - Supports multiple track conditions (Dry, Wet, Dirt, Snow, etc.)
//!
//! ## Data Flow
//!
//! 1. **Fetch Phase**: Downloads car database and track definitions
//! 2. **Parse Phase**: Processes JSON timing files for each car
//! 3. **Analysis Phase**: Compares times across RQ ranges and conditions
//! 4. **Output Phase**: Writes results to CSV files
//!
//! ## Dependencies
//!
//! - `reqwest`: HTTP client for fetching data from GitHub
//! - `serde_json`: JSON parsing for car and track data
//! - `tokio`: Asynchronous runtime for concurrent processing
//! - `futures`: Future utilities for handling async operations
//! - `clap`: Command-line argument parsing
mod commands;
mod output;
mod parsing;
use clap::{Parser, Subcommand};
use top_drives::AppError;
// ---------------------------------------------------------------------------
// Constants (CLI defaults)
// ---------------------------------------------------------------------------
/// Default path for the user's car collection export.
const DEFAULT_MY_CARS_PATH: &str = "my_cars.json";
/// Default path for the car database file.
const DEFAULT_CARS_DB_PATH: &str = "cars_repo.json";
/// Default directory for per-car timing JSON files.
const DEFAULT_TIMES_DIR: &str = "times";
/// Default path for the tracks database file.
const DEFAULT_TRACKS_DB_PATH: &str = "tracks_repo.json";
/// Default directory for output CSV files.
const DEFAULT_OUTPUT_DIR: &str = ".";
// ---------------------------------------------------------------------------
// CLI definition
// ---------------------------------------------------------------------------
/// Command-line interface for the Top Drives Records Analyzer.
#[derive(Parser)]
#[command(name = "records", about = "Top Drives Records Analyzer", version)]
struct Cli {
/// Path to the user's car collection export file
#[arg(long, default_value = DEFAULT_MY_CARS_PATH)]
my_cars: String,
/// Path to the car database file
#[arg(long, default_value = DEFAULT_CARS_DB_PATH)]
cars_db: String,
/// Directory containing per-car timing JSON files
#[arg(long, default_value = DEFAULT_TIMES_DIR)]
times_dir: String,
/// Path to the tracks database file
#[arg(long, default_value = DEFAULT_TRACKS_DB_PATH)]
tracks_db: String,
/// Directory for output CSV files
#[arg(long, default_value = DEFAULT_OUTPUT_DIR)]
output_dir: String,
#[command(subcommand)]
command: Commands,
}
/// Available subcommands for car performance analysis.
#[derive(Subcommand)]
enum Commands {
/// Download latest car and track data from repository
Fetch,
/// Generate best.csv and very_best.csv
Best {
/// Minimum number of tracks a car must be best on to appear in very_best.csv (default: 10)
#[arg(long, default_value_t = 10)]
min_count: usize,
},
/// Generate trash.csv (owned cars not best on any track)
Trash,
/// Generate wishlist.csv, add.csv, and remove.csv
Wishlist {
/// Minimum number of tracks a car must be best on to qualify (default: 10)
#[arg(long, default_value_t = 10)]
min_count: usize,
/// Maximum number of cars on the wishlist (default: 300)
#[arg(long, default_value_t = 300)]
max_wishlist: usize,
},
/// Generate all output files in a single pass
All {
/// Minimum number of tracks a car must be best on to qualify for very_best/wishlist (default: 10)
#[arg(long, default_value_t = 10)]
min_count: usize,
/// Maximum number of cars on the wishlist (default: 300)
#[arg(long, default_value_t = 300)]
max_wishlist: usize,
},
/// Generate details.csv with all timing data
Details,
/// Show best car for a specific track/condition/RQ combination
Track { track: String, condition: String, rq: u8 },
/// Generate improve.csv (owned cars not in final config, sorted by best tracks)
Improve,
}
// ---------------------------------------------------------------------------
// Main entry point
// ---------------------------------------------------------------------------
/// Main entry point for the Top Drives Records Analyzer
///
/// Parses command line arguments using `clap` and executes the requested analysis operation.
/// Supports multiple subcommands for different types of car performance analysis.
///
/// # Example Usage
///
/// ```bash
/// cargo run --bin records -- best
/// cargo run --bin records -- track "Laguna Seca" "Dry" "85"
/// cargo run --bin records -- --cars-db /data/cars.json --times-dir /data/times best
/// ```
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
async fn run() -> Result<(), AppError> {
let cli = Cli::parse();
// Ensure the output directory exists before dispatching any command.
// create_dir_all(".") is a no-op, so no special-casing needed.
std::fs::create_dir_all(&cli.output_dir)?;
match cli.command {
Commands::Fetch => {
commands::fetch::run_fetch(&cli.cars_db, &cli.tracks_db, &cli.times_dir).await?;
}
Commands::Best { min_count } => {
commands::best::run_best(
&cli.my_cars,
&cli.cars_db,
&cli.times_dir,
&cli.output_dir,
min_count,
)
.await?;
}
Commands::Trash => {
commands::trash::run_trash(&cli.my_cars, &cli.cars_db, &cli.times_dir, &cli.output_dir)
.await?;
}
Commands::Wishlist { min_count, max_wishlist } => {
commands::wishlist::run_wishlist(
&cli.my_cars,
&cli.cars_db,
&cli.times_dir,
&cli.output_dir,
min_count,
max_wishlist,
)
.await?;
}
Commands::All { min_count, max_wishlist } => {
commands::all::run_all(
&cli.my_cars,
&cli.cars_db,
&cli.times_dir,
&cli.output_dir,
min_count,
max_wishlist,
)
.await?;
}
Commands::Details => {
commands::details::run_details(
&cli.my_cars,
&cli.cars_db,
&cli.times_dir,
&cli.output_dir,
)
.await?;
}
Commands::Track { track, condition, rq } => {
commands::track::run_track(
&cli.my_cars,
&cli.cars_db,
&cli.times_dir,
&cli.output_dir,
&track,
&condition,
rq,
)
.await?;
}
Commands::Improve => {
commands::improve::run_improve(
&cli.my_cars,
&cli.cars_db,
&cli.times_dir,
&cli.output_dir,
)
.await?;
}
}
Ok(())
}