Added documentation

Added test cases
Added comments
This commit is contained in:
Jan-Peter von Hunnius 2025-10-02 17:35:56 +02:00
parent 7a8d9beedc
commit 32c4d3c10e
10 changed files with 3852 additions and 63 deletions

194
CHANGELOG.md Normal file
View file

@ -0,0 +1,194 @@
# Changelog
All notable changes to the Top Drives Records Analyzer project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Comprehensive documentation suite in `docs/` directory
- Detailed code comments for all public functions
- Architecture documentation explaining system design
- User guide with command examples and troubleshooting
- Data structures documentation for JSON formats
- Development setup guide with dependency explanations
### Improved
- Enhanced error handling throughout the application
- Better progress reporting during data fetching
- More descriptive command-line help text
- Improved CSV output formatting
## [0.2.0] - 2024-12-XX
### Added
- **Club Module**: Alternative data source using web scraping from topdrives.club
- **Track-Specific Analysis**: New `track` command for targeted recommendations
- **Enhanced Output**: CSV file generation for analysis results
- **Improved Concurrency**: Better handling of multiple simultaneous downloads
- **Rate Limiting**: Configurable download limits to respect server constraints
### Changed
- **Modular Architecture**: Split functionality into separate binary targets
- **Error Handling**: More graceful handling of network and parsing errors
- **Configuration**: Externalized track and car data sources
- **Performance**: Optimized memory usage for large datasets
### Fixed
- **Memory Leaks**: Fixed issues with async task cleanup
- **File Handle Management**: Proper cleanup of temporary files
- **Network Timeouts**: Better handling of slow or unreliable connections
## [0.1.0] - 2024-01-XX
### Added
- **Initial Release**: Core functionality for Top Drives car analysis
- **Best Cars Analysis**: Identify top performing cars in collection
- **Missing Cars Detection**: Find cars that would improve performance
- **Collection Optimization**: Identify cars safe to remove
- **Detailed Performance Data**: Comprehensive timing analysis
- **JSON Data Processing**: Parse community timing database
- **Concurrent Processing**: Multi-threaded analysis for performance
### Features
- **Multi-Platform Support**: Windows, macOS, and Linux compatibility
- **Comprehensive Track Support**: All tracks and surface conditions
- **RQ Range Analysis**: Performance analysis across rarity brackets
- **User Collection Integration**: Import and analyze personal car collection
- **Community Data Integration**: Leverage crowd-sourced timing data
## Technical Features by Version
### Data Processing
- **[0.1.0]** Basic JSON parsing and analysis
- **[0.2.0]** Enhanced error handling and data validation
- **[Unreleased]** Improved caching and incremental updates
### Performance
- **[0.1.0]** Single-threaded processing
- **[0.2.0]** Concurrent downloads with rate limiting
- **[Unreleased]** Memory optimization and streaming processing
### User Interface
- **[0.1.0]** Basic command-line interface
- **[0.2.0]** Enhanced output formatting and CSV export
- **[Unreleased]** Comprehensive documentation and help system
### Data Sources
- **[0.1.0]** JSON-based records from community repository
- **[0.2.0]** Added HTML scraping from topdrives.club
- **[Unreleased]** Multiple data source integration
## Migration Guide
### Upgrading from 0.1.0 to 0.2.0
**No breaking changes** - the upgrade is fully backwards compatible.
**New Features Available:**
1. **Club Module**: Alternative data source with `cargo run --bin club`
2. **Track Command**: Specific track analysis with `cargo run --bin records -- track`
3. **CSV Output**: Automatic generation of analysis files
**Performance Improvements:**
- Faster data fetching with concurrent downloads
- Better memory usage for large datasets
- Improved error recovery
## Deprecation Notices
### Future Removals
- **None currently planned**
### Deprecated Features
- **None currently deprecated**
## Community Contributions
### Notable Contributions
- **Community Timing Data**: All performance data from Top Drives community
- **Track Definitions**: Comprehensive track database
- **Bug Reports**: User feedback for improvements
### Contribution Statistics
- **Timing Submissions**: 1000+ cars with community data
- **Track Coverage**: 200+ tracks across all conditions
- **Active Contributors**: Community-driven development
## Performance Benchmarks
### Data Processing Speed
- **Initial Load**: ~2-3 minutes (downloads 1000+ files)
- **Subsequent Runs**: <30 seconds (uses cached data)
- **Concurrent Operations**: 20 simultaneous downloads
### Memory Usage
- **Base Application**: ~50MB
- **With Full Dataset**: ~200MB
- **Peak During Processing**: ~300MB
### Network Efficiency
- **Compression**: Efficient HTTP compression
- **Connection Reuse**: Persistent HTTP connections
- **Rate Limiting**: Respects server constraints
## Known Issues and Limitations
### Current Limitations
- **Data Freshness**: Requires manual refresh for latest community data
- **Network Dependency**: Requires internet for initial data download
- **Storage Requirements**: Large local dataset (~100MB)
- **Single User**: Designed for individual use, not multi-user
### Platform-Specific Issues
- **Windows**: Potential path length limitations with many files
- **macOS**: Gatekeeper may flag the binary (code-signed in releases)
- **Linux**: No known issues
## Future Roadmap
### Planned Features (Unreleased)
- **Web Interface**: Browser-based GUI for non-technical users
- **Historical Analysis**: Track performance changes over time
- **Mobile App**: iOS/Android companion application
- **Cloud Sync**: Synchronize collection across devices
- **Advanced Analytics**: Statistical analysis and trend detection
### Potential Enhancements
- **Real-time Updates**: Live data synchronization
- **Social Features**: Share collections and recommendations
- **Customization**: User-defined analysis parameters
- **Export Options**: Multiple file formats (PDF, Excel)
## Support Information
### Getting Help
1. **Documentation**: Check `docs/` directory for comprehensive guides
2. **Issue Tracker**: Report bugs and request features
3. **Discussions**: Community forum for questions and tips
### Troubleshooting Resources
- **User Guide**: Step-by-step instructions and examples
- **Development Setup**: Technical installation guide
- **Data Structures**: Detailed format documentation
- **Architecture Guide**: System design and data flow
## Acknowledgments
### Contributors
- **Original Author**: Project creator and maintainer
- **Community Contributors**: Timing data providers and testers
- **Open Source Projects**: Rust ecosystem and tooling
### Data Sources
- **Top Drives Community**: Primary timing data source
- **topdrives.club**: Alternative data source
- **GitHub Contributors**: Track and car database maintenance
---
**Changelog Format**: Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format
**Versioning**: Follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) principles
**Last Updated**: 2024-12-XX

View file

@ -19,3 +19,6 @@ serde_json = "1.0.140"
futures = "0.3.31"
once_cell = "1.20.2"
serde = { version = "1.0.210", features = ["derive"] }
[dev-dependencies]
tempfile = "3.10.1"

275
README.md Normal file
View file

@ -0,0 +1,275 @@
# Top Drives Records Analyzer
A comprehensive Rust-based tool for analyzing car performance data in the Top Drives racing game. This tool fetches track times from a community-maintained repository and provides detailed analysis including best cars, missing cars, and optimization recommendations.
## 🚀 Features
- **Best Car Analysis**: Find the best performing cars across all tracks and conditions
- **Missing Cars Detection**: Identify cars missing from your collection that would improve performance
- **Collection Optimization**: Identify cars that can be safely removed from your collection
- **Track-Specific Analysis**: Get detailed performance data for specific tracks and conditions
- **RQ-Based Filtering**: Filter results by car rarity (RQ) levels
- **Multiple Surface Types**: Support for Dry, Wet, Dirt, Snow, and other track conditions
## 📋 Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage Guide](#usage-guide)
- [Commands Reference](#commands-reference)
- [Data Sources](#data-sources)
- [Configuration](#configuration)
- [Development](#development)
- [Contributing](#contributing)
## 🛠️ Installation
### Prerequisites
- [Rust](https://rustup.rs/) (latest stable version)
- Internet connection for fetching data
### Setup
1. **Clone the repository**
```bash
git clone <repository-url>
cd top_drives
```
2. **Build the project**
```bash
cargo build --release
```
3. **Run the analyzer**
```bash
cargo run --bin records -- fetch
```
## 🚦 Quick Start
1. **Initial Setup**: Fetch the latest car and track data
```bash
cargo run --bin records -- fetch
```
2. **Basic Analysis**: Find the best cars in your collection
```bash
cargo run --bin records -- best
```
3. **Find Missing Cars**: Discover cars that would improve your performance
```bash
cargo run --bin records -- missing
```
4. **Collection Cleanup**: Identify cars safe to remove
```bash
cargo run --bin records -- trash
```
## 📖 Usage Guide
### Basic Commands
#### Fetch Data
```bash
cargo run --bin records -- fetch
```
Downloads the latest car database and track information from the community repository.
#### Best Cars Analysis
```bash
cargo run --bin records -- best
```
Shows all cars in your collection ranked by their performance across tracks.
**Sample Output:**
```
RQ,Brand,Model,Year,Count
85,McLaren,F1,1993,15
82,Ferrari,250 GTO,1962,12
80,Lamborghini,Countach,1974,8
```
#### Missing Cars Analysis
```bash
cargo run --bin records -- missing
```
Shows the best cars that you don't own, helping prioritize acquisitions.
#### Collection Optimization
```bash
cargo run --bin records -- trash
```
Identifies cars in your collection that aren't among the best performers and could be removed.
#### Detailed Performance Data
```bash
cargo run --bin records -- details
```
Shows comprehensive timing data for all tracks and conditions.
**Sample Output:**
```
RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time
85,laguna,Dry,85,McLaren,F1,1993,233,112.45
82,monaco,Dry,82,Ferrari,250 GTO,1962,111,45.23
```
#### Track-Specific Analysis
```bash
cargo run --bin records -- track "Laguna Seca" "Dry" "85"
```
Finds the best car for a specific track, condition, and RQ combination.
### Advanced Usage
#### Multiple RQ Analysis
The tool supports analyzing performance across different RQ ranges. Use the `missing` and `best` commands to see how cars perform at various rarity levels.
#### Track Conditions
The analyzer supports multiple track conditions:
- **Dry**: Standard asphalt conditions
- **Wet**: Rain-affected tracks
- **Dirt**: Off-road conditions
- **Snow**: Winter conditions
- **Gravel**: Loose surface conditions
## 🗃️ Commands Reference
| Command | Description | Arguments |
|---------|-------------|-----------|
| `fetch` | Download latest car and track data | None |
| `best` | Show best performing cars in collection | None |
| `missing` | Show best cars not in collection | None |
| `trash` | Show cars safe to remove from collection | None |
| `details` | Show detailed timing data for all tracks | None |
| `track` | Show best car for specific track | `<track_name> <condition> <rq>` |
## 📊 Data Sources
### Car Data
- **Source**: Community-maintained GitHub repository
- **Format**: JSON files with performance data per car
- **Update Frequency**: Regular updates from community contributors
### Track Data
- **Source**: Same community repository
- **Coverage**: All tracks and conditions in Top Drives
- **Format**: Structured track definitions with conditions
### User Collection
- **File**: `my_cars.json`
- **Format**: Top Drives game export format
- **Usage**: Identifies owned cars for analysis
## ⚙️ Configuration
### Files Structure
```
top_drives/
├── src/
│ ├── records.rs # Main analyzer binary
│ └── club.rs # Alternative data source
├── times/ # Car performance data (1000+ files)
├── cars_repo.json # Car database
├── tracks_repo.json # Track definitions
├── my_cars.json # Your car collection
└── Cargo.toml # Rust project configuration
```
### Dependencies
- **reqwest**: HTTP client for data fetching
- **serde_json**: JSON parsing and serialization
- **tokio**: Asynchronous runtime
- **scraper**: HTML parsing for alternative data source
## 🔧 Development
### Project Structure
The project consists of two main binaries:
1. **records**: Primary analysis tool using JSON data
2. **club**: Alternative tool scraping topdrives.club
### Key Data Structures
#### Car Information
```rust
struct Car {
rid: String, // Unique car identifier
brand: String, // Manufacturer name
only_name: String, // Model name
year: u16, // Model year
rq: u8, // Rarity level (10-113)
prize: bool, // Whether car is prize-only
guid: String, // Game-specific identifier
}
```
#### Performance Records
```rust
struct BestCarRecord {
car_rid: String, // Car identifier
brand: String, // Manufacturer
only_name: String, // Model name
year: u16, // Year
rq: u8, // Rarity level
config: String, // Tune configuration
time: f64, // Track time
owned: bool, // Whether you own this car
guid: String, // Game identifier
}
```
### Building
```bash
# Debug build
cargo build
# Release build
cargo build --release
# Run specific binary
cargo run --bin records
cargo run --bin club
```
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
### Adding New Tracks
Track definitions are stored in `tracks_repo.json`. Each track includes:
- Unique identifier
- Display name
- Supported conditions
- Track type classification
### Adding Car Data
New car performance data should be submitted to the upstream repository following their contribution guidelines.
## 📄 License
This project is open source. Please check the upstream data repositories for their respective licenses.
## 🆘 Support
For issues and questions:
1. Check existing documentation
2. Review open issues
3. Create new issue with detailed description
## 🔄 Updates
The tool fetches fresh data each time you run the `fetch` command. Keep your local data updated for best results.
---
*Built with ❤️ for the Top Drives community*

654
docs/API.md Normal file
View file

@ -0,0 +1,654 @@
# API Documentation
## Overview
This document provides comprehensive API documentation for the Top Drives Records Analyzer, including all public functions, data structures, and their usage patterns.
## Core Modules
### Records Module (`src/records.rs`)
Primary analysis engine for processing JSON-based timing data.
#### Public Functions
##### `main()`
**Entry point for the records analyzer**
```rust
#[tokio::main]
async fn main()
```
**Purpose**: Command-line interface and operation dispatcher
**Arguments**: Command line arguments
- `args[1]`: Command (`fetch`, `best`, `missing`, `trash`, `details`, `track`)
**Commands**:
- `fetch`: Download all data files
- `best`: Show best owned cars
- `missing`: Show best unowned cars
- `trash`: Show removable cars
- `details`: Export all timing data
- `track`: Track-specific analysis
**Example**:
```bash
cargo run --bin records -- best
```
##### `print_best_cars(filter_out_owned: bool)`
**Displays ranked list of best performing cars**
```rust
fn print_best_cars(filter_out_owned: bool)
```
**Purpose**: Aggregates performance data across all tracks and conditions
**Arguments**:
- `filter_out_owned`: If true, excludes owned cars from output
**Output Format**:
```csv
RQ,Brand,Model,Year,Count
85,McLaren,F1,1993,15
82,Ferrari,250 GTO,1962,12
```
**Algorithm**:
1. Count occurrences of each car across all track/RQ/condition combinations
2. Sort by RQ level, then by car identifier
3. Apply ownership filter if requested
##### `print_best_times()`
**Exports comprehensive timing data for all combinations**
```rust
fn print_best_times()
```
**Purpose**: Detailed performance export for external analysis
**Output Format**:
```csv
RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time
85,laguna,Dry,85,McLaren,F1,1993,233,112.45
```
**Data Volume**: Large output file with all track/condition/RQ combinations
##### `print_best_car(track: &String, condition: &String, rq: &String)`
**Finds best car for specific track conditions**
```rust
fn print_best_car(track: &String, condition: &String, rq: &String)
```
**Purpose**: Targeted analysis for specific racing scenarios
**Arguments**:
- `track`: Track identifier (e.g., "laguna", "monaco")
- `condition`: Surface condition (e.g., "Dry", "Wet")
- `rq`: RQ level limit
**Example**:
```rust
print_best_car(&"laguna".to_string(), &"Dry".to_string(), &"85".to_string());
```
##### `print_trash()`
**Identifies cars safe for removal from collection**
```rust
fn print_trash()
```
**Purpose**: Collection optimization recommendations
**Safety Criteria**:
- Car not among best performers on any track
- RQ level below 80 (preserves high-end cars)
- Not a prize car (may have collection value)
**Output Format**:
```csv
RQ,Brand,Model,Year
45,Toyota,Corolla,1995
```
##### `parse_car_times(car: Car)`
**Processes timing data for a single car**
```rust
async fn parse_car_times(car: Car)
```
**Purpose**: Core parsing logic for individual car performance data
**Process Flow**:
1. Load car's JSON timing file
2. Parse each configuration (111, 233, 323, etc.)
3. Extract timing data for each track/condition
4. Apply quality filters (downvotes, time validation)
5. Update global best cars cache
**Quality Filters**:
- Minimum time: 1.2 seconds
- No downvotes from community
- Special handling for Test Bowl track
##### `parse_all_times()`
**Orchestrates concurrent processing of all cars**
```rust
async fn parse_all_times()
```
**Purpose**: Coordinates parallel processing of car timing files
**Concurrency**: One async task per car file
**Synchronization**: Waits for all tasks to complete
##### `update_best_car(track_id: &str, car_rq: u8, surface: &str, new_record: BestCarRecord)`
**Updates global best cars database with new performance data**
```rust
fn update_best_car(track_id: &str, car_rq: u8, surface: &str, new_record: BestCarRecord)
```
**Purpose**: Implements core best-car selection algorithm
**RQ Range Analysis**:
- Tests car from base RQ up to maximum (115)
- Compares against existing best times
- Updates records for improved performance
**Logic**:
1. For each RQ level from car_rq to 115:
2. If new time is better → replace record
3. If new time ties → add to list (if not duplicate)
4. If new time is worse → ignore
##### `get_car_list() -> Vec<Car>`
**Loads complete car database from repository file**
```rust
fn get_car_list() -> Vec<Car>
```
**Purpose**: Provides access to all known cars in the database
**Source**: `cars_repo.json` file
**Returns**: Vector of all cars with complete metadata
##### `get_my_car_list() -> Vec<String>`
**Loads user's car collection from game export**
```rust
fn get_my_car_list() -> Vec<String>
```
**Purpose**: Identifies which cars the user owns
**Source**: `my_cars.json` game export
**Filter**: Only `locked: true` cards
**Returns**: Vector of owned car GUIDs
##### `fetch_times()`
**Downloads timing files for all cars with rate limiting**
```rust
async fn fetch_times()
```
**Purpose**: Batch download of car timing data
**Rate Limiting**: 20 concurrent downloads maximum
**Progress**: Shows download progress for each file
##### `fetch_car(rid: String)`
**Downloads timing data for single car**
```rust
async fn fetch_car(rid: String)
```
**Purpose**: Individual car data fetching
**URL Format**: `https://api.topdrivesrecords.com/car/{rid}`
**Storage**: `times/{rid}.json`
##### `get_cars_file()`
**Downloads main car database**
```rust
async fn get_cars_file()
```
**Purpose**: Updates car metadata database
**Source**: GitHub repository cars database
**Destination**: `cars_repo.json`
##### `get_tracks_file()`
**Downloads track definitions**
```rust
async fn get_tracks_file()
```
**Purpose**: Updates track name mappings
**Source**: GitHub repository tracks database
**Destination**: `tracks_repo.json`
##### `get_file(file_url: &str, local_file_path: &str)`
**Generic file download utility**
```rust
async fn get_file(file_url: &str, local_file_path: &str)
```
**Purpose**: Downloads files from remote URLs
**Features**:
- Automatic existing file removal
- Error handling with graceful failure
- Progress indication through errors
##### `translate_track_name(track_code: &str) -> &str`
**Converts track codes to human-readable names**
```rust
fn translate_track_name(track_code: &str) -> &str
```
**Purpose**: User-friendly track name display
**Coverage**: 200+ track mappings
**Fallback**: Returns original code if not found
##### `translate_track_type(track_code: &str) -> &str`
**Converts condition codes to surface descriptions**
```rust
fn translate_track_type(track_code: &str) -> &str
```
**Purpose**: Human-readable surface condition names
**Mappings**:
- `"00"` → `"Dry"`
- `"01"` → `"Wet"`
- `"10"` → `"Dirt"`
- `"11"` → `"Dirt Wet"`
##### `split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a str, &'a str)>`
**String splitting utility for track ID parsing**
```rust
fn split_prefix_postfix<'a>(input: &'a str, delimiter: &'a str) -> Option<(&'a str, &'a str)>
```
**Purpose**: Parse track IDs with condition suffixes
**Example**:
```rust
split_prefix_postfix("laguna_a00", "_a") // Some(("laguna", "00"))
```
### Club Module (`src/club.rs`)
Alternative data source using web scraping from topdrives.club.
#### Public Functions
##### `main()`
**Entry point for club scraper**
```rust
#[tokio::main]
async fn main()
```
**Purpose**: Web scraping interface for live data
**Arguments**:
- Command line arguments for RQ and track filtering
- `-d` flag for detailed output
##### `get_page(url: &str) -> String`
**Fetches HTML content from URLs**
```rust
async fn get_page(url: &str) -> String
```
**Purpose**: Downloads web pages for scraping
**Security**: Accepts invalid SSL certificates for compatibility
##### `parse_best_car(text: String) -> Car`
**Extracts car data from HTML content**
```rust
fn parse_best_car(text: String) -> Car
```
**Purpose**: Parses HTML tables to extract car information
**CSS Selectors**:
- `.carList__item-manufacturer`: Car brand
- `.carList__item-model`: Car model
- `.carList__item-year`: Model year
- `.carList__item-level`: Class and RQ (complex parsing)
- `.carList__item-upgrade`: Upgrade configuration
##### `parse_tracks(file_name: &str) -> HashMap<u16, String>`
**Loads track definitions from file**
```rust
fn parse_tracks(file_name: &str) -> HashMap<u16, String>
```
**Purpose**: Creates track ID to name mappings
**Format**: Text file with `ID NAME` format per line
##### `get_best_car(track: u16, track_name: String, max_level: u16, detailed: bool) -> String`
**Scrapes best car data for specific track**
```rust
async fn get_best_car(track: u16, track_name: String, max_level: u16, detailed: bool) -> String
```
**Purpose**: Fetches and parses best car from topdrives.club
**API Integration**:
- Constructs topdrives.club API URLs
- Parses HTML response tables
- Formats output based on detail level
##### `parse_argument(args: &Vec<String>, index: usize, min: u16, max: u16) -> u16`
**Validates command line arguments**
```rust
fn parse_argument(args: &Vec<String>, index: usize, min: u16, max: u16) -> u16
```
**Purpose**: Input validation with range checking
**Panics**: On invalid values with descriptive messages
## Data Structures
### Records Module Data Types
#### `Card`
**User's car collection entry**
```rust
struct Card {
cardId: String, // Unique card identifier
locked: bool, // Ownership status
}
```
#### `Car`
**Car metadata from database**
```rust
struct Car {
rid: String, // Repository ID
brand: String, // Manufacturer
only_name: String, // Model name
year: u16, // Model year
rq: u8, // Rarity level
prize: bool, // Prize car status
guid: String, // Game GUID
}
```
#### `BestCarRecord`
**Performance record for best cars**
```rust
struct BestCarRecord {
car_rid: String, // Car identifier
brand: String, // Manufacturer
only_name: String, // Model name
year: u16, // Year
rq: u8, // RQ level
config: String, // Tune configuration
time: f64, // Track time
owned: bool, // User ownership
guid: String, // Game identifier
}
```
#### `TripletKey`
**Composite key for track conditions**
```rust
type TripletKey = (String, u8, String);
// Track ID RQ Level Surface Condition
```
### Club Module Data Types
#### `Car`
**Scraped car data from website**
```rust
struct Car {
class: String, // Class letter (S, A, B, etc.)
rq: u8, // Rarity level
manufacturer: String, // Brand name
model: String, // Model name
year: u16, // Model year
upgrade: String, // Upgrade config
}
```
## Global State
### `BEST_CARS` Cache
**Thread-safe global performance database**
```rust
static BEST_CARS: Lazy<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>
```
**Initialization**: One-time setup with `once_cell::Lazy`
**Thread Safety**: Protected by `Mutex` for concurrent access
**Structure**: HashMap with composite keys for efficient lookups
## Error Handling
### Network Errors
- **HTTP Failures**: Logged but don't crash application
- **Invalid JSON**: Graceful fallback to defaults
- **Missing Files**: Clear error messages for user
### Parse Errors
- **Malformed Data**: Skip invalid entries
- **Type Conversion**: Use safe defaults
- **Missing Fields**: Provide sensible fallbacks
### User Input Errors
- **Invalid Commands**: Clear usage instructions
- **Missing Arguments**: Descriptive error messages
- **Out of Range**: Validation with helpful feedback
## Usage Examples
### Basic Analysis Workflow
```rust
// 1. Load user's car collection
let my_cars = get_my_car_list();
// 2. Process all car timing data
parse_all_times().await;
// 3. Generate best cars report
print_best_cars(false);
// 4. Find collection optimization opportunities
print_trash();
```
### Track-Specific Analysis
```rust
// Analyze specific track conditions
let track = "laguna".to_string();
let condition = "Dry".to_string();
let rq = "85".to_string();
print_best_car(&track, &condition, &rq);
```
### Data Fetching
```rust
// Download fresh data
get_cars_file().await;
get_tracks_file().await;
fetch_times().await;
```
## Performance Characteristics
### Time Complexity
- **Parsing**: O(n) where n = number of cars × tracks
- **Best Car Selection**: O(1) HashMap operations
- **Sorting**: O(n log n) for output ordering
### Memory Usage
- **Peak**: ~200-300MB during full analysis
- **Baseline**: ~50MB application overhead
- **Per Car**: ~1-2KB metadata + timing data
### Concurrency
- **Downloads**: 20 concurrent connections max
- **Processing**: One task per car file
- **Thread Safety**: Mutex-protected shared state
## Extension Points
### Adding New Commands
1. Add command parsing in `main()`
2. Implement analysis function
3. Update help documentation
### Adding New Data Sources
1. Implement new module (similar to `club.rs`)
2. Add data source selection logic
3. Update configuration system
### Custom Analysis Types
1. Create new output formatting functions
2. Add aggregation algorithms
3. Extend data structures as needed
## Testing
### Unit Tests
```rust
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_parse_car_times() {
// Test implementation
}
}
```
### Integration Tests
- Full workflow testing
- File I/O validation
- Network request mocking
### Benchmarking
- Parse performance measurement
- Memory usage profiling
- Concurrent operation testing
## Security Considerations
### Network Security
- HTTPS-only external requests
- Certificate validation (with fallback)
- No sensitive data transmission
### File System Security
- Read-only access to game files
- No modification of user data
- Safe file path handling
### Input Validation
- Command injection prevention
- Path traversal protection
- Input sanitization
## Troubleshooting API Issues
### Common Problems
**"Function not found"**
- Check module imports
- Verify function signatures
- Ensure proper visibility
**"Type mismatch"**
- Review struct field types
- Check function signatures
- Verify generic parameters
**"Deadlock"**
- Avoid nested Mutex locks
- Check lock ordering
- Use timeout where possible
### Debug Strategies
**Logging**:
```rust
println!("Processing car: {}", car.rid);
eprintln!("Error: {}", error);
```
**Tracing**:
```rust
// Add at function entry
let _span = tracing::span!(Level::INFO, "function_name");
let _enter = _span.enter();
```
**Profiling**:
```bash
cargo flamegraph --bin records -- best
```
## Migration Guide
### Version Compatibility
- **Current**: 0.2.0 API stable
- **Previous**: 0.1.0 fully compatible
- **Future**: Semantic versioning maintained
### Breaking Changes
- None currently planned
- Advance notice provided for API changes
- Deprecation warnings for removed features
---
**API Version**: 0.2.0
**Last Updated**: 2024-12-XX
**Compatibility**: Rust 1.70.0+

242
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,242 @@
# Architecture Documentation
## System Overview
The Top Drives Records Analyzer consists of two main components working together to provide comprehensive car performance analysis for the Top Drives racing game.
## Component Architecture
### 1. Records Module (`src/records.rs`)
**Primary analysis engine** that processes pre-downloaded JSON timing data.
#### Key Responsibilities
- **Data Fetching**: Downloads car and track databases from GitHub repository
- **File Processing**: Parses JSON timing files for each car configuration
- **Performance Analysis**: Compares timing data across RQ ranges and track conditions
- **Collection Management**: Identifies owned cars and optimization opportunities
#### Data Flow
```
GitHub Repository
↓
cars_repo.json + tracks_repo.json
↓
times/*.json (1000+ car files)
↓
JSON Parsing → BestCarRecord Analysis
↓
User Collection (my_cars.json)
↓
Output: CSV/Terminal Results
```
### 2. Club Module (`src/club.rs`)
**Alternative data source** that scrapes live data from topdrives.club website.
#### Key Responsibilities
- **Web Scraping**: Fetches HTML content from topdrives.club
- **HTML Parsing**: Extracts car performance data using CSS selectors
- **Real-time Data**: Provides fresh data without pre-downloaded files
#### Data Flow
```
topdrives.club Website
↓
HTML Content
↓
CSS Selector Parsing
↓
Car Data Extraction
↓
Output: Terminal Results
```
## Data Structures
### Core Types
#### TripletKey
```rust
type TripletKey = (String, u8, String);
// ↓ ↓ ↓
// Track ID RQ Level Surface Condition
```
**Purpose**: Composite key for uniquely identifying track conditions
**Usage**: HashMap key for storing best car records
#### Car (Records Module)
```rust
struct Car {
rid: String, // Repository ID
brand: String, // Manufacturer
only_name: String, // Model name
year: u16, // Model year
rq: u8, // Rarity level
prize: bool, // Prize car status
guid: String, // Game GUID
}
```
#### BestCarRecord
```rust
struct BestCarRecord {
car_rid: String, // Car identifier
brand: String, // Manufacturer
only_name: String, // Model name
year: u16, // Year
rq: u8, // RQ level
config: String, // Tune config (111, 233, etc.)
time: f64, // Track time (seconds)
owned: bool, // User ownership status
guid: String, // Game identifier
}
```
#### Car (Club Module)
```rust
struct Car {
class: String, // Class letter (S, A, B, etc.)
rq: u8, // Rarity level
manufacturer: String, // Brand
model: String, // Model
year: u16, // Year
upgrade: String, // Upgrade config
}
```
## Global State Management
### BEST_CARS Cache
```rust
static BEST_CARS: Lazy<Mutex<HashMap<TripletKey, Vec<BestCarRecord>>>>
```
**Thread Safety**: Protected by `Mutex` for concurrent access during parsing
**Initialization**: Uses `once_cell::Lazy` for one-time setup
**Structure**: HashMap with composite keys for efficient lookups
## Algorithm Details
### RQ Range Analysis
The system analyzes car performance across multiple RQ brackets:
1. **Base RQ**: Car's natural rarity level
2. **RQ Range**: Tests from car's RQ up to maximum (115)
3. **Cross-RQ Competition**: Allows cars to compete above their base level
**Example**: A RQ 85 car might perform well at RQ 90-95 levels against higher-rated cars
### Time Validation
Multiple validation layers ensure data quality:
1. **Minimum Time**: Times below 1.2 seconds are discarded
2. **Downvote Filter**: Community-downvoted times are excluded
3. **Special Cases**: Test Bowl track uses inverted time calculation (2000.0 / time)
### Concurrent Processing
#### Records Module
- **Task Spawning**: One async task per car file
- **Rate Limiting**: Semaphore limits concurrent downloads (20 max)
- **Error Isolation**: Individual car failures don't stop batch processing
#### Synchronization
- **Join Handles**: Waits for all parsing tasks to complete
- **Mutex Protection**: Safe concurrent access to shared BEST_CARS cache
## File Organization
### Input Files
```
├── cars_repo.json # Car database from repository
├── tracks_repo.json # Track definitions
├── my_cars.json # User's car collection (game export)
└── times/ # Individual car timing files
├── Ferrari_F40_1987.json
├── McLaren_F1_1993.json
└── ...
```
### Output Files
```
├── best.csv # Best cars analysis results
├── missing.csv # Missing recommended cars
└── details.csv # Detailed timing data
```
## Performance Characteristics
### Records Module
- **Startup Time**: Fast (uses cached JSON files)
- **Memory Usage**: Moderate (loads all timing data into memory)
- **Accuracy**: High (comprehensive dataset)
- **Update Frequency**: Manual refresh required
### Club Module
- **Startup Time**: Slow (web scraping required)
- **Memory Usage**: Low (processes one track at a time)
- **Accuracy**: Variable (depends on website structure)
- **Update Frequency**: Real-time on each run
## Error Handling Strategy
### Graceful Degradation
- **Missing Files**: Continue with available data
- **Network Errors**: Log errors but don't crash
- **Parse Failures**: Use default values or skip invalid entries
### User Feedback
- **Progress Indication**: Shows fetching/download progress
- **Error Messages**: Clear descriptions of what went wrong
- **Fallback Options**: Multiple data sources available
## Extension Points
### Adding New Tracks
1. Update `tracks_repo.json` with new track definitions
2. Add track name mappings in `translate_track_name()`
3. Add condition mappings in `translate_track_type()`
### Adding New Analysis Types
1. Create new output functions (similar to `print_best_cars()`)
2. Add command parsing in main function
3. Update help text and documentation
### Custom Data Sources
1. Implement new scraping module (similar to club.rs)
2. Add data source selection logic
3. Update configuration system
## Security Considerations
### Network Access
- **HTTPS Only**: All external requests use HTTPS
- **Certificate Validation**: Accepts invalid certificates for compatibility
- **Rate Limiting**: Respects server limits with controlled concurrency
### Local File Access
- **Read Only**: No modification of game files
- **User Data**: Only reads exported collection data
- **No Persistence**: Analysis results are output-only
## Future Enhancements
### Potential Improvements
1. **Caching Layer**: Cache web scraping results
2. **Database Backend**: Replace in-memory HashMap with persistent storage
3. **Configuration Files**: External configuration for analysis parameters
4. **GUI Interface**: Web-based interface for non-technical users
5. **Export Formats**: Multiple output formats (JSON, XML, etc.)
### Scalability Considerations
1. **Pagination**: Handle large datasets with pagination
2. **Incremental Updates**: Only fetch changed data
3. **Compression**: Compress large timing files
4. **Streaming**: Process data without loading everything into memory

407
docs/DATA_STRUCTURES.md Normal file
View file

@ -0,0 +1,407 @@
# JSON Data Structures Documentation
## Overview
This document describes the JSON data structures used by the Top Drives Records Analyzer, including their purpose, format, and usage within the application.
## Core Data Files
### 1. Car Database (`cars_repo.json`)
**Purpose**: Master database of all cars in Top Drives with their metadata.
**Source**: Downloaded from `https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/cars_final.json`
**Structure**:
```json
[
{
"rid": "Ferrari_F40_1987",
"brand": "Ferrari",
"onlyName": "F40",
"year": 1987,
"rq": 95,
"prize": false,
"guid": "card_ferrari_f40_1987"
}
]
```
**Field Descriptions**:
| Field | Type | Description |
|-------|------|-------------|
| `rid` | String | Repository identifier (unique) |
| `brand` | String | Car manufacturer/brand name |
| `onlyName` | String | Car model name (without brand) |
| `year` | Number | Model year |
| `rq` | Number | Rarity level (10-113) |
| `prize` | Boolean | Whether car is prize-only |
| `guid` | String | Game-specific card identifier |
**Usage in Application**:
- Loaded by `get_car_list()` function
- Used to identify cars for timing file processing
- Provides metadata for analysis output
### 2. User's Car Collection (`my_cars.json`)
**Purpose**: Contains the user's owned cars exported from the Top Drives game.
**Source**: Exported from Top Drives game (Settings → Account → Export Data)
**Structure**:
```json
{
"playerDeck": [
{
"cardId": "card_ferrari_f40_1987",
"locked": true
},
{
"cardId": "card_mclaren_f1_1993",
"locked": false
}
]
}
```
**Field Descriptions**:
| Field | Type | Description |
|-------|------|-------------|
| `playerDeck` | Array | List of user's cards |
| `cardId` | String | Unique card identifier |
| `locked` | Boolean | Whether card is owned (true = owned) |
**Processing Logic**:
- Only `locked: true` cards are considered owned
- `cardId` values are matched against car database GUIDs
- Used to mark cars as "owned" in analysis results
### 3. Individual Car Timing Files (`times/{rid}.json`)
**Purpose**: Contains detailed timing data for a specific car across all tracks and conditions.
**Source**: Downloaded from `https://api.topdrivesrecords.com/car/{rid}`
**Example Structure**:
```json
{
"users": ["Player1", "Player2"],
"data": {
"111": {
"times": {
"laguna_a00": {
"t": 112.45,
"u": "Player1"
},
"monaco_a00": {
"t": 45.23,
"u": "Player2",
"down": ["Player3"]
}
},
"info": {
"topSpeed": "201",
"acel": "3.2",
"hand": "92"
}
}
},
"rid": "Ferrari_F40_1987"
}
```
**Field Descriptions**:
| Field | Type | Description |
|-------|------|-------------|
| `users` | Array | List of players who submitted times |
| `data` | Object | Timing data organized by configuration |
| `rid` | String | Car repository identifier |
**Configuration Structure** (`data.{config}`):
| Field | Type | Description |
|-------|------|-------------|
| `times` | Object | Track timing data |
| `info` | Object | Car performance stats |
**Track Timing Structure** (`times.{track}_{condition}`):
| Field | Type | Description |
|-------|------|-------------|
| `t` | Number | Time in seconds |
| `u` | String | Username who submitted the time |
| `down` | Array | Usernames who downvoted this time (optional) |
| `d` | String | Date/time of submission (optional) |
**Usage in Application**:
- Processed by `parse_car_times()` function
- Each track entry becomes a `BestCarRecord`
- Downvoted times are filtered out for quality
- Times below 1.2 seconds are considered invalid
### 4. Tracks Database (`tracks_repo.json`)
**Purpose**: Defines all tracks and their properties in Top Drives.
**Source**: Downloaded from `https://github.com/TiagoXavi/topdrives-records/raw/refs/heads/main/src/database/tracks_repo.json`
**Example Structure**:
```json
[
{
"id": 1,
"name": "Laguna Seca",
"conditions": ["00", "01", "10"],
"type": "circuit"
}
]
```
**Field Descriptions**:
| Field | Type | Description |
|-------|------|-------------|
| `id` | Number | Unique track identifier |
| `name` | String | Human-readable track name |
| `conditions` | Array | Supported surface conditions |
| `type` | String | Track category (circuit, drag, rally, etc.) |
**Usage in Application**:
- Provides track name mappings for output
- Used for track-specific analysis queries
- Supports condition filtering
## Data Processing Pipeline
### 1. Data Acquisition Phase
**Files Downloaded:**
1. `cars_repo.json` - Complete car database
2. `tracks_repo.json` - Track definitions
3. `times/{rid}.json` - Individual car timing data (1000+ files)
**Validation:**
- HTTP response codes checked
- JSON parsing errors handled gracefully
- File corruption detection
### 2. Data Parsing Phase
**Car Database Processing:**
```rust
// Parse cars_repo.json
let cars_data: Vec<Value> = serde_json::from_str(&file_content)?;
for car in cars_data {
let car = Car {
rid: car["rid"].as_str().unwrap_or(""),
brand: car["brand"].as_str().unwrap_or(""),
// ... other fields
};
}
```
**Timing Data Processing:**
```rust
// Parse individual car timing file
let json_data: Value = serde_json::from_str(&file_content)?;
if let Some(configurations) = json_data["data"].as_object() {
for (config, entry) in configurations {
if let Some(times) = entry["times"].as_object() {
for (track_id, details) in times {
// Process each track time
}
}
}
}
```
### 3. Data Transformation Phase
**Track ID Parsing:**
- Input: `"laguna_a00"` (track + condition)
- Process: Split on `"_a"` delimiter
- Output: `("laguna", "00")` (track name, condition code)
**Condition Code Translation:**
- `"00"` → `"Dry"`
- `"01"` → `"Wet"`
- `"10"` → `"Dirt"`
- `"11"` → `"Dirt Wet"`
**Time Validation:**
1. Check for downvotes: `details["down"].as_array()`
2. Validate time range: `time > 1.2`
3. Special handling: Test Bowl inversion `2000.0 / time`
### 4. Data Aggregation Phase
**Best Car Records Creation:**
```rust
BestCarRecord {
car_rid: car.rid.clone(),
brand: car.brand.clone(),
only_name: car.only_name.clone(),
year: car.year,
rq: car.rq,
config: config.clone(),
time,
owned: my_cars.contains(&car.guid),
guid: car.guid.clone(),
}
```
**RQ Range Analysis:**
- Test each car from its base RQ up to 115
- Compare against existing best times
- Update records if better performance found
## Data Quality Measures
### Validation Rules
1. **Time Thresholds**:
- Minimum time: 1.2 seconds
- Test Bowl: Inverted calculation (2000.0 / time)
2. **Community Validation**:
- Downvoted times excluded
- Multiple submissions required for reliability
3. **Data Consistency**:
- Track name normalization
- Condition code standardization
- Car identifier validation
### Error Handling
**Network Errors**:
- Failed downloads logged but don't stop processing
- Existing cached data used as fallback
- Clear error messages for user
**Parse Errors**:
- Malformed JSON handled gracefully
- Missing fields use default values
- Invalid times filtered out
**File System Errors**:
- Missing files reported clearly
- Permission issues identified
- Corrupt files detected and skipped
## Performance Considerations
### Memory Usage
**Large Data Structures**:
- `BEST_CARS` HashMap: Contains all best records across RQ ranges
- Individual timing files: Loaded and parsed sequentially
- User collection: Small, loaded once
**Optimization Strategies**:
- Concurrent processing of car files
- Rate-limited downloads (20 concurrent max)
- Streaming JSON parsing where possible
### Processing Speed
**Bottlenecks**:
1. **Network I/O**: 1000+ small files downloaded
2. **JSON Parsing**: Each timing file parsed individually
3. **HashMap Operations**: Frequent insertions with RQ range checking
**Optimizations Applied**:
1. **Concurrency**: One task per car file
2. **Connection Pooling**: Reuse HTTP connections
3. **Early Filtering**: Invalid times rejected quickly
## Data Evolution
### Version Compatibility
**Backwards Compatibility**:
- Old timing files still parseable
- New fields added gracefully
- Deprecated fields handled
**Schema Evolution**:
- New track additions supported
- Condition code extensions possible
- Car metadata fields expandable
### Update Strategy
**Incremental Updates**:
- Only changed cars need re-downloading
- Track database updated periodically
- User collection refreshed as needed
**Data Freshness**:
- Community timing data updates frequently
- Car database relatively stable
- Track definitions rarely change
## Troubleshooting Data Issues
### Common Problems
**Missing Timing Data**:
- **Symptom**: Cars show no performance data
- **Cause**: Timing file not downloaded or corrupted
- **Solution**: Re-run `fetch` command
**Incorrect Ownership**:
- **Symptom**: Wrong cars marked as owned/missing
- **Cause**: Outdated `my_cars.json` export
- **Solution**: Re-export from Top Drives game
**Track Name Issues**:
- **Symptom**: Unknown track codes in output
- **Cause**: Outdated track definitions
- **Solution**: Update `tracks_repo.json`
### Data Validation Tools
**Integrity Checks**:
1. **File Count**: Verify expected number of timing files
2. **JSON Validity**: Parse all files for syntax errors
3. **Reference Integrity**: Cross-check car and track references
**Diagnostic Commands**:
```bash
# Check for missing timing files
ls times/ | wc -l # Should be 1000+
# Validate JSON syntax
find times/ -name "*.json" -exec jq empty {} \;
# Check for common issues
grep -r "No RID" times/
```
## Future Data Structure Enhancements
### Potential Improvements
1. **Compression**: Gzip timing files to reduce storage
2. **Indexing**: Add database indexes for faster queries
3. **Caching**: Cache parsed data to avoid re-processing
4. **Incremental Updates**: Only download changed files
### Schema Extensions
**Additional Fields**:
- Car weight and dimensions
- Performance curves
- Weather condition effects
- Driver skill impact
**New Analysis Types**:
- Historical performance trends
- Meta change impact analysis
- Community submission quality metrics
- Regional performance differences

532
docs/DEVELOPMENT_SETUP.md Normal file
View file

@ -0,0 +1,532 @@
# Development Setup Guide
## Prerequisites
### System Requirements
**Operating System**: Windows 11, macOS 10.15+, or Linux (Ubuntu 18.04+, CentOS 7+)
**Architecture**: x86_64 (Intel/AMD) or ARM64 (Apple Silicon)
**Disk Space**: ~500MB free space (includes Rust toolchain and project data)
**Memory**: 2GB RAM minimum, 4GB recommended
**Network**: Broadband internet connection for downloading dependencies and data
### Required Software
1. **Rust Toolchain** (Latest Stable)
2. **Git** (for version control)
3. **Text Editor/IDE** (VS Code recommended)
## Installation Steps
### Step 1: Install Rust
**Windows:**
```powershell
# Download and run rustup installer from https://rustup.rs/
# Or use Winget:
winget install Rustlang.Rustup
```
**macOS:**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
**Linux (Ubuntu/Debian):**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt install build-essential # Required for compilation
```
**Verify Installation:**
```bash
rustc --version
cargo --version
```
**Expected Output:**
```
rustc 1.82.0 (f6e511eec 2024-10-15)
cargo 1.82.0 (f6e511eec 2024-10-15)
```
### Step 2: Configure Rust
**Add Rust to PATH** (if not automatically added):
```bash
# Add to ~/.bashrc, ~/.zshrc, or system PATH
export PATH="$HOME/.cargo/bin:$PATH"
source ~/.bashrc
```
**Update Rust** (recommended):
```bash
rustup update stable
```
**Add Nightly Toolbox** (optional, for advanced features):
```bash
rustup toolchain install nightly
rustup component add rustfmt --toolchain nightly
rustup component add clippy --toolchain nightly
```
### Step 3: Install Project Dependencies
**Clone the Repository:**
```bash
git clone <repository-url>
cd top_drives
```
**Build the Project:**
```bash
cargo build
```
**Expected Output:**
```
Compiling top_drives v0.2.0
Finished dev [unoptimized + debuginfo] target(s) in 1m 30s
```
**Verify Build:**
```bash
cargo check # Fast compilation check
cargo build --release # Optimized build
```
## Dependency Analysis
### Core Dependencies
#### 1. `tokio` - Asynchronous Runtime
```toml
tokio = { version = "1.44.1", features = ["full"] }
```
**Purpose**: Provides async/await functionality for concurrent operations
**Features Used**:
- `fs`: File system operations
- `net`: Network I/O
- `rt-multi-thread`: Multi-threaded runtime
- `macros`: `#[tokio::main]` and `#[tokio::test]`
- `sync`: Async primitives (Semaphore, Mutex)
- `task`: Task spawning and management
**Why Needed**:
- Concurrent processing of 1000+ car files
- Rate-limited HTTP downloads
- Non-blocking file I/O operations
**Memory Impact**: ~5-10MB overhead for runtime
#### 2. `reqwest` - HTTP Client
```toml
reqwest = "0.12.15"
```
**Purpose**: HTTP client for downloading car data and track definitions
**Features Used**:
- JSON parsing from HTTP responses
- Custom headers and user agents
- Connection pooling and reuse
- HTTPS support with certificate validation
**Why Needed**:
- Download car database from GitHub
- Fetch individual car timing files from API
- Retrieve track definitions
**Network Behavior**:
- Connection pool: 10-20 concurrent connections
- Timeout: 30 seconds default
- Retry logic: Built-in exponential backoff
#### 3. `serde` - Serialization Framework
```toml
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.140"
```
**Purpose**: JSON parsing and serialization for data structures
**Features Used**:
- `Deserialize` derive macro for structs
- JSON parsing with `from_str()` and `from_reader()`
- Error handling for malformed JSON
- Custom field renaming with `#[serde(rename = "...")]`
**Why Needed**:
- Parse `cars_repo.json` (car database)
- Parse `my_cars.json` (user collection)
- Parse individual car timing files
- Parse `tracks_repo.json` (track definitions)
**Performance**: Zero-copy parsing where possible
#### 4. `futures` - Future Utilities
```toml
futures = "0.3.31"
```
**Purpose**: Utilities for working with asynchronous operations
**Features Used**:
- `join_all()`: Wait for multiple futures concurrently
- `Future` trait implementations
- Stream processing utilities
**Why Needed**:
- Coordinate multiple async file downloads
- Process multiple car timing files concurrently
- Handle collections of futures
#### 5. `once_cell` - Lazy Initialization
```toml
once_cell = "1.20.2"
```
**Purpose**: Thread-safe lazy initialization of global statics
**Features Used**:
- `Lazy`: One-time initialization of expensive resources
- Thread-safe access patterns
- Interior mutability with `Mutex`
**Why Needed**:
- Global `BEST_CARS` cache initialization
- Thread-safe access during concurrent parsing
- Avoid expensive computation on every access
#### 6. `scraper` - HTML Parsing (Club Module Only)
```toml
scraper = "0.23.1"
```
**Purpose**: HTML parsing and CSS selector engine for web scraping
**Features Used**:
- CSS selector parsing with `Selector::parse()`
- HTML document parsing with `Html::parse_document()`
- Element selection and text extraction
**Why Needed**:
- Alternative data source for club.rs module
- Parse car data from topdrives.club HTML tables
- Extract performance information from web pages
### Development Dependencies
#### 7. Build Tools and Quality
**Recommended additions for development:**
```toml
[dev-dependencies]
tokio-test = "0.4"
criterion = "0.5" # Benchmarking
[[bench]]
name = "parsing_benchmark"
harness = false
```
## Development Environment Setup
### VS Code Configuration
**Recommended Extensions:**
1. **rust-analyzer**: Rust language support
2. **Better TOML**: TOML file support
3. **crates**: Rust package search
4. **Rust Test Explorer**: Test discovery and execution
**Settings** (`settings.json`):
```json
{
"rust-analyzer.check.command": "clippy",
"rust-analyzer.cargo.loadOutDirsFromCheck": true,
"rust-analyzer.procMacro.enable": true,
"files.associations": {
"*.rs": "rust"
}
}
```
### Debugging Setup
**VS Code Launch Configuration** (`.vscode/launch.json`):
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Records",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/target/debug/records",
"args": ["best"],
"cwd": "${workspaceFolder}"
}
]
}
```
## Build Configuration
### Cargo.toml Analysis
**Project Structure:**
```toml
[package]
name = "top_drives"
version = "0.2.0"
edition = "2021"
[[bin]]
name = "club"
path = "src/club.rs"
[[bin]]
name = "records"
path = "src/records.rs"
```
**Binary Configuration:**
- **records**: Main analysis tool (JSON-based)
- **club**: Alternative scraper (web-based)
**Dependency Versions:**
- All dependencies pinned to specific versions for reproducibility
- Compatible versions tested together
- Security updates monitored
## Performance Optimization
### Compilation Settings
**Debug Build:**
```bash
cargo build # Fast compilation, slow execution
```
**Release Build:**
```bash
cargo build --release # Slow compilation, fast execution
```
**Profile Configuration** (Optional `Cargo.toml`):
```toml
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
```
### Runtime Performance
**Memory Settings:**
```bash
# Increase stack size for deep recursion (if needed)
RUST_MIN_STACK=8388608 cargo run
# Enable backtraces for debugging
RUST_BACKTRACE=1 cargo run
```
**Concurrency Tuning:**
- Default: 20 concurrent downloads (configurable via Semaphore)
- Thread count: Automatic based on CPU cores
- Memory usage: ~100-200MB for full dataset
## Testing Setup
### Running Tests
```bash
cargo test # Run all tests
cargo test --release # Run tests on release build
cargo test -- --nocapture # Show test output
```
### Writing Tests
**Example Test Structure:**
```rust
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_parse_car_times() {
// Test implementation
}
}
```
**Test Data:**
- Create sample JSON files in `tests/fixtures/`
- Mock HTTP responses for network tests
- Use `tempfile` crate for temporary files
## Troubleshooting
### Common Installation Issues
**"rustc not found"**
- **Cause**: Rust not in PATH
- **Solution**: Add `$HOME/.cargo/bin` to PATH
**"Permission denied" (Unix)**
- **Cause**: Insufficient permissions
- **Solution**: Check file permissions or use `sudo` for system-wide install
**"Linker not found" (Linux)**
- **Cause**: Missing build tools
- **Solution**: Install `build-essential` (Ubuntu) or `gcc` (CentOS)
### Build Issues
**"Package not found"**
- **Cause**: Missing dependencies
- **Solution**: Run `cargo fetch` or `cargo build` with internet
**"Conflicting versions"**
- **Cause**: Dependency version conflicts
- **Solution**: Update Cargo.lock with `cargo update`
**"Compilation failed"**
- **Cause**: Syntax or type errors
- **Solution**: Check error messages, fix code issues
### Runtime Issues
**"Thread panicked"**
- **Cause**: Unhandled error conditions
- **Solution**: Add proper error handling, check unwrap() calls
**"Out of memory"**
- **Cause**: Large dataset processing
- **Solution**: Process data in batches, increase system memory
## Development Workflow
### 1. Code Changes
```bash
# Make changes to source files
code src/records.rs
# Check compilation
cargo check
# Run tests
cargo test
# Format code
cargo fmt
# Lint code
cargo clippy
```
### 2. Testing Changes
```bash
# Test specific functionality
cargo test parse_car_times
# Benchmark performance
cargo bench
# Check for memory leaks
valgrind cargo run
```
### 3. Debugging
```bash
# Debug build with symbols
cargo build
# Run with debugger
gdb target/debug/records
# Profile performance
cargo flamegraph --bin records -- best
```
## Deployment
### Binary Distribution
**Release Build:**
```bash
cargo build --release
```
**Binary Location:**
```
target/release/records # Main analyzer
target/release/club # Club scraper
```
**Cross-Platform Building:**
```bash
# Build for Windows from Linux/macOS
cargo build --target x86_64-pc-windows-gnu --release
# Build for macOS from Linux/Windows
cargo build --target x86_64-apple-darwin --release
```
## Maintenance
### Regular Tasks
**Update Dependencies:**
```bash
cargo update # Update to latest compatible versions
cargo outdated # Check for outdated packages
```
**Security Updates:**
```bash
cargo audit # Check for security vulnerabilities
cargo update # Apply security patches
```
**Performance Monitoring:**
```bash
cargo bloat # Check binary size
cargo profdata # Profile-guided optimization
```
## Getting Help
### Documentation
- **Rust Book**: https://doc.rust-lang.org/book/
- **Cargo Guide**: https://doc.rust-lang.org/cargo/
- **Tokio Tutorial**: https://tokio.rs/tokio/tutorial
### Community Resources
- **Rust Users Forum**: https://users.rust-lang.org/
- **Stack Overflow**: Tag `rust`
- **Discord**: Rust community servers
### Project-Specific Help
- Check existing documentation in `docs/` directory
- Review issue tracker for known problems
- Create new issues for bugs or feature requests
---
**Development Environment Summary:**
- **Rust**: 1.82.0+ (latest stable recommended)
- **Build Tool**: Cargo (included with Rust)
- **Editor**: VS Code with rust-analyzer (recommended)
- **Testing**: Built-in Rust testing framework
- **Debugging**: GDB/LLDB integration available

334
docs/USER_GUIDE.md Normal file
View file

@ -0,0 +1,334 @@
# Top Drives Records Analyzer - User Guide
## Introduction
This guide provides comprehensive instructions for using the Top Drives Records Analyzer to optimize your car collection and improve your racing performance. The tool analyzes timing data from thousands of community-submitted runs to help you make informed decisions about your car collection.
## Quick Start Checklist
- [ ] Install Rust development environment
- [ ] Export your car collection from Top Drives game
- [ ] Run initial data fetch
- [ ] Analyze your collection
- [ ] Review recommendations
## Setup Instructions
### 1. Game Data Export
**Export your car collection from Top Drives:**
1. Open Top Drives on your device
2. Go to Settings → Account → Export Data
3. Save the file as `my_cars.json` in the project directory
### 2. Initial Data Download
Fetch the latest car and track databases:
```bash
cargo run --bin records -- fetch
```
**What this does:**
- Downloads `cars_repo.json` (car database)
- Downloads `tracks_repo.json` (track definitions)
- Downloads timing files for all cars (1000+ files in `times/` directory)
**Expected output:**
```
Fetching cars file...
Fetching tracks file...
Fetching https://api.topdrivesrecords.com/car/1001.json.
Fetching https://api.topdrivesrecords.com/car/1002.json.
...
```
## Command Reference
### Basic Analysis Commands
#### Best Cars Analysis
**Command:**
```bash
cargo run --bin records -- best
```
**Purpose:** Shows the best performing cars in your collection, ranked by how often they achieve top times.
**Sample Output:**
```csv
RQ,Brand,Model,Year,Count
85,McLaren,F1,1993,15
82,Ferrari,250 GTO,1962,12
80,Lamborghini,Countach,1974,8
75,Porsche,911 Turbo,1975,6
```
**Understanding the output:**
- **RQ**: Car's rarity level
- **Brand**: Car manufacturer
- **Model**: Car model name
- **Year**: Model year
- **Count**: How many times this car was the fastest across different tracks
**Interpretation:**
- Higher counts indicate more versatile cars
- Focus on cars with counts ≥ 5 for your core collection
- Compare with your owned cars to identify strengths
#### Missing Cars Analysis
**Command:**
```bash
cargo run --bin records -- missing
```
**Purpose:** Identifies high-performing cars you don't own that would improve your collection.
**Sample Output:**
```csv
RQ,Brand,Model,Year,Count
90,Pagani,Zonda R,2010,18
88,Ferrari,F40,1987,15
87,McLaren,Senna,2018,12
```
**Action items:**
- Prioritize cars with high counts (≥10)
- Consider RQ restrictions in your racing bracket
- Look for cars that perform well across multiple track types
#### Collection Optimization (Trash Analysis)
**Command:**
```bash
cargo run --bin records -- trash
```
**Purpose:** Identifies cars safe to remove from your collection.
**Sample Output:**
```csv
RQ,Brand,Model,Year
45,Toyota,Corolla,1995
52,Honda,Civic,1992
38,Ford,Focus,2001
```
**Safety guidelines:**
- Only removes cars below RQ 80
- Excludes prize cars (may have collection value)
- Only suggests cars that never achieve best times
- **Always verify before selling in-game**
#### Detailed Performance Data
**Command:**
```bash
cargo run --bin records -- details
```
**Purpose:** Comprehensive timing data for all tracks and conditions.
**Sample Output:**
```csv
RQ,TrackID,Condition,CarRQ,Brand,Model,Year,Config,Time
85,laguna,Dry,85,McLaren,F1,1993,233,112.45
85,monaco,Dry,85,Pagani,Zonda,2002,111,45.23
82,laguna,Dry,82,Ferrari,250 GTO,1962,111,115.67
```
**Use cases:**
- External spreadsheet analysis
- Track-specific performance research
- Historical performance tracking
#### Track-Specific Analysis
**Command:**
```bash
cargo run --bin records -- track "Laguna Seca" "Dry" "85"
```
**Purpose:** Find the best car for a specific track, condition, and RQ level.
**Sample Output:**
```
Best car for Track: laguna, Condition: Dry, RQ: 85 -> McLaren F1 (1993) with time 112.45
```
**Track name format:**
- Use internal names: `laguna`, `monaco`, `testBowl`
- Full mappings in `translate_track_name()` function
**Condition options:**
- `Dry` - Standard asphalt
- `Wet` - Rain conditions
- `Dirt` - Off-road tracks
- `Snow` - Winter conditions
## Advanced Usage
### RQ Range Analysis
The tool analyzes performance across RQ brackets. Understanding RQ ranges:
- **RQ 10-50**: Entry level cars
- **RQ 51-80**: Mid-tier performance
- **RQ 81-100**: High performance
- **RQ 101-113**: Elite/supercars
**Strategy:**
- Focus on 2-3 RQ brackets where you race most
- Identify cars that perform well across multiple brackets
- Consider upgrade paths for favorite cars
### Collection Planning
#### Step 1: Assess Current Collection
```bash
cargo run --bin records -- best
```
- Identify your strongest cars
- Note RQ distribution
- Find gaps in coverage
#### Step 2: Identify Priorities
```bash
cargo run --bin records -- missing
```
- Focus on high-count cars in your RQ range
- Consider track variety needs
- Factor in prize car availability
#### Step 3: Optimize Collection
```bash
cargo run --bin records -- trash
```
- Remove redundant low performers
- Free up space for better cars
- Maintain diversity for different track types
### Track Type Strategy
Different cars excel on different track types:
#### Speed Tracks
- **Characteristics**: Long straights, high speeds
- **Best cars**: High top speed, good acceleration
- **Examples**: Laguna Seca, Monaco Streets
#### Technical Tracks
- **Characteristics**: Tight corners, elevation changes
- **Best cars**: Good handling, braking
- **Examples**: Deals Gap, Mountain Hairpin
#### Off-Road Tracks
- **Characteristics**: Dirt, gravel, snow surfaces
- **Best cars**: Good grip, durability
- **Examples**: Desert Rally, Forest Road
## Output File Analysis
### CSV File Usage
The tool generates CSV files for external analysis:
#### best.csv
- Your best performing cars
- Import into spreadsheet for sorting/filtering
- Track performance across time
#### missing.csv
- Cars to acquire
- Prioritize by count and RQ relevance
- Plan acquisition strategy
#### details.csv
- Comprehensive timing database
- Advanced analysis and research
- Historical performance tracking
### Spreadsheet Analysis Tips
**Excel/Google Sheets setup:**
1. Import CSV files
2. Create pivot tables by RQ, Brand, Track
3. Add conditional formatting for high performers
4. Create dashboards for collection overview
## Troubleshooting
### Common Issues
#### "Please provide a command"
**Problem:** No arguments provided
**Solution:** Use `best`, `missing`, `trash`, `details`, or `track`
#### "Could not read my cars file"
**Problem:** `my_cars.json` not found or not readable
**Solution:**
1. Export fresh data from Top Drives game
2. Ensure file is named exactly `my_cars.json`
3. Check file permissions
#### "Failed to fetch the file"
**Problem:** Network issues or API unavailable
**Solution:**
1. Check internet connection
2. Try again later (may be temporary)
3. Use existing cached data if available
#### No output or empty results
**Problem:** Missing timing data or configuration issues
**Solution:**
1. Re-run `fetch` command to get fresh data
2. Check that `times/` directory has files
3. Verify `cars_repo.json` exists
### Performance Issues
#### Slow analysis
- **Expected**: First run takes 2-3 minutes (downloads data)
- **Subsequent runs**: Should be fast (uses cached data)
- **Large datasets**: Details command processes all timing data
#### Memory usage
- **Normal**: 100-200MB for full analysis
- **High**: Details command loads all timing data
- **Solution**: Use specific commands instead of details for large datasets
## Tips and Best Practices
### Regular Maintenance
1. **Weekly**: Run `fetch` to get latest data
2. **Monthly**: Review `missing` cars for new acquisitions
3. **Quarterly**: Use `trash` to optimize collection
### Data-Driven Decisions
1. **Trust the numbers**: Count-based recommendations are data-driven
2. **Consider context**: Some cars may have strategic value beyond performance
3. **Track changes**: Keep performance history for trend analysis
### Collection Strategy
1. **Diversify**: Don't focus only on one RQ bracket
2. **Specialize**: Identify your preferred track types
3. **Balance**: Mix proven performers with potential upgrades
## Getting Help
### Documentation
- `README.md`: Project overview and setup
- `docs/ARCHITECTURE.md`: Technical details for developers
- `docs/USER_GUIDE.md`: This comprehensive guide
### Community Resources
- **Data Source**: Community timing database
- **Updates**: Check for new features and improvements
- **Issues**: Report bugs or request features
---
**Remember**: This tool provides data-driven insights, but ultimately you're the best judge of which cars you enjoy driving. Use the analysis as a guide, not a strict rulebook.

View file

@ -1,4 +1,29 @@
use std::collections::{HashMap};
//! # Top Drives Club Scraper
//!
//! Alternative data source for the Top Drives Records Analyzer that scrapes car
//! performance data from the topdrives.club website. This provides an alternative
//! to the JSON-based records system for fetching best car information.
//!
//! ## Purpose
//!
//! While the main `records.rs` module uses pre-downloaded JSON timing files,
//! this module can fetch fresh data directly from the topdrives.club website
//! by scraping HTML tables and parsing car information.
//!
//! ## Features
//!
//! - Scrapes best car data from topdrives.club
//! - Supports track-specific car recommendations
//! - Configurable RQ ranges and track filtering
//! - HTML parsing with fallback error handling
//!
//! ## Dependencies
//!
//! - `reqwest`: HTTP client for web scraping
//! - `scraper`: HTML parsing and CSS selector library
//! - `tokio`: Asynchronous runtime for web requests
use std::collections::HashMap;
use std::env;
use std::fs::read_to_string;
use std::str::FromStr;
@ -7,14 +32,48 @@ use tokio;
use scraper::{Html, Selector};
/// Represents a car entry scraped from the topdrives.club website
///
/// Contains the essential car information needed for performance analysis,
/// parsed from the HTML tables on the topdrives.club best cars pages.
struct Car {
/// Car class letter (A, B, C, D, E, S)
class: String,
/// Rarity level (10-113)
rq: u8,
/// Car manufacturer/brand name
manufacturer: String,
/// Car model name
model: String,
/// Car model year
year: u16,
/// Upgrade configuration string (e.g., "333", "323")
upgrade: String,
}
/// Fetches HTML content from a URL with error handling
///
/// Downloads the complete HTML content from the specified URL. Uses a custom
/// HTTP client that accepts invalid SSL certificates to handle potential
/// certificate issues with the target website.
///
/// # Arguments
///
/// * `url` - The URL to fetch HTML content from
///
/// # Returns
///
/// Returns the HTML content as a String, or an empty string if the request fails.
/// Failures are logged to the console but don't cause the program to panic.
///
/// # Security Note
///
/// Uses `danger_accept_invalid_certs(true)` to handle websites with certificate
/// issues. This should only be used with trusted sources like topdrives.club.
///
/// # Error Handling
///
/// Returns empty string on any network or parsing error, allowing the calling
/// code to handle the absence of data gracefully.
async fn get_page(url: &str) -> String {
match reqwest::Client::builder()
.danger_accept_invalid_certs(true)
@ -38,7 +97,236 @@ async fn get_page(url: &str) -> String {
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Creates mock HTML for testing car parsing
fn create_mock_car_html() -> String {
r#"
<div class="carList__item-manufacturer">Ferrari</div>
<div class="carList__item-model">F40</div>
<div class="carList__item-year">1987</div>
<div class="carList__item-level">
<span>S</span>
<span>95</span>
</div>
<div class="carList__item-upgrade">323</div>
"#
.to_string()
}
/// Creates mock HTML with missing elements
fn create_incomplete_car_html() -> String {
r#"
<div class="carList__item-manufacturer">McLaren</div>
<div class="carList__item-model">F1</div>
<div class="carList__item-year">1993</div>
"#
.to_string()
}
/// Creates mock tracks file content
fn create_mock_tracks_content() -> String {
"1 Laguna Seca\n2 Monaco Streets\n3 Test Bowl\n".to_string()
}
#[test]
fn test_parse_best_car_complete() {
let html = create_mock_car_html();
let car = parse_best_car(html);
assert_eq!(car.manufacturer, "Ferrari");
assert_eq!(car.model, "F40");
assert_eq!(car.year, 1987);
assert_eq!(car.class, "S");
assert_eq!(car.rq, 95);
assert_eq!(car.upgrade, "323");
}
#[test]
fn test_parse_best_car_incomplete() {
let html = create_incomplete_car_html();
let car = parse_best_car(html);
// Should use default values for missing fields
assert_eq!(car.manufacturer, "McLaren");
assert_eq!(car.model, "F1");
assert_eq!(car.year, 1993);
assert_eq!(car.class, "Z"); // Default value
assert_eq!(car.rq, 0); // Default value
assert_eq!(car.upgrade, "000"); // Default value
}
#[test]
fn test_parse_best_car_empty() {
let html = String::new();
let car = parse_best_car(html);
// Should use all default values
assert_eq!(car.class, "Z");
assert_eq!(car.rq, 0);
assert_eq!(car.manufacturer, "");
assert_eq!(car.model, "");
assert_eq!(car.year, 1900);
assert_eq!(car.upgrade, "000");
}
#[test]
fn test_parse_tracks() {
let content = create_mock_tracks_content();
let tracks = parse_tracks_from_string(content);
assert_eq!(tracks.len(), 3);
assert_eq!(tracks.get(&1), Some(&"Laguna Seca".to_string()));
assert_eq!(tracks.get(&2), Some(&"Monaco Streets".to_string()));
assert_eq!(tracks.get(&3), Some(&"Test Bowl".to_string()));
assert_eq!(tracks.get(&99), None);
}
#[test]
fn test_parse_tracks_empty() {
let content = String::new();
let tracks = parse_tracks_from_string(content);
assert_eq!(tracks.len(), 0);
}
#[test]
fn test_parse_argument_valid() {
let args = vec!["program".to_string(), "85".to_string()];
let result = parse_argument(&args, 1, 10, 113);
assert_eq!(result, 85);
}
#[test]
#[should_panic(expected = "Argument 1 is out of range")]
fn test_parse_argument_too_low() {
let args = vec!["program".to_string(), "5".to_string()];
parse_argument(&args, 1, 10, 113);
}
#[test]
#[should_panic(expected = "Argument 1 is out of range")]
fn test_parse_argument_too_high() {
let args = vec!["program".to_string(), "150".to_string()];
parse_argument(&args, 1, 10, 113);
}
#[test]
#[should_panic(expected = "Argument 1 could not be parsed")]
fn test_parse_argument_invalid() {
let args = vec!["program".to_string(), "invalid".to_string()];
parse_argument(&args, 1, 10, 113);
}
#[test]
fn test_parse_argument_boundary_values() {
let args_low = vec!["program".to_string(), "10".to_string()];
let result_low = parse_argument(&args_low, 1, 10, 113);
assert_eq!(result_low, 10);
let args_high = vec!["program".to_string(), "113".to_string()];
let result_high = parse_argument(&args_high, 1, 10, 113);
assert_eq!(result_high, 113);
}
#[tokio::test]
async fn test_get_best_car_url_construction() {
// Test that URL construction follows expected pattern
// Note: We can't easily test the actual HTTP request without mocking
let track = 1;
let track_name = "Laguna Seca".to_string();
let max_level = 85;
let detailed = false;
// This would be the expected URL format (for documentation purposes)
let _expected_url_pattern = "https://topdrives.club/ajax/load-best-vehicles";
// In a real test environment, we'd mock the HTTP request
// For now, we verify the function doesn't panic with valid inputs
let result = get_best_car(track, track_name, max_level, detailed).await;
// Result may be empty due to network requirements, but shouldn't panic
assert!(result.is_empty() || result.len() > 0);
}
#[test]
fn test_car_struct_creation() {
let car = Car {
class: "S".to_string(),
rq: 95,
manufacturer: "Ferrari".to_string(),
model: "F40".to_string(),
year: 1987,
upgrade: "323".to_string(),
};
assert_eq!(car.class, "S");
assert_eq!(car.rq, 95);
assert_eq!(car.manufacturer, "Ferrari");
assert_eq!(car.model, "F40");
assert_eq!(car.year, 1987);
assert_eq!(car.upgrade, "323");
}
#[test]
fn test_parse_tracks_malformed_lines() {
// Test handling of malformed track file lines
let content = "1 Laguna Seca\ninvalid line\n2 Monaco Streets\n".to_string();
let tracks = parse_tracks_from_string(content);
// Should only parse valid lines
assert_eq!(tracks.len(), 2);
assert_eq!(tracks.get(&1), Some(&"Laguna Seca".to_string()));
assert_eq!(tracks.get(&2), Some(&"Monaco Streets".to_string()));
}
/// Helper function to test parse_tracks without file I/O
fn parse_tracks_from_string(content: String) -> HashMap<u16, String> {
let mut result: HashMap<u16, String> = HashMap::new();
for line in content.lines() {
if let Some((index, track)) = line.split_once(' ') {
if let Ok(id) = index.parse::<u16>() {
result.insert(id, String::from(track));
}
}
}
result
}
}
/// Parses HTML content to extract the best car information
///
/// Uses CSS selectors to extract car details from the HTML table structure
/// used by topdrives.club. The function parses manufacturer, model, year,
/// class, RQ level, and upgrade configuration from the page content.
///
/// # Arguments
///
/// * `text` - The HTML content containing the car information table
///
/// # Returns
///
/// Returns a `Car` struct populated with the parsed information. If parsing
/// fails at any step, default values are used to prevent crashes.
///
/// # Parsing Strategy
///
/// 1. **Manufacturer**: Extracts from `.carList__item-manufacturer` element
/// 2. **Model**: Extracts from `.carList__item-model` element
/// 3. **Year**: Extracts from `.carList__item-year` element
/// 4. **Class & RQ**: Complex parsing from `.carList__item-level` element
/// - Inner HTML contains both class letter and RQ number
/// - Uses nested HTML parsing to separate the two values
/// 5. **Upgrade**: Extracts from `.carList__item-upgrade` element
///
/// # Error Handling
///
/// Uses `unwrap_or` defaults for all fields to ensure the function never
/// panics due to malformed HTML or missing elements.
fn parse_best_car(text: String) -> Car {
// Initialize with default values in case parsing fails
let mut car = Car {
class: String::from("Z"),
rq: 0,
@ -50,18 +338,21 @@ fn parse_best_car(text: String) -> Car {
let document = Html::parse_document(&text);
// Extract car manufacturer name
let manufacturer_selector = Selector::parse(".carList__item-manufacturer").unwrap();
car.manufacturer = String::from(match document.select(&manufacturer_selector).next() {
None => "",
Some(manufacturer) => manufacturer.text().collect::<Vec<_>>()[0],
});
// Extract car model name
let model_selector = Selector::parse(".carList__item-model").unwrap();
car.model = String::from(match document.select(&model_selector).next() {
None => "",
Some(model) => model.text().collect::<Vec<_>>()[0],
});
// Extract car model year
let year_selector = Selector::parse(".carList__item-year").unwrap();
car.year = u16::from_str(match document.select(&year_selector).next() {
None => "1900",
@ -69,14 +360,21 @@ fn parse_best_car(text: String) -> Car {
})
.unwrap();
// Parse RQ level and class (more complex as they're in the same element)
let level_selector = Selector::parse(".carList__item-level").unwrap();
let rq_raw = document
.select(&level_selector)
.next()
.unwrap()
.inner_html();
let rq_raw = match document.select(&level_selector).next() {
Some(element) => element.inner_html(),
None => {
// If level element is missing, use defaults
car.class = "Z".to_string();
car.rq = 0;
return car;
}
};
let rq_fragment = Html::parse_document(rq_raw.trim());
// Extract class letter and RQ number from nested spans
let span_selector = Selector::parse("span").unwrap();
let mut element_iterator = rq_fragment.select(&span_selector);
let class = String::from(match element_iterator.next() {
@ -90,6 +388,7 @@ fn parse_best_car(text: String) -> Car {
};
car.rq = u8::from_str(rq).unwrap();
// Extract upgrade configuration
let upgrade_selector = Selector::parse(".carList__item-upgrade ").unwrap();
let upgrade_raw = match document.select(&upgrade_selector).next() {
None => "",
@ -100,9 +399,37 @@ fn parse_best_car(text: String) -> Car {
car
}
/// Parses the tracks definition file to create track ID to name mappings
///
/// Reads a text file containing track definitions where each line consists
/// of a track ID number followed by the track name. Creates a HashMap for
/// efficient track ID to name lookups.
///
/// # Arguments
///
/// * `file_name` - Path to the tracks definition file
///
/// # Returns
///
/// Returns a `HashMap<u16, String>` mapping track IDs to track names.
///
/// # File Format
///
/// The tracks file should have one track per line in the format:
/// ```text
/// 1 Laguna Seca
/// 2 Monaco Streets
/// 3 Test Bowl
/// ```
///
/// # Error Handling
///
/// Uses `unwrap()` for simplicity - in production, proper error handling
/// should be implemented for file I/O and parsing errors.
fn parse_tracks(file_name: &str) -> HashMap<u16, String> {
let mut result: HashMap<u16, String> = HashMap::new();
// Process each line in the tracks file
for line in read_to_string(file_name).unwrap().lines() {
let (index, track) = line.split_once(' ').unwrap();
result.insert(index.parse::<u16>().unwrap(), String::from(track));
@ -111,6 +438,33 @@ fn parse_tracks(file_name: &str) -> HashMap<u16, String> {
result
}
/// Main entry point for the Top Drives Club scraper
///
/// Scrapes best car data from topdrives.club for specified tracks and RQ ranges.
/// Provides an alternative data source to the JSON-based records system.
///
/// # Command Line Arguments
///
/// - No args: Process all tracks with default RQ range (10-113)
/// - 1 arg: Set maximum RQ level for all tracks
/// - 2 args: Set max RQ and process single track
/// - 3 args: Set max RQ, start track, and end track for range processing
/// - `-d` flag: Enable detailed output format
///
/// # Arguments
///
/// * `max_rq` - Maximum RQ level to consider (default: 113)
/// * `min_track` - First track ID to process (default: 4)
/// * `max_track` - Last track ID to process (default: 278)
/// * `detailed` - Whether to show detailed output with upgrade info
///
/// # Example Usage
///
/// ```bash
/// cargo run --bin club # All tracks, default RQ
/// cargo run --bin club -d 100 # All tracks, RQ <= 100, detailed
/// cargo run --bin club 85 1 50 # Tracks 1-50, RQ <= 85
/// ```
#[tokio::main]
async fn main() {
let mut args: Vec<_> = env::args().collect();
@ -163,7 +517,10 @@ async fn main() {
let best_car = get_best_car(track, track_name.clone(), level, detailed).await;
if best_car.len() > 10 {
// println!("Best Car under RQ {} for track {} ({}): {}", level, track, track_name, best_car);
keepers.entry(best_car).and_modify(|counter| { *counter += 1 } ).or_insert(1);
keepers
.entry(best_car)
.and_modify(|counter| *counter += 1)
.or_insert(1);
}
}
}
@ -173,11 +530,37 @@ async fn main() {
for car in keepers {
match detailed {
true => println!("{}", car.0),
false => println!("{} {}", car.0, car.1)
false => println!("{} {}", car.0, car.1),
}
}
}
/// Parses and validates command line arguments with range checking
///
/// Validates that numeric arguments are within acceptable ranges and provides
/// clear error messages for invalid input. Used for RQ levels and track IDs.
///
/// # Arguments
///
/// * `args` - The command line arguments vector
/// * `index` - Index of the argument to parse
/// * `min` - Minimum acceptable value
/// * `max` - Maximum acceptable value
///
/// # Returns
///
/// Returns the parsed and validated value
///
/// # Panics
///
/// Panics with descriptive error messages if:
/// - The argument cannot be parsed as a number
/// - The value is outside the acceptable range
///
/// # Error Messages
///
/// Provides clear feedback about expected value ranges and which argument
/// position is problematic.
fn parse_argument(args: &Vec<String>, index: usize, min: u16, max: u16) -> u16 {
match args[index].parse::<u16>() {
Ok(parsed_value) => {
@ -196,11 +579,54 @@ fn parse_argument(args: &Vec<String>, index: usize, min: u16, max: u16) -> u16 {
}
}
/// Fetches and parses the best car data for a specific track from topdrives.club
///
/// Constructs the appropriate URL for the topdrives.club API, fetches the HTML
/// response, and parses it to extract the best car information for the given
/// track and RQ constraints.
///
/// # Arguments
///
/// * `track` - The track ID number (1-278)
/// * `track_name` - Human-readable track name for display
/// * `max_level` - Maximum RQ level to consider
/// * `detailed` - Whether to include upgrade configuration in output
///
/// # Returns
///
/// Returns a formatted string containing the best car information, or an empty
/// string if the request fails or no data is available.
///
/// # API Details
///
/// The function constructs a URL with the following parameters:
/// - `sort_by=level&sortorder=desc`: Sort by RQ level, highest first
/// - `prize=all`: Include all cars, not just prize cars
/// - `level=10%3B{max_level}`: RQ range from 10 to max_level
/// - `acceleration=1%3B50&grip=40%3B110`: Performance filters
/// - `year=1900%3B2030`: Year range filter
/// - `seats_count=1%3B9`: Include all seat counts
/// - `track={track}`: Specific track ID
/// - `page=0`: First page of results
///
/// # Output Formats
///
/// **Detailed mode:**
/// ```text
/// S85 Ferrari 250 GTO 1962: Laguna Seca (333)
/// ```
///
/// **Normal mode:**
/// ```text
/// S85 Ferrari 250 GTO 1962
/// ```
async fn get_best_car(track: u16, track_name: String, max_level: u16, detailed: bool) -> String {
// Construct the API URL with all necessary parameters
let url = format!("https://topdrives.club/ajax/load-best-vehicles?sort_by=level&sortorder=desc&prize=all&level=10%3B{}&acceleration=1%3B50&grip=40%3B110&year=1900%3B2030&seats_count=1%3B9&track={}&page=0",
max_level, track);
max_level, track);
// println!("Getting {}", url);
let text = get_page(&url).await;
if text.len() > 10 {
let best_car = parse_best_car(text);
@ -208,27 +634,28 @@ async fn get_best_car(track: u16, track_name: String, max_level: u16, detailed:
true => {
format!(
"{}{} {} {} {}: {} ({})",
best_car.class,
best_car.rq,
best_car.manufacturer,
best_car.model,
best_car.year,
track_name,
best_car.upgrade,
best_car.class, // Car class (S, A, B, etc.)
best_car.rq, // RQ level
best_car.manufacturer, // Brand name
best_car.model, // Model name
best_car.year, // Model year
track_name, // Track name
best_car.upgrade, // Upgrade configuration
)
}
false => {
format!(
"{}{} {} {} {}",
best_car.class,
best_car.rq,
best_car.manufacturer,
best_car.model,
best_car.year,
best_car.class, // Car class
best_car.rq, // RQ level
best_car.manufacturer, // Brand name
best_car.model, // Model name
best_car.year, // Model year
)
}
}
} else {
// No data received or request failed
String::from("")
}
}

File diff suppressed because it is too large Load diff