dark mode release

This commit is contained in:
Scott Pruett 2022-05-22 22:23:28 -04:00
parent 4a920b4b42
commit 34a2b61757
15 changed files with 427 additions and 342 deletions

14
car-classes.txt Normal file
View File

@ -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

View File

@ -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/"
}

3
race_stats.csv Normal file
View File

@ -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
1 2022-05-22-20:39 whistle valley gp 1 22.349 22.349 100
2 2022-05-22-20:39 whistle valley gp 2 21.888 21.888 99 83 82
3 2022-05-22-20:39 whistle valley gp 3 22.031 21.888 99 75 73

View File

@ -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()

View File

@ -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<Region>,
pub track_region: Option<Region>,
pub ocr_server_endpoint: String,
pub filter_threshold: Option<f64>,
pub use_ocr_cache: Option<bool>,
pub ocr_interval_ms: Option<u64>,
}
impl Config {
pub fn load() -> Result<Self> {
load_or_make_default("config.json", include_str!("configs/config.default.json"))
}
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct LearnedConfig {
pub learned_images: HashMap<String, String>,
pub learned_tracks: HashMap<String, String>,
}
impl LearnedConfig {
pub fn load() -> Result<Self> {
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<T: DeserializeOwned>(path: &str, default: &str) -> Result<T> {
let file_path = PathBuf::from(path);
if !file_path.exists() {
std::fs::write(&path, default)?;
}
load_json_config(path)
}
fn load_json_config<T: DeserializeOwned>(path: &str) -> Result<T> {
let data = std::fs::read(path)?;
let value = serde_json::from_slice(&data)?;
Ok(value)
}
fn save_json_config<T: Serialize>(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<Region>,
pub track_region: Option<Region>,
pub ocr_server_endpoint: String,
pub filter_threshold: Option<f64>,
pub use_ocr_cache: Option<bool>,
pub ocr_interval_ms: Option<u64>,
}
impl Config {
pub fn load() -> Result<Self> {
load_or_make_default("config.json", include_str!("configs/config.default.json"))
}
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct LearnedConfig {
pub learned_images: HashMap<String, String>,
pub learned_tracks: HashMap<String, String>,
}
impl LearnedConfig {
pub fn load() -> Result<Self> {
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<T: DeserializeOwned>(path: &str, default: &str) -> Result<T> {
let file_path = PathBuf::from(path);
if !file_path.exists() {
std::fs::write(&path, default)?;
}
load_json_config(path)
}
fn load_json_config<T: DeserializeOwned>(path: &str) -> Result<T> {
let data = std::fs::read(path)?;
let value = serde_json::from_slice(&data)?;
Ok(value)
}
fn save_json_config<T: Serialize>(path: &str, val: &T) -> Result<()> {
let serialized = serde_json::to_vec_pretty(val)?;
Ok(std::fs::write(path, &serialized)?)
}

View File

@ -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/"
}

View File

@ -1,4 +1,4 @@
{
"learned_images": {},
"learned_tracks": {}
{
"learned_images": {},
"learned_tracks": {}
}

View File

@ -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));
}
}

View File

@ -11,6 +11,7 @@ pub struct Region {
width: usize,
height: usize,
pub threshold: Option<f64>,
pub use_ocr_cache: Option<bool>,
}
pub fn extract_and_filter(image: &RgbImage, region: &Region) -> RgbImage {

View File

@ -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<String>,
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();
}
});
});
}

View File

@ -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) => {

View File

@ -17,6 +17,8 @@ pub struct ParsedFrame {
pub best_time: Option<Duration>,
pub lap_time: Option<Duration>,
pub striked: bool,
}
fn parse_duration(time: &str) -> Option<Duration> {
@ -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 {

View File

@ -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(())
}

5
suggestions.md Normal file
View File

@ -0,0 +1,5 @@
- Recognize
- Penalties
- Pit stops
- ComboBox for car/track
- GLobal best time not current best

19
tracklist.txt Normal file
View File

@ -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