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
This commit is contained in:
parent
67a79f190a
commit
bf21d3d1f6
25 changed files with 735 additions and 759 deletions
|
|
@ -3,7 +3,7 @@
|
|||
//! Handles parsing the game export file to extract owned car GUIDs
|
||||
//! and wishlist entries.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
|
|
@ -21,6 +21,10 @@ pub struct MyCarsData {
|
|||
pub owned: HashSet<String>,
|
||||
/// GUIDs of wishlisted cars (from `wishlists[0].cards`)
|
||||
pub wishlist: HashSet<String>,
|
||||
/// Actual tune config for each owned car (guid → "{engineMajor}{weightMajor}{chassisMajor}").
|
||||
/// Only populated for locked cards. Used by the improve command to determine
|
||||
/// which cars are not yet at a final build.
|
||||
pub owned_configs: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Loads and parses `my_cars.json` once, extracting both owned and wishlist data.
|
||||
|
|
@ -49,22 +53,33 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
|
|||
let json: Value = serde_json::from_reader(reader)
|
||||
.map_err(|e| AppError::Parse { file: path.to_string(), reason: e.to_string() })?;
|
||||
|
||||
// Extract owned car GUIDs from playerDeck
|
||||
let owned: HashSet<String> = json
|
||||
.get("playerDeck")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|deck| {
|
||||
deck.iter()
|
||||
.filter_map(|card| {
|
||||
if card.get("locked").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
card.get("cardId").and_then(|v| v.as_str()).map(|s| s.to_string())
|
||||
// Extract owned car GUIDs and their actual configs from playerDeck
|
||||
let mut owned: HashSet<String> = HashSet::new();
|
||||
let mut owned_configs: HashMap<String, String> = HashMap::new();
|
||||
|
||||
if let Some(deck) = json.get("playerDeck").and_then(|d| d.as_array()) {
|
||||
for card in deck {
|
||||
if card.get("locked").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
if let Some(guid) = card.get("cardId").and_then(|v| v.as_str()) {
|
||||
owned.insert(guid.to_string());
|
||||
// Extract actual tune config: engineMajor + weightMajor + chassisMajor.
|
||||
// Skip cards with incomplete tune data to avoid false "000" entries
|
||||
// that would incorrectly appear as needing upgrade.
|
||||
let engine = card.get("engineMajor").and_then(|v| v.as_u64());
|
||||
let weight = card.get("weightMajor").and_then(|v| v.as_u64());
|
||||
let chassis = card.get("chassisMajor").and_then(|v| v.as_u64());
|
||||
if let (Some(e), Some(w), Some(c)) = (engine, weight, chassis) {
|
||||
let config = format!("{e}{w}{c}");
|
||||
owned_configs.insert(guid.to_string(), config);
|
||||
} else {
|
||||
None
|
||||
eprintln!(
|
||||
"Warning: locked card {guid} has incomplete tune data — skipped"
|
||||
);
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract wishlist GUIDs from wishlists[0].cards
|
||||
let wishlist: HashSet<String> = json
|
||||
|
|
@ -72,15 +87,10 @@ pub fn load_my_cars_data(path: &str) -> Result<MyCarsData, AppError> {
|
|||
.and_then(|w| w.get(0))
|
||||
.and_then(|w| w.get("cards"))
|
||||
.and_then(|c| c.as_array())
|
||||
.map(|cards| {
|
||||
cards
|
||||
.iter()
|
||||
.filter_map(|c| c.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.map(|cards| cards.iter().filter_map(|c| c.as_str().map(|s| s.to_string())).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(MyCarsData { owned, wishlist })
|
||||
Ok(MyCarsData { owned, wishlist, owned_configs })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue