diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9e98ff5..ea777d8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,11 +4,14 @@ { "type": "cargo", "command": "build", + "args": [ + "--release" + ], "problemMatcher": [ "$rustc" ], "group": "build", - "label": "rust: cargo build --release" + "label": "rust: cargo build" }, { "type": "shell", diff --git a/Cargo.toml b/Cargo.toml index 9e877bf..0f90ea2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,10 +10,14 @@ crate-type = ["cdylib"] [dependencies] vulkan-rs = { git = "https://gavania.de/hodasemi/vulkan_lib.git" } +assetpath = { git = "https://gavania.de/hodasemi/vulkan_lib.git" } +utilities = { git = "https://gavania.de/hodasemi/utilities.git" } rfactor_sm_reader = { git = "https://gavania.de/hodasemi/rfactor_sm_reader.git" } +ui = { git = "https://gavania.de/hodasemi/ui.git" } anyhow = { version = "1.0.68", features = ["backtrace"] } cgmath = { version = "0.18.0", features = ["swizzle", "serde"] } paste = "1.0.11" serde = "1.0.152" serde_json = "1.0.91" +ringbuf = "0.3.2" diff --git a/README.md b/README.md index 7dced95..8d702f0 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,31 @@ This project is an attempt to render custom HUD elements for rFactor2 by being a Vulkan layer. # Current state -I just started doing it. That means it isn't very usable right now. I'm working on a radar right now as the first element. +I would consider the following elements as working: + +* Leader Board (enabled on in race) +* Radar +* Pedals # How to enable -* Build this repository `cargo build --release` -* Change the path where the `libvk_layer_rs.so` is located (`library_path` parameter in the rFactorOverlay.json file) -* Put the rFactorOverlay.json into a layer directory ([layer directories](https://vulkan.lunarg.com/doc/view/1.3.216.0/mac/loader_and_layer_interface.html#user-content-linux-layer-discovery)) + +### Archlinux based +Simply use the PKGBUILD from the pkgbuild directory ([How to use it](https://wiki.archlinux.org/title/Makepkg)) + +### Manual installation +1) Build this repository `cargo build --release` +2) Change the path where the `libvk_layer_rs.so` is located (`library_path` parameter in the rFactorOverlay.json file) +3) Put the rFactorOverlay.json into a layer directory ([layer directories](https://vulkan.lunarg.com/doc/view/1.3.216.0/mac/loader_and_layer_interface.html#user-content-linux-layer-discovery)) + +--- + * Add `RFACTOR_HUD=1` to steam launch command (example: `RFACTOR_HUD=1 %command%`) A config file is generated the first time you start it at: `$HOME/.config/rFactorHUD/config.json` +### Requirement +You need to have rFactor2 memory plugin shared file enabled ([rF2SharedMemoryMapPlugin_Wine](https://github.com/schlegp/rF2SharedMemoryMapPlugin_Wine)) + # Resources ## Vulkan Layer diff --git a/build.rs b/build.rs index d0fb369..5586f94 100644 --- a/build.rs +++ b/build.rs @@ -10,8 +10,10 @@ const VK_HEADER: &[&str] = &[ const FN_PREFIX: &str = "PFN_"; const SHADER: &[&str] = &[ - "src/overlay/shader/single_color.vert", - "src/overlay/shader/single_color.frag", + "src/overlay/elements/radar/single_color.vert", + "src/overlay/elements/radar/single_color.frag", + "src/overlay/elements/pedals/history.vert", + "src/overlay/elements/pedals/history.frag", ]; fn query_vulkan_function_typedefs() { diff --git a/font.png b/font.png new file mode 100644 index 0000000..d10f072 Binary files /dev/null and b/font.png differ diff --git a/src/overlay/elements/gui.xsd b/src/overlay/elements/gui.xsd new file mode 100644 index 0000000..28c6e1c --- /dev/null +++ b/src/overlay/elements/gui.xsd @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/overlay/elements/leaderboard/leaderboard_entry.xml b/src/overlay/elements/leaderboard/leaderboard_entry.xml new file mode 100644 index 0000000..a61afb4 --- /dev/null +++ b/src/overlay/elements/leaderboard/leaderboard_entry.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/overlay/elements/leaderboard/leaderboard_grid.xml b/src/overlay/elements/leaderboard/leaderboard_grid.xml new file mode 100644 index 0000000..0ad24dc --- /dev/null +++ b/src/overlay/elements/leaderboard/leaderboard_grid.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/src/overlay/elements/leaderboard/mod.rs b/src/overlay/elements/leaderboard/mod.rs new file mode 100644 index 0000000..f189029 --- /dev/null +++ b/src/overlay/elements/leaderboard/mod.rs @@ -0,0 +1,291 @@ +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