use std::{ ffi::{c_char, CStr}, sync::Arc, }; use anyhow::Result; use rfactor_sm_reader::{rF2VehicleTelemetry, VehicleScoringInfoV01}; use ui::prelude::*; use utilities::prelude::Color; use crate::overlay::{ rfactor_data::{DataReceiver, GamePhase}, UiOverlay, }; use crate::write_log; pub struct LeaderBoard { gui_handler: Arc<GuiHandler>, gui: Arc<GuiBuilder>, main_grid: Arc<Grid>, entries: Vec<LeaderBoardEntry>, entry_backgrounds: [Color; 2], player_background: Color, } impl LeaderBoard { const GRID: &str = include_str!("leaderboard_grid.xml"); const ENTRY: &str = include_str!("leaderboard_entry.xml"); pub fn new(gui_handler: &Arc<GuiHandler>) -> Result<Self> { let gui = GuiBuilder::from_str(gui_handler, Self::GRID)?; let main_grid = gui.element("main_grid")?; Ok(Self { gui_handler: gui_handler.clone(), gui, main_grid, entries: Vec::new(), entry_backgrounds: [Color::try_from("#838383")?, Color::try_from("#545454")?], player_background: Color::try_from("#b4bf26")?, }) } fn c_char_to_string(c: [c_char; 32usize]) -> String { unsafe { CStr::from_ptr(&c as *const c_char) } .to_str() .unwrap() .to_string() } fn update_leaderboard<F>( &mut self, vehicle_scorings: &[VehicleScoringInfoV01], f: F, ) -> Result<()> where F: Fn(&mut LeaderBoardEntry, &VehicleScoringInfoV01) -> Result<()>, { for vehicle_scoring in vehicle_scorings { let driver_name = Self::c_char_to_string(vehicle_scoring.mDriverName); // check driver list match self .entries .iter_mut() .find(|entry| vehicle_scoring.mID == entry.id()) { Some(entry) => { if entry.name() != driver_name { entry.change_name(driver_name)?; } entry.update_place(vehicle_scoring.mPlace)?; f(entry, vehicle_scoring)?; } None => { let entry = LeaderBoardEntry::new( &self.gui_handler, vehicle_scoring.mID, driver_name, vehicle_scoring.mPlace, vehicle_scoring.mTimeBehindLeader, vehicle_scoring.mTimeBehindNext, vehicle_scoring.mBestLapTime, )?; self.entries.push(entry); } } } write_log!("create entries"); // check if entry count in grid is the same as the gathered entries let force_update = if !self.entries.is_empty() && self .main_grid .child_at(0, self.entries.len() - 1)? .is_none() { for i in 0..self.entries.len() { self.main_grid.detach(0, i)?; } true } else { false }; // check if any entry needs resorting if force_update || self.entries.iter().any(|entry| entry.needs_resorting()) { write_log!("leader board update required"); self.entries .sort_by(|lhs, rhs| lhs.place().cmp(&rhs.place())); for (i, entry) in self.entries.iter_mut().enumerate() { entry.resorting_finished(); entry.change_background_color(self.entry_backgrounds[i % 2])?; self.main_grid.attach(entry.snippet(), 0, i, 1, 1)?; } } Ok(()) } fn race_leaderboard(&mut self, vehicle_scorings: &[VehicleScoringInfoV01]) -> Result<()> { self.update_leaderboard(vehicle_scorings, |entry, scoring| { entry.update_time_behind_leader(scoring.mTimeBehindLeader) }) } fn quali_leaderboard(&mut self, vehicle_scorings: &[VehicleScoringInfoV01]) -> Result<()> { self.update_leaderboard(vehicle_scorings, |entry, scoring| { entry.update_best_lap(scoring.mBestLapTime) }) } } impl UiOverlay for LeaderBoard {} impl DataReceiver for LeaderBoard { fn game_phase_change(&mut self, phase: GamePhase) -> Result<()> { match phase { GamePhase::Practice | GamePhase::Qualifying | GamePhase::Race => self.gui.enable(), _ => self.gui.disable(), } } fn update_for_phase(&self, phase: GamePhase) -> bool { match phase { GamePhase::Practice | GamePhase::Qualifying | GamePhase::Race => true, _ => false, } } fn scoring_update( &mut self, phase: GamePhase, vehicle_scorings: &[VehicleScoringInfoV01], ) -> Result<()> { write_log!("=================== leader board: scoring update ==================="); match phase { GamePhase::Practice | GamePhase::Qualifying => { self.quali_leaderboard(vehicle_scorings)? } GamePhase::Race => self.race_leaderboard(vehicle_scorings)?, _ => (), } write_log!("leader board update finished"); Ok(()) } fn telemetry_update( &mut self, player_id: Option<i32>, _telemetries: &[rF2VehicleTelemetry], ) -> Result<()> { if let Some(player_id) = player_id { if let Some(entry) = self.entries.iter().find(|entry| entry.id() == player_id) { entry.change_background_color(self.player_background)?; } } Ok(()) } } struct LeaderBoardEntry { id: i32, name: String, place: u8, time_behind_leader: f64, time_behind_next: f64, best_lap: f64, snippet: Arc<GuiSnippet>, grid: Arc<Grid>, name_label: Arc<Label>, place_label: Arc<Label>, time_label: Arc<Label>, place_updated: bool, } impl LeaderBoardEntry { pub fn new( gui_handler: &Arc<GuiHandler>, id: i32, name: String, place: u8, time_behind_leader: f64, time_behind_next: f64, best_lap: f64, ) -> Result<Self> { let snippet = GuiSnippet::from_str(gui_handler, LeaderBoard::ENTRY)?; let background = snippet.element("grid")?; let name_label: Arc<Label> = snippet.element("name")?; let place_label: Arc<Label> = snippet.element("place")?; let time_label: Arc<Label> = snippet.element("time")?; name_label.set_text(&name)?; place_label.set_text(place)?; time_label.set_text("---")?; Ok(Self { id, name, place, time_behind_leader, time_behind_next, best_lap, snippet, grid: background, name_label, place_label, time_label, place_updated: true, }) } pub fn id(&self) -> i32 { self.id } pub fn place(&self) -> u8 { self.place } pub fn name(&self) -> &str { &self.name } pub fn change_name(&mut self, name: String) -> Result<()> { self.name = name; self.name_label.set_text(&self.name) } pub fn snippet(&self) -> Arc<GuiSnippet> { self.snippet.clone() } pub fn change_background_color(&self, color: Color) -> Result<()> { self.grid.set_background(color) } pub fn update_place(&mut self, place: u8) -> Result<()> { if self.place != place { self.place_updated = true; } self.place = place; self.place_label.set_text(self.place) } pub fn update_time_behind_leader(&mut self, time: f64) -> Result<()> { if self.time_behind_leader != time { self.time_behind_leader = time; // check if we are leader if self.time_behind_leader == 0.0 { self.time_label.set_text("---")?; } else { self.time_label .set_text(format!("+{:.3}", self.time_behind_leader))?; } } Ok(()) } pub fn update_best_lap(&mut self, time: f64) -> Result<()> { if self.best_lap != time { self.best_lap = time; if self.best_lap < 0.0 { self.time_label.set_text("---")?; } else { let text = if self.best_lap > 60.0 { let full_minutes = (self.best_lap / 60.0).floor(); let remainder = self.best_lap - (full_minutes * 60.0); format!("{:.0}:{:.3}", full_minutes, remainder) } else { format!("{:.3}", self.best_lap) }; self.time_label.set_text(text)?; } } Ok(()) } pub fn update_time_behind_next(&mut self, time: f64) -> Result<()> { self.time_behind_next = time; self.time_label .set_text(format!("+{:.3}", self.time_behind_next)) } pub fn needs_resorting(&self) -> bool { self.place_updated } pub fn resorting_finished(&mut self) { self.place_updated = false; } }