rFactor2_vk_hud/src/overlay/elements/leaderboard/mod.rs

453 lines
15 KiB
Rust

mod bg_generator;
mod leaderboard_entry;
use leaderboard_entry::*;
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 vulkan_rs::prelude::*;
use crate::overlay::{
rfactor_data::{DataReceiver, GamePhase},
UiOverlay,
};
use crate::write_log;
use self::bg_generator::BackgroundGenerator;
pub struct LeaderBoard {
gui_handler: Arc<GuiHandler>,
leaderboard: Arc<GuiBuilder>,
deltaboard: Arc<GuiBuilder>,
leaderboard_grid: Arc<Grid>,
deltaboard_grid: Arc<Grid>,
leaderboard_entries: Vec<LeaderBoardEntry>,
deltaboard_entries: [LeaderBoardEntry; 5],
leaderboard_redraw: bool,
last_player_id: i32,
entry_backgrounds: [(Arc<Image>, Arc<Image>, Arc<Image>); 2],
player_background: (Arc<Image>, Arc<Image>, Arc<Image>),
}
impl LeaderBoard {
const LEADERBOARD: &str = include_str!("leaderboard_grid.xml");
const DELTABOARD: &str = include_str!("deltaboard_grid.xml");
pub fn new(gui_handler: &Arc<GuiHandler>) -> Result<Self> {
let leaderboard = GuiBuilder::from_str(gui_handler, Self::LEADERBOARD)?;
let deltaboard = GuiBuilder::from_str(gui_handler, Self::DELTABOARD)?;
let leaderboard_grid: Arc<Grid> = 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<Icon> = dummy_snippet.element("place")?;
let name: Arc<Icon> = dummy_snippet.element("name")?;
let time: Arc<Icon> = dummy_snippet.element("time")?;
leaderboard_grid.attach(dummy_snippet, 0, 0, 1, 1)?;
let colors = [
{
let a: [f32; 3] = Color::try_from("#545454")?.into();
[a[0], a[1], a[2], 1.0]
},
{
let a: [f32; 3] = Color::try_from("#838383")?.into();
[a[0], a[1], a[2], 1.0]
},
{
let a: [f32; 3] = Color::try_from("#b4bf26")?.into();
[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::<Result<Vec<(Arc<Image>, Arc<Image>, Arc<Image>)>>>()?;
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<F, D>(
&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 {
let laps_behind = scoring.mLapsBehindLeader;
if laps_behind != 0 {
BehindLeader::Laps(laps_behind)
} else {
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<i32>,
_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(())
}
}