diff --git a/car-classes.txt b/car-classes.txt new file mode 100644 index 0000000..3fb5432 --- /dev/null +++ b/car-classes.txt @@ -0,0 +1,14 @@ +Piccino +Superlight +Eurotruck +Muscle Car +Stock Car +Super Truck +Rally +50s GT +Touring Car +GT +Prototype +60s GP +80s GP +GP \ No newline at end of file diff --git a/config.json b/config.json index 8a63bb6..a13ec06 100644 --- a/config.json +++ b/config.json @@ -1,55 +1,56 @@ -{ - "ocr_regions": [ - { - "name": "lap", - "x": 2300, - "y": 46, - "width": 145, - "height": 90 - }, - { - "name": "health", - "x": 90, - "y": 1364, - "width": 52, - "height": 24 - }, - { - "name": "gas", - "x": 208, - "y": 1364, - "width": 52, - "height": 24 - }, - { - "name": "tyres", - "x": 325, - "y": 1364, - "width": 52, - "height": 24 - }, - { - "name": "best", - "x": 2325, - "y": 169, - "width": 183, - "height": 43 - }, - { - "name": "lap_time", - "x": 2325, - "y": 222, - "width": 183, - "height": 43 - } - ], - "track_region": { - "name": "track", - "x": 2020, - "y": 1030, - "width": 540, - "height": 410, - "threshold": 0.85 - }, - "ocr_server_endpoint": "https://tesserver.spruett.dev/" +{ + "ocr_regions": [ + { + "name": "lap", + "x": 2300, + "y": 46, + "width": 140, + "height": 90 + }, + { + "name": "health", + "x": 90, + "y": 1364, + "width": 52, + "height": 24 + }, + { + "name": "gas", + "x": 208, + "y": 1364, + "width": 52, + "height": 24 + }, + { + "name": "tyres", + "x": 325, + "y": 1364, + "width": 52, + "height": 24 + }, + { + "name": "best", + "x": 2325, + "y": 169, + "width": 183, + "height": 43 + }, + { + "name": "lap_time", + "x": 2325, + "y": 222, + "width": 183, + "height": 43, + "use_ocr_cache": false + } + ], + "track_region": { + "name": "track", + "x": 2020, + "y": 1030, + "width": 540, + "height": 410, + "threshold": 0.85 + }, + "ocr_server_endpoint": "https://tesserver.spruett.dev/" } \ No newline at end of file diff --git a/race_stats.csv b/race_stats.csv new file mode 100644 index 0000000..d3695bb --- /dev/null +++ b/race_stats.csv @@ -0,0 +1,3 @@ +2022-05-22-20:39,whistle valley,gp,1,22.349,22.349,100,, +2022-05-22-20:39,whistle valley,gp,2,21.888,21.888,99,83,82 +2022-05-22-20:39,whistle valley,gp,3,22.031,21.888,99,75,73 diff --git a/scale_config.py b/scale_config.py index 5099b3d..2ca378c 100644 --- a/scale_config.py +++ b/scale_config.py @@ -1,42 +1,42 @@ -#!/usr/bin/env python3 - -import argparse -import json -from typing import Tuple - - -def parse_resolution(resolution: str) -> Tuple[int, int]: - a, b = resolution.split('x') - return int(a), int(b) - -def scale_x_y(x, y, from_resolution, to_resolution): - return (x * to_resolution[0] / from_resolution[0], y * to_resolution[1] / from_resolution[1]) - -def scale_region(region, from_resolution, to_resolution): - x, y = scale_x_y(region['x'], region['y'], from_resolution, to_resolution) - width, height = scale_x_y(region['width'], region['height'], from_resolution, to_resolution) - region['x'] = round(x) - region['y'] = round(y) - region['width'] = round(width) - region['height'] = round(height) - -def main(): - argparser = argparse.ArgumentParser() - argparser.add_argument("--from_res", help="From resolution", default="2560x1440") - argparser.add_argument("--to_res", help="To resolution (e.g. 1920x1080)") - argparser.add_argument("--config", help="Config file", default="config.json") - - args = argparser.parse_args() - - from_resolution = parse_resolution(args.from_res) - to_resolution = parse_resolution(args.to_res) - config = json.load(open(args.config, 'r')) - for region in config['ocr_regions']: - scale_region(region, from_resolution, to_resolution) - scale_region(config['track_region'], from_resolution, to_resolution) - print(json.dumps(config, indent=4)) - - - -if __name__ == '__main__': +#!/usr/bin/env python3 + +import argparse +import json +from typing import Tuple + + +def parse_resolution(resolution: str) -> Tuple[int, int]: + a, b = resolution.split('x') + return int(a), int(b) + +def scale_x_y(x, y, from_resolution, to_resolution): + return (x * to_resolution[0] / from_resolution[0], y * to_resolution[1] / from_resolution[1]) + +def scale_region(region, from_resolution, to_resolution): + x, y = scale_x_y(region['x'], region['y'], from_resolution, to_resolution) + width, height = scale_x_y(region['width'], region['height'], from_resolution, to_resolution) + region['x'] = round(x) + region['y'] = round(y) + region['width'] = round(width) + region['height'] = round(height) + +def main(): + argparser = argparse.ArgumentParser() + argparser.add_argument("--from_res", help="From resolution", default="2560x1440") + argparser.add_argument("--to_res", help="To resolution (e.g. 1920x1080)") + argparser.add_argument("--config", help="Config file", default="config.json") + + args = argparser.parse_args() + + from_resolution = parse_resolution(args.from_res) + to_resolution = parse_resolution(args.to_res) + config = json.load(open(args.config, 'r')) + for region in config['ocr_regions']: + scale_region(region, from_resolution, to_resolution) + scale_region(config['track_region'], from_resolution, to_resolution) + print(json.dumps(config, indent=4)) + + + +if __name__ == '__main__': main() \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 3e17601..0364a0e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,56 +1,56 @@ -use std::{collections::HashMap, path::PathBuf}; - -use anyhow::Result; -use serde::{Serialize, Deserialize, de::DeserializeOwned}; - -use crate::image_processing::Region; - -#[derive(Default, Serialize, Deserialize, Clone)] -pub struct Config { - pub ocr_regions: Vec, - pub track_region: Option, - pub ocr_server_endpoint: String, - pub filter_threshold: Option, - pub use_ocr_cache: Option, - pub ocr_interval_ms: Option, -} - -impl Config { - pub fn load() -> Result { - load_or_make_default("config.json", include_str!("configs/config.default.json")) - } -} - -#[derive(Default, Serialize, Deserialize, Clone)] -pub struct LearnedConfig { - pub learned_images: HashMap, - pub learned_tracks: HashMap, -} - -impl LearnedConfig { - pub fn load() -> Result { - load_or_make_default("learned.json", include_str!("configs/learned.default.json")) - } - pub fn save(&self) -> Result<()> { - save_json_config("learned.json", self) - } -} - -fn load_or_make_default(path: &str, default: &str) -> Result { - let file_path = PathBuf::from(path); - if !file_path.exists() { - std::fs::write(&path, default)?; - } - load_json_config(path) -} - -fn load_json_config(path: &str) -> Result { - let data = std::fs::read(path)?; - let value = serde_json::from_slice(&data)?; - Ok(value) -} - -fn save_json_config(path: &str, val: &T) -> Result<()> { - let serialized = serde_json::to_vec_pretty(val)?; - Ok(std::fs::write(path, &serialized)?) +use std::{collections::HashMap, path::PathBuf}; + +use anyhow::Result; +use serde::{Serialize, Deserialize, de::DeserializeOwned}; + +use crate::image_processing::Region; + +#[derive(Default, Serialize, Deserialize, Clone)] +pub struct Config { + pub ocr_regions: Vec, + pub track_region: Option, + pub ocr_server_endpoint: String, + pub filter_threshold: Option, + pub use_ocr_cache: Option, + pub ocr_interval_ms: Option, +} + +impl Config { + pub fn load() -> Result { + load_or_make_default("config.json", include_str!("configs/config.default.json")) + } +} + +#[derive(Default, Serialize, Deserialize, Clone)] +pub struct LearnedConfig { + pub learned_images: HashMap, + pub learned_tracks: HashMap, +} + +impl LearnedConfig { + pub fn load() -> Result { + load_or_make_default("learned.json", include_str!("configs/learned.default.json")) + } + pub fn save(&self) -> Result<()> { + save_json_config("learned.json", self) + } +} + +fn load_or_make_default(path: &str, default: &str) -> Result { + let file_path = PathBuf::from(path); + if !file_path.exists() { + std::fs::write(&path, default)?; + } + load_json_config(path) +} + +fn load_json_config(path: &str) -> Result { + let data = std::fs::read(path)?; + let value = serde_json::from_slice(&data)?; + Ok(value) +} + +fn save_json_config(path: &str, val: &T) -> Result<()> { + let serialized = serde_json::to_vec_pretty(val)?; + Ok(std::fs::write(path, &serialized)?) } \ No newline at end of file diff --git a/src/configs/config.default.json b/src/configs/config.default.json index 8a63bb6..9012d8e 100644 --- a/src/configs/config.default.json +++ b/src/configs/config.default.json @@ -1,55 +1,56 @@ -{ - "ocr_regions": [ - { - "name": "lap", - "x": 2300, - "y": 46, - "width": 145, - "height": 90 - }, - { - "name": "health", - "x": 90, - "y": 1364, - "width": 52, - "height": 24 - }, - { - "name": "gas", - "x": 208, - "y": 1364, - "width": 52, - "height": 24 - }, - { - "name": "tyres", - "x": 325, - "y": 1364, - "width": 52, - "height": 24 - }, - { - "name": "best", - "x": 2325, - "y": 169, - "width": 183, - "height": 43 - }, - { - "name": "lap_time", - "x": 2325, - "y": 222, - "width": 183, - "height": 43 - } - ], - "track_region": { - "name": "track", - "x": 2020, - "y": 1030, - "width": 540, - "height": 410, - "threshold": 0.85 - }, - "ocr_server_endpoint": "https://tesserver.spruett.dev/" +{ + "ocr_regions": [ + { + "name": "lap", + "x": 2300, + "y": 46, + "width": 142, + "height": 90 + }, + { + "name": "health", + "x": 90, + "y": 1364, + "width": 52, + "height": 24 + }, + { + "name": "gas", + "x": 208, + "y": 1364, + "width": 52, + "height": 24 + }, + { + "name": "tyres", + "x": 325, + "y": 1364, + "width": 52, + "height": 24 + }, + { + "name": "best", + "x": 2325, + "y": 169, + "width": 183, + "height": 43 + }, + { + "name": "lap_time", + "x": 2325, + "y": 222, + "width": 183, + "height": 43, + "use_ocr_cache": false + } + ], + "track_region": { + "name": "track", + "x": 2020, + "y": 1030, + "width": 540, + "height": 410, + "threshold": 0.85 + }, + "ocr_server_endpoint": "https://tesserver.spruett.dev/" } \ No newline at end of file diff --git a/src/configs/learned.default.json b/src/configs/learned.default.json index 9190260..3563fb1 100644 --- a/src/configs/learned.default.json +++ b/src/configs/learned.default.json @@ -1,4 +1,4 @@ -{ - "learned_images": {}, - "learned_tracks": {} +{ + "learned_images": {}, + "learned_tracks": {} } \ No newline at end of file diff --git a/src/control_loop.rs b/src/control_loop.rs index 2271587..1a592d0 100644 --- a/src/control_loop.rs +++ b/src/control_loop.rs @@ -57,6 +57,7 @@ fn merge_frames(prev: &ParsedFrame, next: &ParsedFrame) -> ParsedFrame { tyres: merge_with_max(&prev.tyres, &next.tyres), lap_time: merge_with_max(&prev.lap_time, &next.lap_time), best_time: merge_with_max(&prev.best_time, &next.best_time), + striked: false, } } @@ -158,7 +159,7 @@ fn run_loop_once(capturer: &mut Capturer, state: &SharedAppState) -> Result<()> } { let mut state = state.lock().unwrap(); - let parsed = ParsedFrame::parse(&ocr_results); + let mut parsed = ParsedFrame::parse(&ocr_results); handle_new_frame(&mut state, parsed, &frame); state.raw_data = ocr_results; state.saved_frames = saved_frames; @@ -172,6 +173,7 @@ pub fn run_control_loop(state: SharedAppState) { if let Err(e) = run_loop_once(&mut capturer, &state) { eprintln!("Error in control loop: {:?}", e) } - thread::sleep(Duration::from_millis(state.lock().unwrap().config.ocr_interval_ms.unwrap_or(500))); + let interval = state.lock().unwrap().config.ocr_interval_ms.unwrap_or(500); + thread::sleep(Duration::from_millis(interval)); } } diff --git a/src/image_processing.rs b/src/image_processing.rs index 9867da6..9e515cf 100644 --- a/src/image_processing.rs +++ b/src/image_processing.rs @@ -11,6 +11,7 @@ pub struct Region { width: usize, height: usize, pub threshold: Option, + pub use_ocr_cache: Option, } pub fn extract_and_filter(image: &RgbImage, region: &Region) -> RgbImage { diff --git a/src/main.rs b/src/main.rs index 4adc7e9..642f9de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,26 @@ // #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release mod capture; +mod config; mod control_loop; mod image_processing; mod ocr; mod state; -mod config; mod stats_writer; use std::{ sync::{Arc, Mutex}, - time::Duration, thread, + thread, + time::{Duration, Instant}, }; use config::{Config, LearnedConfig}; use eframe::{ - egui::{self, Ui}, - epaint::Color32, emath::Vec2, + egui::{self, Ui, Visuals}, + emath::Vec2, + epaint::Color32, }; -use state::{AppState, RaceState, SharedAppState}; +use state::{AppState, RaceState, SharedAppState, ParsedFrame}; use stats_writer::export_race_stats; fn main() -> anyhow::Result<()> { @@ -92,18 +94,23 @@ struct MyApp { state: SharedAppState, config_load_err: Option, - + hash_to_learn: String, value_to_learn: String, } impl MyApp { pub fn new(state: SharedAppState) -> Self { - Self { state, config_load_err: None, hash_to_learn: "".to_owned(), value_to_learn: "".to_owned() } + Self { + state, + config_load_err: None, + hash_to_learn: "".to_owned(), + value_to_learn: "".to_owned(), + } } } -fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) { +fn show_race_state(ui: &mut Ui, race_name: &str, race: &mut RaceState) { egui::Grid::new(format!("race:{}", race_name)).show(ui, |ui| { ui.label("Lap"); ui.label("Time"); @@ -113,9 +120,9 @@ fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) { ui.label("Gas"); ui.label("Tyres"); ui.end_row(); - for (i, lap) in race.laps.iter().enumerate() { + let mut prev_lap: Option<&ParsedFrame> = None; + for (i, lap) in race.laps.iter_mut().enumerate() { if let Some(lap_time) = lap.lap_time { - let prev_lap = race.laps.get(i - 1); ui.label(format!("#{}", lap.lap.unwrap_or(i + 1))); ui.label(format_time(lap_time)); @@ -141,14 +148,24 @@ fn show_race_state(ui: &mut Ui, race_name: &str, race: &RaceState) { &lap.tyres, ); + if lap.striked { + ui.colored_label(Color32::RED, "Striked from the record"); + } else { + if ui.button("Strike").clicked() { + lap.striked = true; + } + } + ui.end_row(); } + prev_lap = Some(lap); } }); } impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + ctx.set_visuals(Visuals::dark()); let mut state = self.state.lock().unwrap(); egui::SidePanel::left("frame").show(ctx, |ui| { if let Some(frame) = &state.last_frame { @@ -175,95 +192,107 @@ impl eframe::App for MyApp { )); } - ui.separator(); - ui.heading("Raw OCR results"); - let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect(); - raw_data_sorted.sort(); - for (key, val) in raw_data_sorted { - ui.label(format!("{}: {:?}", key, val)); + if state.debug_frames { + ui.separator(); + ui.heading("Raw OCR results"); + let mut raw_data_sorted: Vec<_> = state.raw_data.iter().collect(); + raw_data_sorted.sort(); + for (key, val) in raw_data_sorted { + ui.label(format!("{}: {:?}", key, val)); + } } ui.separator(); ui.checkbox(&mut state.debug_frames, "Debug OCR regions"); }); egui::CentralPanel::default().show(ctx, |ui| { - if let Some(race) = &state.current_race { - ui.heading("Current Race"); - show_race_state(ui, "current", race); - } - let len = state.past_races.len(); - for (i, race) in state.past_races.iter_mut().enumerate() { - ui.separator(); - ui.heading(format!("Race #{}", len - i)); - show_race_state(ui, &format!("{}", i), race); - if let Some(img) = &race.screencap { - img.show_max_size(ui, Vec2::new(600.0, 500.0)); + egui::ScrollArea::vertical().show(ui, |ui| { + if let Some(race) = &mut state.current_race { + ui.heading("Current Race"); + show_race_state(ui, "current", race); } - if !race.exported { - ui.label("Car:"); - ui.text_edit_singleline(&mut race.car); - ui.label("Track:"); - ui.text_edit_singleline(&mut race.track); - if ui.button("Export").clicked() { - match export_race_stats(race) { - Ok(_) => { - race.exported = true; - } - Err(e) => { - race.export_error = Some(format!("failed to export race: {:?}", e)); + let len = state.past_races.len(); + for (i, race) in state.past_races.iter_mut().enumerate() { + ui.separator(); + ui.heading(format!("Race #{}", len - i)); + show_race_state(ui, &format!("{}", i), race); + if let Some(img) = &race.screencap { + img.show_max_size(ui, Vec2::new(600.0, 500.0)); + } + if !race.exported { + ui.label("Car:"); + ui.text_edit_singleline(&mut race.car); + ui.label("Track:"); + ui.text_edit_singleline(&mut race.track); + ui.label("Comments:"); + ui.text_edit_singleline(&mut race.comments); + if ui.button("Export").clicked() { + match export_race_stats(race) { + Ok(_) => { + race.exported = true; + } + Err(e) => { + race.export_error = + Some(format!("failed to export race: {:?}", e)); + } } } + if let Some(e) = &race.export_error { + ui.colored_label(Color32::RED, e); + } + } else { + ui.label("Exported ✅"); } - if let Some(e) = &race.export_error { - ui.colored_label(Color32::RED, e); - } - } else { - ui.label("Exported ✅"); } - } + }); }); if state.debug_frames { egui::SidePanel::right("screenshots").show(ctx, |ui| { - let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect(); - screenshots_sorted.sort_by_key(|(name, _)| name.clone()); - for (name, image) in screenshots_sorted { - ui.label(name); - if ui.button(&image.img_hash).on_hover_text("Copy").clicked() { - ui.output().copied_text = image.img_hash.clone(); - } - image.image.show_max_size(ui, ui.available_size()); - } - - if ui.button("Reload config").clicked() { - match Config::load() { - Ok(c) => { - state.config = Arc::new(c); - self.config_load_err = None; + egui::ScrollArea::vertical().show(ui, |ui| { + let mut screenshots_sorted: Vec<_> = state.saved_frames.iter().collect(); + screenshots_sorted.sort_by_key(|(name, _)| name.clone()); + for (name, image) in screenshots_sorted { + ui.label(name); + if ui.button(&image.img_hash).on_hover_text("Copy").clicked() { + ui.output().copied_text = image.img_hash.clone(); } - Err(e) => { - self.config_load_err = Some(format!("failed to load config: {:?}", e)); + image.image.show_max_size(ui, ui.available_size()); + } + + if ui.button("Reload config").clicked() { + match Config::load() { + Ok(c) => { + state.config = Arc::new(c); + self.config_load_err = None; + } + Err(e) => { + self.config_load_err = + Some(format!("failed to load config: {:?}", e)); + } } } - } - if let Some(e) = &self.config_load_err { - ui.colored_label(Color32::RED, e); - } + if let Some(e) = &self.config_load_err { + ui.colored_label(Color32::RED, e); + } - ui.separator(); - ui.label("Hash"); - ui.text_edit_singleline(&mut self.hash_to_learn); - ui.label("Value"); - ui.text_edit_singleline(&mut self.value_to_learn); - if ui.button("Learn").clicked() { - let mut learned_config = (*state.learned).clone(); - learned_config.learned_images.insert(self.hash_to_learn.clone(), self.value_to_learn.clone()); - learned_config.save().unwrap(); - state.learned = Arc::new(learned_config); + ui.separator(); + ui.label("Hash"); + ui.text_edit_singleline(&mut self.hash_to_learn); + ui.label("Value"); + ui.text_edit_singleline(&mut self.value_to_learn); + if ui.button("Learn").clicked() { + let mut learned_config = (*state.learned).clone(); + learned_config + .learned_images + .insert(self.hash_to_learn.clone(), self.value_to_learn.clone()); + learned_config.save().unwrap(); + state.learned = Arc::new(learned_config); - self.hash_to_learn = "".to_owned(); - self.value_to_learn = "".to_owned(); - } + self.hash_to_learn = "".to_owned(); + self.value_to_learn = "".to_owned(); + } + }); }); } diff --git a/src/ocr.rs b/src/ocr.rs index 092c164..bf5b372 100644 --- a/src/ocr.rs +++ b/src/ocr.rs @@ -76,8 +76,9 @@ pub async fn ocr_all_regions( let locked = ocr_cache.read().unwrap(); locked.get(&hash).cloned() }; - if let Some(cached) = cached { - cached + let use_cache = region.use_ocr_cache.unwrap_or(true) && config.use_ocr_cache.unwrap_or(true); + if cached.is_some() && use_cache { + cached.unwrap() } else { match run_ocr(&filtered_image, &config.ocr_server_endpoint).await { Ok(v) => { diff --git a/src/state.rs b/src/state.rs index 3bd12c5..c12757a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -17,6 +17,8 @@ pub struct ParsedFrame { pub best_time: Option, pub lap_time: Option, + + pub striked: bool, } fn parse_duration(time: &str) -> Option { @@ -54,6 +56,7 @@ impl ParsedFrame { tyres: parse_to_0_100(raw.get("tyres")), best_time: parse_to_duration(raw.get("best")), lap_time: parse_to_duration(raw.get("lap_time")), + striked: false, } } } @@ -71,6 +74,7 @@ pub struct RaceState { pub car: String, pub track: String, + pub comments: String, } impl RaceState { diff --git a/src/stats_writer.rs b/src/stats_writer.rs index b93a3fa..d5db4f6 100644 --- a/src/stats_writer.rs +++ b/src/stats_writer.rs @@ -1,52 +1,57 @@ -use std::{ - io::BufWriter, - time::{Duration}, -}; - -use crate::state::RaceState; - -use anyhow::Result; - - -pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> { - let race_name = race_stats.name(); - - let file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open("race_stats.csv")?; - let writer = BufWriter::new(file); - let mut csv_writer = csv::Writer::from_writer(writer); - - for lap in &race_stats.laps { - csv_writer.write_record(vec![ - race_name.clone(), - race_stats.track.clone(), - race_stats.car.clone(), - lap.lap - .map(|x| x.to_string()) - .unwrap_or_else(|| "?".to_owned()), - format!( - "{:.3}", - lap.lap_time.unwrap_or(Duration::from_secs(0)).as_secs_f64() - ), - format!( - "{:.3}", - lap.best_time - .unwrap_or(Duration::from_secs(0)) - .as_secs_f64() - ), - lap.health - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_owned()), - lap.gas - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_owned()), - lap.tyres - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_owned()), - ])?; - } - - Ok(()) -} +use std::{ + io::BufWriter, + time::{Duration}, +}; + +use crate::state::RaceState; + +use anyhow::Result; + + +pub fn export_race_stats(race_stats: &mut RaceState) -> Result<()> { + let race_name = race_stats.name(); + + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("race_stats.csv")?; + let writer = BufWriter::new(file); + let mut csv_writer = csv::Writer::from_writer(writer); + + for lap in &race_stats.laps { + if lap.striked { + continue; + } + + csv_writer.write_record(vec![ + race_name.clone(), + race_stats.track.clone(), + race_stats.car.clone(), + lap.lap + .map(|x| x.to_string()) + .unwrap_or_else(|| "?".to_owned()), + format!( + "{:.3}", + lap.lap_time.unwrap_or(Duration::from_secs(0)).as_secs_f64() + ), + format!( + "{:.3}", + lap.best_time + .unwrap_or(Duration::from_secs(0)) + .as_secs_f64() + ), + lap.health + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_owned()), + lap.gas + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_owned()), + lap.tyres + .map(|x| x.to_string()) + .unwrap_or_else(|| "".to_owned()), + race_stats.comments.clone(), + ])?; + } + + Ok(()) +} diff --git a/suggestions.md b/suggestions.md new file mode 100644 index 0000000..ddef102 --- /dev/null +++ b/suggestions.md @@ -0,0 +1,5 @@ + - Recognize + - Penalties + - Pit stops + - ComboBox for car/track + - GLobal best time not current best \ No newline at end of file diff --git a/tracklist.txt b/tracklist.txt new file mode 100644 index 0000000..0a8a100 --- /dev/null +++ b/tracklist.txt @@ -0,0 +1,19 @@ +Whistle Valley +Sugar Hill +Maple Ridge +Rennvoort +Magdalena GP +Magdalena Club +Copperwood GP +Copperwood Club +Interstate +Buffalo Hill +Lost Lagoons +Bullseye Speedway +Speedopolis +Faenza +Siena +Thunder Point +Tilksport GP +Tilksport Club +Tilksport Rallycross \ No newline at end of file