mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-08-03 06:38:52 +00:00
feat: move steam library folders parser to own module; function to find steam openxr json; format
This commit is contained in:
parent
61f13dbd8f
commit
a9fa4f8cf4
15 changed files with 119 additions and 93 deletions
|
@ -1,7 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
paths::{get_backup_dir, SYSTEM_PREFIX},
|
paths::{get_backup_dir, SYSTEM_PREFIX},
|
||||||
profile::Profile,
|
profile::Profile,
|
||||||
util::file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
|
util::{
|
||||||
|
file_utils::{copy_file, deserialize_file, get_writer, set_file_readonly},
|
||||||
|
steam_library_folder::SteamLibraryFolder,
|
||||||
|
},
|
||||||
xdg::XDG,
|
xdg::XDG,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -9,6 +12,7 @@ use std::{
|
||||||
fs::remove_file,
|
fs::remove_file,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct ActiveRuntimeInnerRuntime {
|
pub struct ActiveRuntimeInnerRuntime {
|
||||||
|
@ -38,6 +42,23 @@ pub fn is_steam(active_runtime: &ActiveRuntime) -> bool {
|
||||||
matches!(active_runtime.runtime.valve_runtime_is_steamvr, Some(true))
|
matches!(active_runtime.runtime.valve_runtime_is_steamvr, Some(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const STEAMVR_STEAM_APPID: u32 = 250820;
|
||||||
|
|
||||||
|
pub fn find_steam_openxr_json() -> Option<PathBuf> {
|
||||||
|
match SteamLibraryFolder::get_folders() {
|
||||||
|
Ok(libraryfolders) => libraryfolders
|
||||||
|
.iter()
|
||||||
|
.find(|(_, folder)| folder.apps.contains_key(&STEAMVR_STEAM_APPID))
|
||||||
|
.map(|(_, folder)| {
|
||||||
|
PathBuf::from(&folder.path).join("steamapps/common/SteamVR/steamxr_linux64.json")
|
||||||
|
}),
|
||||||
|
Err(e) => {
|
||||||
|
error!("unable to find steam openxr json: unable to load steam libraryfolders: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_backup_steam_active_runtime_path() -> PathBuf {
|
fn get_backup_steam_active_runtime_path() -> PathBuf {
|
||||||
get_backup_dir().join("active_runtime.json.steam.bak")
|
get_backup_dir().join("active_runtime.json.steam.bak")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ use file_builders::{
|
||||||
openvrpaths_vrpath::{get_current_openvrpaths, set_current_openvrpaths_to_steam},
|
openvrpaths_vrpath::{get_current_openvrpaths, set_current_openvrpaths_to_steam},
|
||||||
};
|
};
|
||||||
use gettextrs::LocaleCategory;
|
use gettextrs::LocaleCategory;
|
||||||
use tracing::warn;
|
|
||||||
use relm4::{
|
use relm4::{
|
||||||
adw,
|
adw,
|
||||||
gtk::{self, gdk, gio, glib, prelude::*},
|
gtk::{self, gdk, gio, glib, prelude::*},
|
||||||
|
@ -13,6 +12,7 @@ use relm4::{
|
||||||
};
|
};
|
||||||
use std::env;
|
use std::env;
|
||||||
use steam_linux_runtime_injector::restore_runtime_entrypoint;
|
use steam_linux_runtime_injector::restore_runtime_entrypoint;
|
||||||
|
use tracing::warn;
|
||||||
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
|
||||||
use ui::{
|
use ui::{
|
||||||
app::{App, AppInit, Msg},
|
app::{App, AppInit, Msg},
|
||||||
|
|
|
@ -1,76 +1,33 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
paths::{get_backup_dir, get_home_dir},
|
paths::get_backup_dir,
|
||||||
profile::Profile,
|
profile::Profile,
|
||||||
util::file_utils::{copy_file, get_writer},
|
util::{
|
||||||
|
file_utils::{copy_file, get_writer},
|
||||||
|
steam_library_folder::SteamLibraryFolder,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use tracing::error;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
fs::read_to_string,
|
fs::read_to_string,
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct LibraryFolder {
|
|
||||||
pub path: String,
|
|
||||||
pub apps: HashMap<u32, usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PRESSURE_VESSEL_STEAM_APPID: u32 = 1628350;
|
pub const PRESSURE_VESSEL_STEAM_APPID: u32 = 1628350;
|
||||||
|
|
||||||
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
|
|
||||||
let steam_root: PathBuf = get_home_dir().join(".steam/root");
|
|
||||||
|
|
||||||
if steam_root.is_symlink() {
|
|
||||||
Ok(steam_root.read_link()?)
|
|
||||||
} else if steam_root.is_dir() {
|
|
||||||
Ok(steam_root)
|
|
||||||
} else {
|
|
||||||
bail!(
|
|
||||||
"Canonical steam root '{}' is not a dir nor a symlink!",
|
|
||||||
steam_root.to_string_lossy()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_steam_libraryfolders_vdf(path: &Path) -> anyhow::Result<HashMap<u32, LibraryFolder>> {
|
|
||||||
Ok(keyvalues_serde::from_str(read_to_string(path)?.as_str())?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_runtime_entrypoint_path() -> Option<PathBuf> {
|
fn get_runtime_entrypoint_path() -> Option<PathBuf> {
|
||||||
match get_steam_main_dir_path() {
|
match SteamLibraryFolder::get_folders() {
|
||||||
Ok(steam_root) => {
|
Ok(libraryfolders) => libraryfolders
|
||||||
let steam_libraryfolders_path = steam_root.join("steamapps/libraryfolders.vdf");
|
.iter()
|
||||||
|
.find(|(_, folder)| folder.apps.contains_key(&PRESSURE_VESSEL_STEAM_APPID))
|
||||||
if !steam_libraryfolders_path.is_file() {
|
.map(|(_, folder)| {
|
||||||
error!(
|
PathBuf::from(&folder.path)
|
||||||
"Steam libraryfolders.vdf does not exist in its canonical location {}",
|
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
||||||
steam_libraryfolders_path.to_string_lossy()
|
}),
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let libraryfolders: HashMap<u32, LibraryFolder> =
|
|
||||||
parse_steam_libraryfolders_vdf(&steam_libraryfolders_path).ok()?;
|
|
||||||
|
|
||||||
libraryfolders
|
|
||||||
.iter()
|
|
||||||
.find(|(_, libraryfolder)| {
|
|
||||||
libraryfolder
|
|
||||||
.apps
|
|
||||||
.contains_key(&PRESSURE_VESSEL_STEAM_APPID)
|
|
||||||
})
|
|
||||||
.map(|(_, libraryfolder)| {
|
|
||||||
PathBuf::from(&libraryfolder.path)
|
|
||||||
.join("steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error getting steam root path: {e}");
|
error!("unable to get runtime entrypoint path: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,22 +83,3 @@ pub fn set_runtime_entrypoint_launch_opts_from_profile(profile: &Profile) -> any
|
||||||
}
|
}
|
||||||
bail!("Could not find valid runtime entrypoint");
|
bail!("Could not find valid runtime entrypoint");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use super::parse_steam_libraryfolders_vdf;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_steam_libraryfolders_vdf() {
|
|
||||||
let lf = parse_steam_libraryfolders_vdf(Path::new("./test/files/steam_libraryfolders.vdf"))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(lf.len(), 1);
|
|
||||||
let first = lf.get(&0).unwrap();
|
|
||||||
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
|
|
||||||
assert_eq!(first.apps.len(), 10);
|
|
||||||
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
|
|
||||||
assert_eq!(first.apps.get(&632360).unwrap(), &0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -47,7 +47,6 @@ use crate::{
|
||||||
};
|
};
|
||||||
use adw::{prelude::*, ResponseAppearance};
|
use adw::{prelude::*, ResponseAppearance};
|
||||||
use gtk::glib::{self, clone};
|
use gtk::glib::{self, clone};
|
||||||
use tracing::error;
|
|
||||||
use notify_rust::NotificationHandle;
|
use notify_rust::NotificationHandle;
|
||||||
use relm4::{
|
use relm4::{
|
||||||
actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup},
|
actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup},
|
||||||
|
@ -55,6 +54,7 @@ use relm4::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use std::{collections::VecDeque, fs::remove_file, time::Duration};
|
use std::{collections::VecDeque, fs::remove_file, time::Duration};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
application: adw::Application,
|
application: adw::Application,
|
||||||
|
|
|
@ -6,9 +6,9 @@ use crate::{
|
||||||
profile::{Profile, XRServiceType},
|
profile::{Profile, XRServiceType},
|
||||||
};
|
};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use tracing::error;
|
|
||||||
use relm4::{new_action_group, new_stateless_action, prelude::*};
|
use relm4::{new_action_group, new_stateless_action, prelude::*};
|
||||||
use std::fs::remove_file;
|
use std::fs::remove_file;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
const WIVRN_LATEST_RELEASE_APK_URL: &str =
|
const WIVRN_LATEST_RELEASE_APK_URL: &str =
|
||||||
"https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk";
|
"https://github.com/WiVRn/WiVRn/releases/latest/download/WiVRn-standard-release.apk";
|
||||||
|
@ -185,10 +185,7 @@ impl AsyncComponent for InstallWivrnBox {
|
||||||
let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk"));
|
let existing = cache_file_path(WIVRN_LATEST_RELEASE_APK_URL, Some("apk"));
|
||||||
if existing.is_file() {
|
if existing.is_file() {
|
||||||
if let Err(e) = remove_file(&existing) {
|
if let Err(e) = remove_file(&existing) {
|
||||||
error!(
|
error!("failed to remove file {}: {e}", existing.to_string_lossy());
|
||||||
"failed to remove file {}: {e}",
|
|
||||||
existing.to_string_lossy()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into()));
|
sender.input(Self::Input::DoInstall(WIVRN_LATEST_RELEASE_APK_URL.into()));
|
||||||
|
|
|
@ -4,7 +4,6 @@ use self::{
|
||||||
state::JobWorkerState,
|
state::JobWorkerState,
|
||||||
};
|
};
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
use tracing::{error, warn};
|
|
||||||
use nix::sys::signal::{
|
use nix::sys::signal::{
|
||||||
kill,
|
kill,
|
||||||
Signal::{SIGKILL, SIGTERM},
|
Signal::{SIGKILL, SIGTERM},
|
||||||
|
@ -16,6 +15,7 @@ use std::{
|
||||||
thread::{self, sleep},
|
thread::{self, sleep},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
pub mod internal_worker;
|
pub mod internal_worker;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
|
|
|
@ -30,13 +30,13 @@ use crate::{
|
||||||
};
|
};
|
||||||
use adw::{prelude::*, ResponseAppearance};
|
use adw::{prelude::*, ResponseAppearance};
|
||||||
use gtk::glib::clone;
|
use gtk::glib::clone;
|
||||||
use tracing::{error, warn};
|
|
||||||
use relm4::{
|
use relm4::{
|
||||||
actions::{ActionGroupName, RelmAction, RelmActionGroup},
|
actions::{ActionGroupName, RelmAction, RelmActionGroup},
|
||||||
new_action_group, new_stateless_action,
|
new_action_group, new_stateless_action,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use std::{fs::read_to_string, io::Write};
|
use std::{fs::read_to_string, io::Write};
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct MainView {
|
pub struct MainView {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::{constants::APP_NAME, xdg::XDG};
|
use crate::{constants::APP_NAME, xdg::XDG};
|
||||||
use tracing::{debug, error};
|
|
||||||
use relm4::{
|
use relm4::{
|
||||||
gtk::{self, prelude::*},
|
gtk::{self, prelude::*},
|
||||||
ComponentParts, ComponentSender, SimpleComponent,
|
ComponentParts, ComponentSender, SimpleComponent,
|
||||||
};
|
};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct OpenHmdCalibrationBox {
|
pub struct OpenHmdCalibrationBox {
|
||||||
|
|
|
@ -4,7 +4,6 @@ use super::job_worker::{
|
||||||
JobWorker,
|
JobWorker,
|
||||||
};
|
};
|
||||||
use crate::paths::get_steamvr_bin_dir_path;
|
use crate::paths::get_steamvr_bin_dir_path;
|
||||||
use tracing::error;
|
|
||||||
use relm4::{
|
use relm4::{
|
||||||
gtk::{self, prelude::*},
|
gtk::{self, prelude::*},
|
||||||
ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent,
|
ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent,
|
||||||
|
@ -15,6 +14,7 @@ use std::{
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct SteamVrCalibrationBox {
|
pub struct SteamVrCalibrationBox {
|
||||||
|
|
|
@ -20,8 +20,8 @@ use crate::{
|
||||||
};
|
};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use gtk::glib::clone;
|
use gtk::glib::clone;
|
||||||
use tracing::error;
|
|
||||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct WivrnConfEditor {
|
pub struct WivrnConfEditor {
|
||||||
|
|
|
@ -6,8 +6,8 @@ use crate::{
|
||||||
profile::{Profile, XRServiceType},
|
profile::{Profile, XRServiceType},
|
||||||
};
|
};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use tracing::error;
|
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub enum StartClientStatus {
|
pub enum StartClientStatus {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::{async_process::async_process, profile::Profile};
|
use crate::{async_process::async_process, profile::Profile};
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use tracing::{debug, error};
|
|
||||||
use nix::{
|
use nix::{
|
||||||
errno::Errno,
|
errno::Errno,
|
||||||
sys::statvfs::{statvfs, FsFlags},
|
sys::statvfs::{statvfs, FsFlags},
|
||||||
|
@ -10,6 +9,7 @@ use std::{
|
||||||
io::{BufReader, BufWriter},
|
io::{BufReader, BufWriter},
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
|
pub fn get_writer(path: &Path) -> anyhow::Result<BufWriter<std::fs::File>> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod file_utils;
|
pub mod file_utils;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
|
pub mod steam_library_folder;
|
||||||
pub mod steamvr_utils;
|
pub mod steamvr_utils;
|
||||||
|
|
69
src/util/steam_library_folder.rs
Normal file
69
src/util/steam_library_folder.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use crate::paths::get_home_dir;
|
||||||
|
use anyhow::bail;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::read_to_string,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SteamLibraryFolder {
|
||||||
|
pub path: String,
|
||||||
|
pub apps: HashMap<u32, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_steam_main_dir_path() -> anyhow::Result<PathBuf> {
|
||||||
|
let steam_root: PathBuf = get_home_dir().join(".steam/root");
|
||||||
|
|
||||||
|
if steam_root.is_symlink() {
|
||||||
|
Ok(steam_root.read_link()?)
|
||||||
|
} else if steam_root.is_dir() {
|
||||||
|
Ok(steam_root)
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"Canonical steam root '{}' is not a dir nor a symlink!",
|
||||||
|
steam_root.to_string_lossy()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SteamLibraryFolder {
|
||||||
|
pub fn get_folders() -> anyhow::Result<HashMap<u32, Self>> {
|
||||||
|
let libraryfolders_path = get_steam_main_dir_path()?.join("steamapps/libraryfolders.vdf");
|
||||||
|
if !libraryfolders_path.is_file() {
|
||||||
|
bail!(
|
||||||
|
"Steam libraryfolders.vdf does not exist in its canonical location {}",
|
||||||
|
libraryfolders_path.to_string_lossy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::get_folders_from_path(&libraryfolders_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do not use this: use get_folders() instead as it always uses Steam's
|
||||||
|
/// canonical root path. This is intended to be directly used only for
|
||||||
|
/// unit tests
|
||||||
|
pub fn get_folders_from_path(p: &Path) -> anyhow::Result<HashMap<u32, Self>> {
|
||||||
|
Ok(keyvalues_serde::from_str(read_to_string(p)?.as_str())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::SteamLibraryFolder;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_steam_libraryfolders_vdf() {
|
||||||
|
let lf = SteamLibraryFolder::get_folders_from_path(Path::new(
|
||||||
|
"./test/files/steam_libraryfolders.vdf",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(lf.len(), 1);
|
||||||
|
let first = lf.get(&0).unwrap();
|
||||||
|
assert_eq!(first.path, "/home/gabmus/.local/share/Steam");
|
||||||
|
assert_eq!(first.apps.len(), 10);
|
||||||
|
assert_eq!(first.apps.get(&228980).unwrap(), &29212173);
|
||||||
|
assert_eq!(first.apps.get(&632360).unwrap(), &0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use libmonado::{self, BatteryStatus, DeviceRole};
|
use libmonado::{self, BatteryStatus, DeviceRole};
|
||||||
use tracing::error;
|
|
||||||
use std::{collections::HashMap, fmt::Display, slice::Iter};
|
use std::{collections::HashMap, fmt::Display, slice::Iter};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum XRDeviceRole {
|
pub enum XRDeviceRole {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue