Added documentation
Added test cases Added comments
This commit is contained in:
parent
7a8d9beedc
commit
32c4d3c10e
10 changed files with 3852 additions and 63 deletions
469
src/club.rs
469
src/club.rs
|
|
@ -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("")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
805
src/records.rs
805
src/records.rs
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue