docs: update documentation for normalized config and improve command

- Add improve command to all command tables and usage sections
- Add normalized_config field to BestCarRecord struct documentation
- Update CarCounts.by_rid type from String to (String, String)
- Update CarCounts.guid_to_rid type to HashMap<String, HashSet<(String, String)>>
- Add owned_configs field to MyCarsData and CarCounts docs
- Add normalize_to_final_build() to API docs
- Update CSV format examples to include FinalBuild column
- Update my_cars.json structure with engineMajor/weightMajor/chassisMajor
- Update timing file example with v2c_ version prefix
- Add version prefix stripping to data transformation docs
- Update CHANGELOG.md with Unreleased section
- Fix compare_records description to include normalized config sorting
This commit is contained in:
Jan-Peter von Hunnius 2026-05-11 16:58:01 +02:00
parent bf21d3d1f6
commit 2b0602d564
6 changed files with 204 additions and 84 deletions

View file

@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- New `improve` command that generates `improve.csv` — owned cars not yet at a final tune build, sorted by track count
- `normalized_config` field on `BestCarRecord` — maps any tune config to its closest final build (323/233/332)
- `normalize_to_final_build()` function — normalizes tune configs by incrementing the smallest digit until sum reaches 8
- `owned_configs` field on `MyCarsData` — extracts actual player tune config (engineMajor/weightMajor/chassisMajor) from `my_cars.json`
- `owned_configs` field on `CarCounts` — passes player config data through to output functions
- `atomic_write()` helper — writes to a temporary file first, then renames on success (no partial CSV files)
- `empty_my_data()` test helper — creates an empty owned configs map for tests
- Version prefix stripping (`v2c_`/`v1c_`) for config keys at parse time
- `FinalBuild` column in `best.csv`, `very_best.csv`, `wishlist.csv`, `add.csv`, `remove.csv` output files
- Dedup check in `update_best_car` now considers `normalized_config` to prevent same car+config duplicates
### Changed
- `CarCounts.by_rid` key changed from `String` to `(String, String)` for per-config tracking
- `CarCounts.guid_to_rid` changed from `HashMap<String, String>` to `HashMap<String, HashSet<(String, String)>>`
- `compute_car_counts()` now accepts `owned_configs` parameter
- `compare_records()` now sorts by normalized config as secondary key (after RQ, before brand)
- `load_my_cars_data()` now extracts tune config data for locked cards
- CSV output files now include `FinalBuild` column showing the normalized config
- `update_best_car()` tie detection now checks both `car_rid` and `normalized_config`
- Applied `rustfmt` formatting across the entire codebase
### Removed
- `plans/code-review-v2.md` (completed action items)
## [0.3.0] - 2026-05-07 ## [0.3.0] - 2026-05-07
### Added ### Added

View file

@ -7,6 +7,7 @@ A comprehensive Rust-based tool for analyzing car performance data in the Top Dr
- **Best Car Analysis**: Find the best performing cars across all tracks and conditions - **Best Car Analysis**: Find the best performing cars across all tracks and conditions
- **Wishlist Optimization**: Identify cars to add to or remove from your wishlist - **Wishlist Optimization**: Identify cars to add to or remove from your wishlist
- **Collection Cleanup**: Identify cars that can be safely removed from your collection - **Collection Cleanup**: Identify cars that can be safely removed from your collection
- **Upgrade Prioritization**: Find owned cars not yet at their final tune build
- **Track-Specific Analysis**: Get detailed performance data for specific tracks and conditions - **Track-Specific Analysis**: Get detailed performance data for specific tracks and conditions
- **RQ-Based Filtering**: Filter results by car rarity (RQ) levels - **RQ-Based Filtering**: Filter results by car rarity (RQ) levels
- **Multiple Surface Types**: Support for Dry, Wet, Dirt, Snow, and other track conditions - **Multiple Surface Types**: Support for Dry, Wet, Dirt, Snow, and other track conditions
@ -75,6 +76,11 @@ A comprehensive Rust-based tool for analyzing car performance data in the Top Dr
cargo run --bin records -- wishlist cargo run --bin records -- wishlist
``` ```
6. **Upgrade Prioritization**: Find cars that need upgrading
```bash
cargo run --bin records -- improve
```
## 📖 Usage Guide ## 📖 Usage Guide
### Basic Commands ### Basic Commands
@ -140,7 +146,7 @@ cargo run --bin records -- wishlist --max-wishlist 500
```bash ```bash
cargo run --bin records -- all cargo run --bin records -- all
``` ```
Generates all 6 CSV files in a single parse pass. Equivalent to running `best`, `trash`, and `wishlist` together, but more efficient. Generates all 7 CSV files in a single parse pass. Equivalent to running `best`, `trash`, `wishlist`, and `improve` together, but more efficient.
**Output Files:** **Output Files:**
``` ```
@ -150,6 +156,7 @@ trash.csv — Owned cars safe to remove
wishlist.csv — Ideal wishlist (very_best unowned, capped) wishlist.csv — Ideal wishlist (very_best unowned, capped)
add.csv — Cars to add to wishlist add.csv — Cars to add to wishlist
remove.csv — Cars to remove from wishlist remove.csv — Cars to remove from wishlist
improve.csv — Owned cars not yet at final build config
``` ```
Supports both `--min-count N` (default 10) and `--max-wishlist M` (default 300): Supports both `--min-count N` (default 10) and `--max-wishlist M` (default 300):
@ -197,9 +204,10 @@ The analyzer supports multiple track conditions:
| `best` | Generate `best.csv` and `very_best.csv` | `[--min-count N]` | | `best` | Generate `best.csv` and `very_best.csv` | `[--min-count N]` |
| `trash` | Generate `trash.csv` (owned cars safe to remove) | None | | `trash` | Generate `trash.csv` (owned cars safe to remove) | None |
| `wishlist` | Generate `wishlist.csv`, `add.csv`, `remove.csv` | `[--min-count N] [--max-wishlist M]` | | `wishlist` | Generate `wishlist.csv`, `add.csv`, `remove.csv` | `[--min-count N] [--max-wishlist M]` |
| `all` | Generate all 6 CSV files in a single pass | `[--min-count N] [--max-wishlist M]` | | `all` | Generate all 7 CSV files in a single pass | `[--min-count N] [--max-wishlist M]` |
| `details` | Print detailed timing data for all tracks (stdout) | None | | `details` | Print detailed timing data for all tracks (stdout) | None |
| `track` | Print best car for specific track (stdout) | `<track_name> <condition> <rq>` | | `track` | Print best car for specific track (stdout) | `<track_name> <condition> <rq>` |
| `improve` | Generate `improve.csv` (owned cars not at final build) | None |
### Flags ### Flags
@ -226,12 +234,13 @@ This generates the following output files (all sorted by car RQ):
| File | Format | Description | | File | Format | Description |
|------|--------|-------------| |------|--------|-------------|
| `best.csv` | `RQ,Brand,Model,Year,Count` | All cars best on at least one track | | `best.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | All cars best on at least one track |
| `very_best.csv` | `RQ,Brand,Model,Year,Count` | Cars best on ≥ min_count tracks | | `very_best.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Cars best on ≥ min_count tracks |
| `trash.csv` | `RQ,Brand,Model,Year` | Owned cars not best on any track (RQ<80, not prize) | | `trash.csv` | `RQ,Brand,Model,Year` | Owned cars not best on any track (RQ<80, not prize) |
| `wishlist.csv` | `RQ,Brand,Model,Year,Count` | Ideal wishlist (very_best unowned, capped) | | `wishlist.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Ideal wishlist (very_best unowned, capped) |
| `add.csv` | `RQ,Brand,Model,Year,Count` | Cars to add to wishlist | | `add.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Cars to add to wishlist |
| `remove.csv` | `RQ,Brand,Model,Year,Count` | Cars to remove from wishlist | | `remove.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Cars to remove from wishlist |
| `improve.csv` | `RQ,Brand,Model,Year,Target,Count` | Owned cars not yet at final build config |
### Workflow Steps ### Workflow Steps
@ -241,6 +250,7 @@ This generates the following output files (all sorted by car RQ):
4. **Check** `add.csv` for cars worth adding to your wishlist 4. **Check** `add.csv` for cars worth adding to your wishlist
5. **Clean up** `remove.csv` entries from your wishlist 5. **Clean up** `remove.csv` entries from your wishlist
6. **Sell** cars listed in `trash.csv` that are no longer useful 6. **Sell** cars listed in `trash.csv` that are no longer useful
7. **Upgrade** cars listed in `improve.csv` to their target final build
## 📊 Data Sources ## 📊 Data Sources
@ -318,16 +328,17 @@ struct Car {
#### Performance Records #### Performance Records
```rust ```rust
struct BestCarRecord { struct BestCarRecord {
car_rid: String, // Car identifier car_rid: String, // Car identifier
brand: String, // Manufacturer brand: String, // Manufacturer
only_name: String, // Model name only_name: String, // Model name
year: u16, // Year year: u16, // Year
rq: u8, // Rarity level rq: u8, // Rarity level
config: String, // Tune configuration config: String, // Original tune config from JSON
time: f64, // Track time normalized_config: String, // Normalized final build (323, 233, or 332)
owned: bool, // Whether you own this car time: f64, // Track time
prize: bool, // Whether this is a prize car owned: bool, // Whether you own this car
guid: String, // Game identifier prize: bool, // Whether this is a prize car
guid: String, // Game identifier
} }
``` ```

View file

@ -25,6 +25,7 @@ src/
│ ├── wishlist.rs # Wishlist command: wishlist.csv, add.csv, remove.csv │ ├── wishlist.rs # Wishlist command: wishlist.csv, add.csv, remove.csv
│ ├── details.rs # Details command: details.csv │ ├── details.rs # Details command: details.csv
│ ├── track.rs # Track command: stdout query │ ├── track.rs # Track command: stdout query
│ ├── improve.rs # Improve command: improve.csv (owned cars not at final build)
│ └── all.rs # All command: generates all output files │ └── all.rs # All command: generates all output files
├── parsing/ ├── parsing/
│ ├── mod.rs # Core parsing: parse_car_times, update_best_car, parse_all_times │ ├── mod.rs # Core parsing: parse_car_times, update_best_car, parse_all_times
@ -69,16 +70,17 @@ pub struct Car {
```rust ```rust
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BestCarRecord { pub struct BestCarRecord {
pub car_rid: String, // Car identifier pub car_rid: String, // Car identifier
pub brand: String, // Manufacturer pub brand: String, // Manufacturer
pub only_name: String, // Model name pub only_name: String, // Model name
pub year: u16, // Year pub year: u16, // Year
pub rq: u8, // RQ level pub rq: u8, // RQ level
pub config: String, // Tune config (111, 233, etc.) pub config: String, // Original tune config from JSON (111, 221, 233, etc.)
pub time: f64, // Track time in seconds pub normalized_config: String, // Normalized final build (323, 233, or 332)
pub owned: bool, // User ownership status pub time: f64, // Track time in seconds
pub prize: bool, // Prize car status pub owned: bool, // User ownership status
pub guid: String, // Game identifier pub prize: bool, // Prize car status
pub guid: String, // Game identifier
} }
``` ```
@ -109,21 +111,25 @@ pub struct AnalysisContext {
```rust ```rust
pub struct CarCounts { pub struct CarCounts {
pub by_rid: HashMap<String, (BestCarRecord, usize)>, pub by_rid: HashMap<(String, String), (BestCarRecord, usize)>,
pub guid_to_rid: HashMap<String, String>, pub guid_to_rid: HashMap<String, HashSet<(String, String)>>,
pub car_map: HashMap<String, Car>, pub car_map: HashMap<String, Car>,
pub car_list: Vec<Car>, pub car_list: Vec<Car>,
pub owned_configs: HashMap<String, String>,
} }
``` ```
**Key grouping**: `by_rid` is keyed by `(car_rid, normalized_config)` so different tune configs of the same car are tracked separately. `guid_to_rid` maps each GUID to all `(rid, config)` pairs it appears under.
#### `MyCarsData` #### `MyCarsData`
**Parsed user data from `my_cars.json`** **Parsed user data from `my_cars.json`**
```rust ```rust
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct MyCarsData { pub struct MyCarsData {
pub owned: HashSet<String>, // Owned car GUIDs pub owned: HashSet<String>, // Owned car GUIDs
pub wishlist: HashSet<String>, // Wishlist car GUIDs pub wishlist: HashSet<String>, // Wishlist car GUIDs
pub owned_configs: HashMap<String, String>, // GUID → actual tune config (e.g., "233")
} }
``` ```
@ -183,11 +189,12 @@ pub enum AppError {
- Sorts by: RQ → Brand → Model name - Sorts by: RQ → Brand → Model name
#### `compute_car_counts(ctx: &AnalysisContext, car_list: Vec<Car>) -> CarCounts` #### `compute_car_counts(ctx: &AnalysisContext, car_list: Vec<Car>, owned_configs: HashMap<String, String>) -> CarCounts`
**Aggregates per-car win counts from analysis context** **Aggregates per-car win counts from analysis context**
- Iterates all entries in the `DashMap` - Iterates all entries in the `DashMap`
- Returns `CarCounts` with by_rid, guid_to_rid, car_map, and car_list - Groups by `(car_rid, normalized_config)` for per-config tracking
- Returns `CarCounts` with by_rid, guid_to_rid, car_map, car_list, and owned_configs
#### `escape_csv_field(field: &str) -> String` #### `escape_csv_field(field: &str) -> String`
**Escapes a field for safe CSV inclusion** **Escapes a field for safe CSV inclusion**
@ -195,6 +202,14 @@ pub enum AppError {
- Wraps in quotes if field contains comma, quote, newline, or carriage return - Wraps in quotes if field contains comma, quote, newline, or carriage return
- Doubles internal quotes per CSV spec - Doubles internal quotes per CSV spec
#### `normalize_to_final_build(config: &str) -> String`
**Normalizes a tune configuration to the closest "final build"**
- The three final builds are `"323"`, `"233"`, and `"332"` (permutations of digits summing to 8)
- For non-standard configs, repeatedly increments the smallest digit until sum reaches 8
- Returns the original string unchanged if config cannot be parsed or sum already ≥ 8
- Version prefixes (`v2c_`, `v1c_`) are stripped at parse time before reaching this function
--- ---
## Records Binary (`src/records.rs`) ## Records Binary (`src/records.rs`)
@ -221,6 +236,7 @@ CLI entry point. Defines command-line arguments and dispatches to command handle
| `all` | Generate all output files in a single pass | `--min-count N`, `--max-wishlist M` | | `all` | Generate all output files in a single pass | `--min-count N`, `--max-wishlist M` |
| `details` | Generate `details.csv` with all timing data | None | | `details` | Generate `details.csv` with all timing data | None |
| `track` | Show best car for specific track/condition/RQ | `<track> <condition> <rq>` | | `track` | Show best car for specific track/condition/RQ | `<track> <condition> <rq>` |
| `improve` | Generate `improve.csv` (owned cars not at final build) | None |
--- ---
@ -309,7 +325,7 @@ Loads car database, parses all timing files concurrently, and computes car count
#### `write_best_csv(counts)`, `write_very_best_csv(counts, min_count)` #### `write_best_csv(counts)`, `write_very_best_csv(counts, min_count)`
**Generate best car CSV files** **Generate best car CSV files**
- Format: `RQ,Brand,Model,Year,Count` - Format: `RQ,Brand,Model,Year,FinalBuild,Count`
- Fields are CSV-escaped - Fields are CSV-escaped
#### `write_trash_csv(counts, owned)` #### `write_trash_csv(counts, owned)`
@ -323,7 +339,16 @@ Loads car database, parses all timing files concurrently, and computes car count
- Computes ideal wishlist once - Computes ideal wishlist once
- Derives all three files from it - Derives all three files from it
- Format: `RQ,Brand,Model,Year,Count` - Format: `RQ,Brand,Model,Year,FinalBuild,Count`
#### `write_improve_csv(counts) -> Result<usize, AppError>`
**Generate improve CSV file**
- Lists owned cars whose actual player config is not yet a final build (323/233/332)
- Uses actual player config from `my_cars.json`, not timing data configs
- Format: `RQ,Brand,Model,Year,Target,Count`
- Sorted by track count descending (most impactful upgrades first)
- Returns the number of cars written
#### `write_details_csv(ctx)` #### `write_details_csv(ctx)`
**Generate comprehensive timing data CSV** **Generate comprehensive timing data CSV**
@ -347,4 +372,4 @@ Loads car database, parses all timing files concurrently, and computes car count
#### `compare_records(a: &BestCarRecord, b: &BestCarRecord) -> std::cmp::Ordering` #### `compare_records(a: &BestCarRecord, b: &BestCarRecord) -> std::cmp::Ordering`
**Comparison for BestCarRecord sorting** **Comparison for BestCarRecord sorting**
- Sorts by: RQ → Brand → Model name - Sorts by: RQ → Normalized Config → Brand → Model name

View file

@ -14,13 +14,13 @@ The Top Drives Records Analyzer is a Rust CLI tool for analyzing car performance
| Module | File | Key Exports | | Module | File | Key Exports |
|--------|------|-------------| |--------|------|-------------|
| `types` | `src/types.rs` | `AppError`, `BestCarRecord`, `Car`, `TripletKey` | | `types` | `src/types.rs` | `AppError`, `BestCarRecord`, `Car`, `TripletKey`, `normalize_to_final_build` |
| `context` | `src/context.rs` | `AnalysisContext`, `CarCounts`, `compute_car_counts` | | `context` | `src/context.rs` | `AnalysisContext`, `CarCounts`, `compute_car_counts` |
| `user_data` | `src/user_data.rs` | `MyCarsData`, `load_my_cars_data` | | `user_data` | `src/user_data.rs` | `MyCarsData`, `load_my_cars_data` |
| `translate` | `src/translate.rs` | `translate_track_name`, `translate_track_type` | | `translate` | `src/translate.rs` | `translate_track_name`, `translate_track_type` |
| `http` | `src/http.rs` | `get_file` | | `http` | `src/http.rs` | `get_file` |
| `csv_util` | `src/csv_util.rs` | `compare_cars`, `escape_csv_field` | | `csv_util` | `src/csv_util.rs` | `compare_cars`, `escape_csv_field` |
| `test_utils` | `src/test_utils.rs` | `make_record`, `make_car` (test helpers) | | `test_utils` | `src/test_utils.rs` | `make_record`, `make_car`, `empty_my_data` (test helpers) |
#### Error Handling #### Error Handling
All public functions return `Result<T, AppError>` where `AppError` is a unified error type: All public functions return `Result<T, AppError>` where `AppError` is a unified error type:
@ -58,6 +58,7 @@ All public functions return `Result<T, AppError>` where `AppError` is a unified
| `wishlist.rs` | `wishlist` | Generates `wishlist.csv`, `add.csv`, `remove.csv` | | `wishlist.rs` | `wishlist` | Generates `wishlist.csv`, `add.csv`, `remove.csv` |
| `details.rs` | `details` | Generates `details.csv` with all timing data | | `details.rs` | `details` | Generates `details.csv` with all timing data |
| `track.rs` | `track` | Prints best car for a specific track/condition/RQ | | `track.rs` | `track` | Prints best car for a specific track/condition/RQ |
| `improve.rs` | `improve` | Generates `improve.csv` (owned cars not yet at final build) |
| `all.rs` | `all` | Generates all output files in a single analysis pass | | `all.rs` | `all` | Generates all output files in a single analysis pass |
#### Shared Helpers (`src/commands/mod.rs`) #### Shared Helpers (`src/commands/mod.rs`)
@ -86,14 +87,16 @@ All public functions return `Result<T, AppError>` where `AppError` is a unified
**CSV output utilities** and sorting helpers. **CSV output utilities** and sorting helpers.
- `write_sorted_csv()` — Generic sorted CSV writer (index-based sorting to avoid cloning) - `atomic_write()` — Writes to a temporary file first, then renames on success (no partial files)
- `write_sorted_csv()` — Generic sorted CSV writer (index-based sorting to avoid cloning, uses `atomic_write`)
- `write_best_csv()`, `write_very_best_csv()` — Best car CSV output - `write_best_csv()`, `write_very_best_csv()` — Best car CSV output
- `write_trash_csv()` — Trash car CSV output - `write_trash_csv()` — Trash car CSV output
- `write_details_csv()` — Comprehensive timing data CSV output - `write_details_csv()` — Comprehensive timing data CSV output
- `write_wishlist_files()` — Wishlist, add, and remove CSV output - `write_wishlist_files()` — Wishlist, add, and remove CSV output
- `write_improve_csv()` — Improve car CSV output (owned cars not yet at final build)
- `print_best_car()` — Stdout output for track-specific query - `print_best_car()` — Stdout output for track-specific query
- `compute_ideal_wishlist()` — Ideal wishlist computation - `compute_ideal_wishlist()` — Ideal wishlist computation
- `compare_records()` — Sorting comparator for `BestCarRecord` - `compare_records()` — Sorting comparator for `BestCarRecord` (by RQ, normalized config, brand, model)
#### Data Flow #### Data Flow
@ -146,16 +149,17 @@ struct Car {
#### BestCarRecord #### BestCarRecord
```rust ```rust
struct BestCarRecord { struct BestCarRecord {
car_rid: String, // Car identifier car_rid: String, // Car identifier
brand: String, // Manufacturer brand: String, // Manufacturer
only_name: String, // Model name only_name: String, // Model name
year: u16, // Year year: u16, // Year
rq: u8, // RQ level rq: u8, // RQ level
config: String, // Tune config (111, 233, etc.) config: String, // Original tune config from JSON (111, 221, 233, etc.)
time: f64, // Track time (seconds) normalized_config: String, // Normalized final build (323, 233, or 332)
owned: bool, // User ownership status time: f64, // Track time (seconds)
prize: bool, // Prize car status owned: bool, // User ownership status
guid: String, // Game identifier prize: bool, // Prize car status
guid: String, // Game identifier
} }
``` ```
@ -175,15 +179,17 @@ struct AnalysisContext {
### CarCounts ### CarCounts
```rust ```rust
struct CarCounts { struct CarCounts {
by_rid: HashMap<String, (BestCarRecord, usize)>, by_rid: HashMap<(String, String), (BestCarRecord, usize)>,
guid_to_rid: HashMap<String, String>, guid_to_rid: HashMap<String, HashSet<(String, String)>>,
car_map: HashMap<String, Car>, car_map: HashMap<String, Car>,
car_list: Vec<Car>, car_list: Vec<Car>,
owned_configs: HashMap<String, String>,
} }
``` ```
**Purpose**: Aggregated per-car win counts computed once from `AnalysisContext` **Purpose**: Aggregated per-car win counts computed once from `AnalysisContext`
**Usage**: Shared across all output functions to avoid redundant computation **Usage**: Shared across all output functions to avoid redundant computation
**Key grouping**: `by_rid` is keyed by `(car_rid, normalized_config)` so different tune configs of the same car are tracked separately.
## Algorithm Details ## Algorithm Details
@ -241,7 +247,8 @@ Multiple validation layers ensure data quality:
├── trash.csv # Owned cars safe to remove ├── trash.csv # Owned cars safe to remove
├── wishlist.csv # Ideal wishlist (very_best unowned, capped) ├── wishlist.csv # Ideal wishlist (very_best unowned, capped)
├── add.csv # Cars to add to wishlist ├── add.csv # Cars to add to wishlist
└── remove.csv # Cars to remove from wishlist ├── remove.csv # Cars to remove from wishlist
└── improve.csv # Owned cars not yet at final build config
``` ```
## Performance Characteristics ## Performance Characteristics

View file

@ -56,11 +56,17 @@ This document describes the JSON data structures used by the Top Drives Records
"playerDeck": [ "playerDeck": [
{ {
"cardId": "card_ferrari_f40_1987", "cardId": "card_ferrari_f40_1987",
"locked": true "locked": true,
"engineMajor": 3,
"weightMajor": 2,
"chassisMajor": 3
}, },
{ {
"cardId": "card_mclaren_f1_1993", "cardId": "card_mclaren_f1_1993",
"locked": false "locked": false,
"engineMajor": 2,
"weightMajor": 3,
"chassisMajor": 3
} }
] ]
} }
@ -73,11 +79,16 @@ This document describes the JSON data structures used by the Top Drives Records
| `playerDeck` | Array | List of user's cards | | `playerDeck` | Array | List of user's cards |
| `cardId` | String | Unique card identifier | | `cardId` | String | Unique card identifier |
| `locked` | Boolean | Whether card is owned (true = owned) | | `locked` | Boolean | Whether card is owned (true = owned) |
| `engineMajor` | Number | Engine tune level (optional) |
| `weightMajor` | Number | Weight tune level (optional) |
| `chassisMajor` | Number | Chassis tune level (optional) |
**Processing Logic**: **Processing Logic**:
- Only `locked: true` cards are considered owned - Only `locked: true` cards are considered owned
- `cardId` values are matched against car database GUIDs - `cardId` values are matched against car database GUIDs
- Used to mark cars as "owned" in analysis results - Used to mark cars as "owned" in analysis results
- Tune config extracted as `"{engineMajor}{weightMajor}{chassisMajor}"` (e.g., `"323"`)
- Cards with incomplete tune data are skipped for the improve command with a warning
### 3. Individual Car Timing Files (`times/{rid}.json`) ### 3. Individual Car Timing Files (`times/{rid}.json`)
@ -90,7 +101,7 @@ This document describes the JSON data structures used by the Top Drives Records
{ {
"users": ["Player1", "Player2"], "users": ["Player1", "Player2"],
"data": { "data": {
"111": { "v2c_111": {
"times": { "times": {
"laguna_a00": { "laguna_a00": {
"t": 112.45, "t": 112.45,
@ -246,7 +257,12 @@ let cars: Vec<Car> = serde_json::from_str(&content)?;
// Parse individual car timing file // Parse individual car timing file
let json_data: Value = serde_json::from_str(&file_content)?; let json_data: Value = serde_json::from_str(&file_content)?;
if let Some(configurations) = json_data["data"].as_object() { if let Some(configurations) = json_data["data"].as_object() {
for (config, entry) in configurations { for (raw_config, entry) in configurations {
// Strip version prefix (e.g., "v2c_" from "v2c_233")
let config = raw_config
.strip_prefix("v2c_")
.or_else(|| raw_config.strip_prefix("v1c_"))
.unwrap_or(raw_config);
if let Some(times) = entry["times"].as_object() { if let Some(times) = entry["times"].as_object() {
for (track_id, details) in times { for (track_id, details) in times {
// Process each track time // Process each track time
@ -290,8 +306,10 @@ BestCarRecord {
year: car.year, year: car.year,
rq: car.rq, rq: car.rq,
config: config.clone(), config: config.clone(),
normalized_config: normalize_to_final_build(&config),
time, time,
owned: my_cars.contains(&car.guid), owned: my_cars.contains(&car.guid),
prize: car.prize,
guid: car.guid.clone(), guid: car.guid.clone(),
} }
``` ```

View file

@ -86,18 +86,18 @@ cargo run --bin records -- best --min-count 5
`best.csv`: `best.csv`:
```csv ```csv
RQ,Brand,Model,Year,Count RQ,Brand,Model,Year,FinalBuild,Count
85,McLaren,F1,1993,15 85,McLaren,F1,1993,323,15
82,Ferrari,250 GTO,1962,12 82,Ferrari,250 GTO,1962,332,12
80,Lamborghini,Countach,1974,8 80,Lamborghini,Countach,1974,233,8
75,Porsche,911 Turbo,1975,6 75,Porsche,911 Turbo,1975,332,6
``` ```
`very_best.csv`: `very_best.csv`:
```csv ```csv
RQ,Brand,Model,Year,Count RQ,Brand,Model,Year,FinalBuild,Count
85,McLaren,F1,1993,15 85,McLaren,F1,1993,323,15
82,Ferrari,250 GTO,1962,12 82,Ferrari,250 GTO,1962,332,12
``` ```
**Understanding the output:** **Understanding the output:**
@ -105,6 +105,7 @@ RQ,Brand,Model,Year,Count
- **Brand**: Car manufacturer - **Brand**: Car manufacturer
- **Model**: Car model name - **Model**: Car model name
- **Year**: Model year - **Year**: Model year
- **FinalBuild**: Normalized tune configuration (323, 233, or 332)
- **Count**: How many times this car was the fastest across different tracks - **Count**: How many times this car was the fastest across different tracks
**Interpretation:** **Interpretation:**
@ -164,24 +165,24 @@ cargo run --bin records -- wishlist --min-count 5 --max-wishlist 500
`wishlist.csv`: `wishlist.csv`:
```csv ```csv
RQ,Brand,Model,Year,Count RQ,Brand,Model,Year,FinalBuild,Count
90,Pagani,Zonda R,2010,18 90,Pagani,Zonda R,2010,323,18
88,Ferrari,F40,1987,15 88,Ferrari,F40,1987,332,15
87,McLaren,Senna,2018,12 87,McLaren,Senna,2018,233,12
``` ```
`add.csv`: `add.csv`:
```csv ```csv
RQ,Brand,Model,Year,Count RQ,Brand,Model,Year,FinalBuild,Count
90,Pagani,Zonda R,2010,18 90,Pagani,Zonda R,2010,323,18
88,Ferrari,F40,1987,15 88,Ferrari,F40,1987,332,15
``` ```
`remove.csv`: `remove.csv`:
```csv ```csv
RQ,Brand,Model,Year,Count RQ,Brand,Model,Year,FinalBuild,Count
75,BMW,M3,2003,3 75,BMW,M3,2003,332,3
72,Audi,RS6,2002,2 72,Audi,RS6,2002,233,2
``` ```
**Action items:** **Action items:**
@ -196,7 +197,7 @@ RQ,Brand,Model,Year,Count
cargo run --bin records -- all cargo run --bin records -- all
``` ```
**Purpose:** Generates all 6 CSV files in a single parse pass. Equivalent to running `best`, `trash`, and `wishlist` together, but more efficient since data is only parsed once. **Purpose:** Generates all 7 CSV files in a single parse pass. Equivalent to running `best`, `trash`, `wishlist`, and `improve` together, but more efficient since data is only parsed once.
**Flags:** **Flags:**
- `--min-count N` — Minimum number of tracks for "very best" qualification (default: 10) - `--min-count N` — Minimum number of tracks for "very best" qualification (default: 10)
@ -214,6 +215,38 @@ cargo run --bin records -- all --min-count 5 --max-wishlist 500
- `wishlist.csv` — Ideal wishlist (very_best unowned, capped) - `wishlist.csv` — Ideal wishlist (very_best unowned, capped)
- `add.csv` — Cars to add to wishlist - `add.csv` — Cars to add to wishlist
- `remove.csv` — Cars to remove from wishlist - `remove.csv` — Cars to remove from wishlist
- `improve.csv` — Owned cars not yet at final build config
#### Improve Analysis
**Command:**
```bash
cargo run --bin records -- improve
```
**Purpose:** Generates `improve.csv` — owned cars whose actual tune configuration is not yet a final build (323, 233, or 332). Sorted by number of best tracks descending, so the most impactful upgrades appear first.
**Output File:**
`improve.csv`:
```csv
RQ,Brand,Model,Year,Target,Count
90,Pagani,Zonda R,2010,323,18
85,McLaren,F1,1993,332,15
```
**Understanding the output:**
- **RQ**: Car's rarity level
- **Brand**: Car manufacturer
- **Model**: Car model name
- **Year**: Model year
- **Target**: The final build config the player should upgrade to
- **Count**: How many tracks this car is best on (across all its configs)
**Interpretation:**
- Focus on cars with the highest Count first — upgrading them benefits the most tracks
- The Target column shows which of the three final builds (323, 233, 332) to aim for
- Cars already at a final build or maxed config are automatically excluded
#### Detailed Performance Data #### Detailed Performance Data
@ -285,19 +318,20 @@ cargo run --bin records -- fetch && cargo run --bin records -- all
### What `all` Does ### What `all` Does
1. **Parses** all timing data in a single pass 1. **Parses** all timing data in a single pass
2. **Generates** all 6 output CSV files 2. **Generates** all 7 output CSV files
3. **Sorts** all files by car RQ 3. **Sorts** all files by car RQ (and FinalBuild where applicable)
### Output Files ### Output Files
| File | Format | Description | | File | Format | Description |
|------|--------|-------------| |------|--------|-------------|
| `best.csv` | `RQ,Brand,Model,Year,Count` | All cars best on at least one track | | `best.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | All cars best on at least one track |
| `very_best.csv` | `RQ,Brand,Model,Year,Count` | Cars best on ≥ min_count tracks | | `very_best.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Cars best on ≥ min_count tracks |
| `trash.csv` | `RQ,Brand,Model,Year` | Owned cars not best on any track (RQ<80, not prize) | | `trash.csv` | `RQ,Brand,Model,Year` | Owned cars not best on any track (RQ<80, not prize) |
| `wishlist.csv` | `RQ,Brand,Model,Year,Count` | Ideal wishlist (very_best unowned, capped) | | `wishlist.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Ideal wishlist (very_best unowned, capped) |
| `add.csv` | `RQ,Brand,Model,Year,Count` | Cars to add to wishlist | | `add.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Cars to add to wishlist |
| `remove.csv` | `RQ,Brand,Model,Year,Count` | Cars to remove from wishlist | | `remove.csv` | `RQ,Brand,Model,Year,FinalBuild,Count` | Cars to remove from wishlist |
| `improve.csv` | `RQ,Brand,Model,Year,Target,Count` | Owned cars not yet at final build config |
### Typical Daily Routine ### Typical Daily Routine
@ -421,7 +455,7 @@ The tool generates CSV files for external analysis. All files are sorted by car
#### "Please provide a command" #### "Please provide a command"
**Problem:** No arguments provided **Problem:** No arguments provided
**Solution:** Use `best`, `trash`, `wishlist`, `all`, `details`, `track`, or `fetch` **Solution:** Use `best`, `trash`, `wishlist`, `all`, `details`, `track`, `improve`, or `fetch`
#### "Could not read my cars file" / "Missing data file" #### "Could not read my cars file" / "Missing data file"
**Problem:** `my_cars.json` (or another data file) not found or not readable **Problem:** `my_cars.json` (or another data file) not found or not readable