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, gui: 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(), 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 race_leaderboard(&mut self, vehicle_scorings: &[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)?; entry.update_time_behind_leader(vehicle_scoring.mTimeBehindLeader)?; entry.update_time_behind_next(vehicle_scoring.mTimeBehindNext)?; } None => { let entry = LeaderBoardEntry::new( &self.gui_handler, vehicle_scoring.mID, driver_name, vehicle_scoring.mPlace, vehicle_scoring.mTimeBehindLeader, vehicle_scoring.mTimeBehindNext, )?; 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)?; } } if self.entries.is_empty() { self.gui.disable()?; } else { self.gui.enable()?; } Ok(()) } } impl UiOverlay for LeaderBoard {} impl DataReceiver for LeaderBoard { fn scoring_update( &mut self, phase: GamePhase, vehicle_scorings: &[VehicleScoringInfoV01], ) -> Result<()> { write_log!("=================== leader board: scoring update ==================="); match phase { GamePhase::TestDay => self.gui.disable()?, GamePhase::Practice => self.gui.disable()?, GamePhase::Qualifying => self.gui.disable()?, GamePhase::Warmup => self.gui.disable()?, 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(()) } } struct LeaderBoardEntry { id: i32, name: String, place: u8, time_behind_leader: f64, time_behind_next: f64, snippet: Arc, grid: Arc, name_label: Arc