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:
parent
bf21d3d1f6
commit
2b0602d564
6 changed files with 204 additions and 84 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
45
README.md
45
README.md
|
|
@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
63
docs/API.md
63
docs/API.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue