mod bg_generator; mod leaderboard_entry; use leaderboard_entry::*; use serde::{Deserialize, Serialize}; use std::{ ffi::{c_char, CStr}, sync::Arc, }; use anyhow::Result; use rfactor_sm_reader::{rF2VehicleTelemetry, VehicleScoringInfoV01}; use ui::prelude::*; use vulkan_rs::prelude::*; use crate::overlay::{ rfactor_data::{DataReceiver, GamePhase}, UiOverlay, }; use crate::write_log; use bg_generator::BackgroundGenerator; #[derive(Default, Deserialize, Serialize, Clone, Copy)] pub struct LeaderBoardConfig { first_board_color: [f32; 3], second_board_color: [f32; 3], player_board_color: [f32; 3], } impl LeaderBoardConfig { pub const fn new() -> Self { Self { first_board_color: [0.33; 3], second_board_color: [0.51; 3], player_board_color: [0.7, 0.75, 0.15], } } } pub struct LeaderBoard { gui_handler: Arc, leaderboard: Arc, deltaboard: Arc, leaderboard_grid: Arc, deltaboard_grid: Arc, leaderboard_entries: Vec, deltaboard_entries: [LeaderBoardEntry; 5], leaderboard_redraw: bool, last_player_id: i32, entry_backgrounds: [(Arc, Arc, Arc); 2], player_background: (Arc, Arc, Arc), } impl LeaderBoard { const LEADERBOARD: &str = include_str!("leaderboard_grid.xml"); const DELTABOARD: &str = include_str!("deltaboard_grid.xml"); pub fn new( gui_handler: &Arc, leader_board_config: LeaderBoardConfig, ) -> Result { let leaderboard = GuiBuilder::from_str(gui_handler, Self::LEADERBOARD)?; let deltaboard = GuiBuilder::from_str(gui_handler, Self::DELTABOARD)?; let leaderboard_grid: Arc = leaderboard.element("main_grid")?; let deltaboard_grid = deltaboard.element("main_grid")?; let images = { // attach snippet to leader board to let it resize its child element correctly // then use these sizes to create actual UI element background let dummy_snippet = LeaderBoardEntry::create_snippet(gui_handler)?; let place: Arc = dummy_snippet.element("place")?; let name: Arc = dummy_snippet.element("name")?; let time: Arc = dummy_snippet.element("time")?; leaderboard_grid.attach(dummy_snippet, 0, 0, 1, 1)?; let colors = [ { let a = leader_board_config.first_board_color; [a[0], a[1], a[2], 1.0] }, { let a = leader_board_config.second_board_color; [a[0], a[1], a[2], 1.0] }, { let a = leader_board_config.player_board_color; [a[0], a[1], a[2], 1.0] }, ]; let images = colors .iter() .map(|color| { let images = BackgroundGenerator::generate( gui_handler.device(), gui_handler.queue(), *color, [ Self::extent_i_to_u(place.extent()), Self::extent_i_to_u(name.extent()), Self::extent_i_to_u(time.extent()), ], )?; Ok((images[0].clone(), images[1].clone(), images[2].clone())) }) .collect::, Arc, Arc)>>>()?; leaderboard_grid.detach(0, 0)?; images }; Ok(Self { gui_handler: gui_handler.clone(), leaderboard, deltaboard, leaderboard_grid, deltaboard_grid, leaderboard_entries: Vec::new(), deltaboard_entries: [ LeaderBoardEntry::empty(gui_handler)?, LeaderBoardEntry::empty(gui_handler)?, LeaderBoardEntry::empty(gui_handler)?, LeaderBoardEntry::empty(gui_handler)?, LeaderBoardEntry::empty(gui_handler)?, ], leaderboard_redraw: false, last_player_id: -1, entry_backgrounds: [images[0].clone(), images[1].clone()], player_background: images[2].clone(), }) } fn extent_i_to_u((x, y): (i32, i32)) -> (u32, u32) { (x as u32, y as u32) } 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, d: D, ) -> Result<()> where F: Fn(&mut LeaderBoardEntry, &VehicleScoringInfoV01) -> Result<()>, D: Fn(&mut LeaderBoardEntry) -> Result<()>, { for vehicle_scoring in vehicle_scorings { let driver_name = Self::c_char_to_string(vehicle_scoring.mDriverName); // check driver list match self .leaderboard_entries .iter_mut() .find(|entry| vehicle_scoring.mID == entry.id()) { // update existing entry Some(entry) => { if entry.name() != driver_name { entry.change_name(driver_name)?; } entry.update_place(vehicle_scoring.mPlace)?; f(entry, vehicle_scoring)?; } // add new entry if not found None => { let mut entry = LeaderBoardEntry::new( &self.gui_handler, vehicle_scoring.mID, driver_name, vehicle_scoring.mPlace, Self::query_behind_leader(vehicle_scoring), vehicle_scoring.mTimeBehindNext, vehicle_scoring.mBestLapTime, )?; d(&mut entry)?; self.leaderboard_entries.push(entry); } } } write_log!("create entries"); // check if entry count in grid is the same as the gathered entries // that means some joined or left the game let force_update = if !self.leaderboard_entries.is_empty() && self.leaderboard_entries.len() <= self.leaderboard_grid.dimensions().1 && self .leaderboard_grid .child_at(0, self.leaderboard_entries.len() - 1)? .is_none() { for (i, entry) in self.leaderboard_entries.iter().enumerate() { self.leaderboard_grid.detach(0, i)?; entry.snippet().set_visibility(false)?; } true } // there are more entries in leaderboard when someone leaves and ID doesn't get reused else if self.leaderboard_entries.len() > vehicle_scorings.len() { self.leaderboard_entries.retain(|entry| { vehicle_scorings .iter() .any(|scoring| scoring.mID == entry.id()) }); true } else { false }; // check if any entry needs resorting if force_update || self .leaderboard_entries .iter() .any(|entry| entry.needs_resorting()) { write_log!("leader board update required"); self.leaderboard_entries .sort_by(|lhs, rhs| lhs.place().cmp(&rhs.place())); for (i, entry) in self.leaderboard_entries.iter_mut().enumerate() { entry.resorting_finished(); // don't break here, just skip adding to grid // because resorting_finished should be called for every entry if i < self.leaderboard_grid.dimensions().1 { entry.change_background_color(&self.entry_backgrounds[i % 2])?; self.leaderboard_grid.attach(entry.snippet(), 0, i, 1, 1)?; } } self.leaderboard_redraw = true; } // update delta board if self.last_player_id != -1 { write_log!("update delta board"); if let Some((index, _player_entry)) = self .leaderboard_entries .iter() .enumerate() .find(|(_index, entry)| entry.id() == self.last_player_id) { let mut start_index = if index >= 2 { if index == self.leaderboard_entries.len() - 2 { if index >= 3 { index - 3 } else { index - 2 } } else if index == self.leaderboard_entries.len() - 1 { if index >= 4 { index - 4 } else if index >= 3 { index - 3 } else { index - 2 } } else { index - 2 } } else { 0 }; let max = self.leaderboard_entries.len().min(5); write_log!(format!( "Delta Board: start {} - count {}", start_index, max )); // clear old entries for i in 0..5 { if let Some(child) = self.deltaboard_grid.detach(0, i)? { if let Some(visiblity) = child.visibility() { visiblity.set_visibility(false)?; } } } // add new entries for i in 0..max { if let Some(leaderboard_entry) = self.leaderboard_entries.get(start_index) { if let Some(entry) = self.deltaboard_entries.get_mut(i) { entry.change_id(leaderboard_entry.id()); entry.update_place(leaderboard_entry.place())?; if entry.id() == self.last_player_id { entry.change_background_color(&self.player_background)?; } else { entry.change_background_color(&self.entry_backgrounds[i % 2])?; } if entry.name() != leaderboard_entry.name() { entry.change_name(leaderboard_entry.name().to_string())?; } if let Some(vehicle_scoring) = vehicle_scorings .iter() .find(|scoring| scoring.mID == entry.id()) { entry.update_time_behind_next(vehicle_scoring.mTimeBehindNext)?; } self.deltaboard_grid.attach(entry.snippet(), 0, i, 1, 1)?; } start_index += 1; } } } } Ok(()) } fn query_behind_leader(scoring: &VehicleScoringInfoV01) -> BehindLeader { match scoring.mFinishStatus { 0 | 1 => { if scoring.mInPits != 0 { BehindLeader::PITS } else { let laps_behind = scoring.mLapsBehindLeader; if laps_behind != 0 { BehindLeader::Laps(laps_behind) } else { BehindLeader::Time(scoring.mTimeBehindLeader) } } } 2 => BehindLeader::DNF, 3 => BehindLeader::DSQ, _ => { write_log!(format!( "not allowed finish state: {}", scoring.mFinishStatus )); BehindLeader::Time(scoring.mTimeBehindLeader) } } } fn race_leaderboard(&mut self, vehicle_scorings: &[VehicleScoringInfoV01]) -> Result<()> { self.update_leaderboard( vehicle_scorings, |entry, scoring| entry.update_time_behind_leader(Self::query_behind_leader(scoring)), |entry| entry.force_display_behind_leader(), ) } fn quali_leaderboard(&mut self, vehicle_scorings: &[VehicleScoringInfoV01]) -> Result<()> { self.update_leaderboard( vehicle_scorings, |entry, scoring| entry.update_best_lap(scoring.mBestLapTime), |entry| entry.force_display_best_lap(), ) } } impl UiOverlay for LeaderBoard {} impl DataReceiver for LeaderBoard { fn game_phase_change(&mut self, phase: GamePhase) -> Result<()> { match phase { GamePhase::Practice | GamePhase::Qualifying | GamePhase::TestDay => { for entry in self.leaderboard_entries.iter_mut() { entry.reset_time()?; } self.leaderboard.enable()?; } GamePhase::Race => { for entry in self.leaderboard_entries.iter_mut() { entry.reset_time()?; } for entry in self.deltaboard_entries.iter_mut() { entry.reset_time()?; } self.leaderboard.enable()?; self.deltaboard.enable()?; } _ => { self.last_player_id = -1; self.leaderboard.disable()?; self.deltaboard.disable()?; } } Ok(()) } fn update_for_phase(&self, phase: GamePhase) -> bool { match phase { GamePhase::Practice | GamePhase::Qualifying | GamePhase::Race | GamePhase::TestDay => { 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 | GamePhase::TestDay => { 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 self.leaderboard_redraw { self.leaderboard_redraw = false; if let Some(player_id) = player_id { if let Some(entry) = self .leaderboard_entries .iter() .find(|entry| entry.id() == player_id) { write_log!(format!( "Update player entry background color: {:?}", self.player_background )); entry.change_background_color(&self.player_background)?; } } } if let Some(player_id) = player_id { self.last_player_id = player_id; } Ok(()) } }