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, leaderboard: Arc, main_grid: Arc, entries: Vec, 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) -> Result { let gui = GuiBuilder::from_str(gui_handler, Self::GRID)?; let main_grid = gui.element("main_grid")?; Ok(Self { gui_handler: gui_handler.clone(), leaderboard: 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( &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, { let laps_behind = vehicle_scoring.mLapsBehindLeader; if laps_behind != 0 { BehindLeader::Laps(laps_behind) } else { BehindLeader::Time(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| { let laps_behind = scoring.mLapsBehindLeader; if laps_behind != 0 { entry.update_time_behind_leader(BehindLeader::Laps(laps_behind)) } else { entry.update_time_behind_leader(BehindLeader::Time(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 => { for entry in self.entries.iter_mut() { entry.update_best_lap(-1.0)?; } self.leaderboard.enable()? } GamePhase::Race => { for entry in self.entries.iter_mut() { entry.update_time_behind_leader(BehindLeader::Time(0.0))?; } self.leaderboard.enable()?; } _ => self.leaderboard.disable()?, } Ok(()) } 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, _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(()) } } #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] enum BehindLeader { Time(f64), Laps(i32), } struct LeaderBoardEntry { id: i32, name: String, place: u8, behind: BehindLeader, time_behind_next: f64, best_lap: f64, snippet: Arc, name_label: Arc