use once_cell::sync::Lazy; use regex::bytes::Regex; use rustc_hash::FxHashMap; use std::collections::VecDeque; use std::ffi::{OsStr, OsString}; use std::fmt::{Debug, Display, Formatter}; use std::ops::Add; use std::path::{Component, Path, PathBuf}; #[repr(u32)] #[derive(Debug, Copy, Clone)] pub enum VideoDriver { None = 0, GlCore, Gl, Vulkan, Direct3D11, Direct3D9Hlsl, Direct3D12, Metal, } impl Display for VideoDriver { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { VideoDriver::None => f.write_str("null"), VideoDriver::GlCore => f.write_str("glcore"), VideoDriver::Gl => f.write_str("gl"), VideoDriver::Vulkan => f.write_str("vulkan"), VideoDriver::Direct3D11 => f.write_str("d3d11"), VideoDriver::Direct3D9Hlsl => f.write_str("d3d9_hlsl"), VideoDriver::Direct3D12 => f.write_str("d3d12"), VideoDriver::Metal => f.write_str("metal"), } } } #[repr(u32)] #[derive(Debug, Copy, Clone)] pub enum ShaderExtension { Slang = 0, Glsl, Cg, } impl Display for ShaderExtension { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ShaderExtension::Slang => f.write_str("slang"), ShaderExtension::Glsl => f.write_str("glsl"), ShaderExtension::Cg => f.write_str("cg"), } } } #[repr(u32)] #[derive(Debug, Copy, Clone)] pub enum PresetExtension { Slangp = 0, Glslp, Cgp, } impl Display for PresetExtension { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { PresetExtension::Slangp => f.write_str("slangp"), PresetExtension::Glslp => f.write_str("glslp"), PresetExtension::Cgp => f.write_str("cgp"), } } } #[repr(u32)] #[derive(Debug, Copy, Clone)] pub enum Rotation { /// Zero Zero = 0, /// 90 degrees Right = 1, /// 180 degrees Straight = 2, /// 270 degrees Reflex = 3, } impl From for Rotation { fn from(value: u32) -> Self { let value = value % 4; match value { 0 => Rotation::Zero, 1 => Rotation::Right, 2 => Rotation::Straight, 3 => Rotation::Reflex, _ => unreachable!(), } } } impl Add for Rotation { type Output = Rotation; fn add(self, rhs: Self) -> Self::Output { let lhs = self as u32; let out = lhs + rhs as u32; Rotation::from(out) } } impl Display for Rotation { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Rotation::Zero => f.write_str("0"), Rotation::Right => f.write_str("90"), Rotation::Straight => f.write_str("180"), Rotation::Reflex => f.write_str("270"), } } } #[repr(u32)] #[derive(Debug, Copy, Clone)] pub enum Orientation { Vertical = 0, Horizontal, } impl Display for Orientation { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Orientation::Vertical => f.write_str("VERT"), Orientation::Horizontal => f.write_str("HORZ"), } } } #[derive(Debug, Clone)] pub enum ContextItem { ContentDirectory(String), CoreName(String), GameName(String), Preset(String), PresetDirectory(String), VideoDriver(VideoDriver), VideoDriverShaderExtension(ShaderExtension), VideoDriverPresetExtension(PresetExtension), CoreRequestedRotation(Rotation), AllowCoreRotation(bool), UserRotation(Rotation), FinalRotation(Rotation), ScreenOrientation(Rotation), ViewAspectOrientation(Orientation), CoreAspectOrientation(Orientation), ExternContext(String, String), } impl ContextItem { fn toggle_str(v: bool) -> &'static str { if v { "ON" } else { "OFF" } } pub fn key(&self) -> &str { match self { ContextItem::ContentDirectory(_) => "CONTENT-DIR", ContextItem::CoreName(_) => "CORE", ContextItem::GameName(_) => "GAME", ContextItem::Preset(_) => "PRESET", ContextItem::PresetDirectory(_) => "PRESET_DIR", ContextItem::VideoDriver(_) => "VID-DRV", ContextItem::CoreRequestedRotation(_) => "CORE-REQ-ROT", ContextItem::AllowCoreRotation(_) => "VID-ALLOW-CORE-ROT", ContextItem::UserRotation(_) => "VID-USER-ROT", ContextItem::FinalRotation(_) => "VID-FINAL-ROT", ContextItem::ScreenOrientation(_) => "SCREEN-ORIENT", ContextItem::ViewAspectOrientation(_) => "VIEW-ASPECT-ORIENT", ContextItem::CoreAspectOrientation(_) => "CORE-ASPECT-ORIENT", ContextItem::VideoDriverShaderExtension(_) => "VID-DRV-SHADER-EXT", ContextItem::VideoDriverPresetExtension(_) => "VID-DRV-PRESET-EXT", ContextItem::ExternContext(key, _) => key, } } } impl Display for ContextItem { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ContextItem::ContentDirectory(v) => f.write_str(v), ContextItem::CoreName(v) => f.write_str(v), ContextItem::GameName(v) => f.write_str(v), ContextItem::Preset(v) => f.write_str(v), ContextItem::PresetDirectory(v) => f.write_str(v), ContextItem::VideoDriver(v) => f.write_fmt(format_args!("{}", v)), ContextItem::CoreRequestedRotation(v) => { f.write_fmt(format_args!("{}-{}", self.key(), v)) } ContextItem::AllowCoreRotation(v) => f.write_fmt(format_args!( "{}-{}", self.key(), ContextItem::toggle_str(*v) )), ContextItem::UserRotation(v) => f.write_fmt(format_args!("{}-{}", self.key(), v)), ContextItem::FinalRotation(v) => f.write_fmt(format_args!("{}-{}", self.key(), v)), ContextItem::ScreenOrientation(v) => f.write_fmt(format_args!("{}-{}", self.key(), v)), ContextItem::ViewAspectOrientation(v) => { f.write_fmt(format_args!("{}-{}", self.key(), v)) } ContextItem::CoreAspectOrientation(v) => { f.write_fmt(format_args!("{}-{}", self.key(), v)) } ContextItem::VideoDriverShaderExtension(v) => f.write_fmt(format_args!("{}", v)), ContextItem::VideoDriverPresetExtension(v) => f.write_fmt(format_args!("{}", v)), ContextItem::ExternContext(_, v) => f.write_fmt(format_args!("{}", v)), } } } /// A preset wildcard context. /// /// Any items added after will have higher priority /// when passed to the shader preset parser. /// /// When passed to the preset parser, the preset parser /// will automatically add inferred items at lowest priority. /// /// Any items added by the user will override the automatically /// inferred items. #[derive(Debug, Clone)] pub struct WildcardContext(VecDeque); impl WildcardContext { /// Create a new wildcard context. pub fn new() -> Self { Self(VecDeque::new()) } /// Prepend an item to the context builder. pub fn prepend_item(&mut self, item: ContextItem) { self.0.push_front(item); } /// Append an item to the context builder. /// The new item will take precedence over all items added before it. pub fn append_item(&mut self, item: ContextItem) { self.0.push_back(item); } /// Prepend sensible defaults for the given video driver. /// /// Any values added, either previously or afterwards will not be overridden. pub fn add_video_driver_defaults(&mut self, video_driver: VideoDriver) { self.prepend_item(ContextItem::VideoDriverPresetExtension( PresetExtension::Slangp, )); self.prepend_item(ContextItem::VideoDriverShaderExtension( ShaderExtension::Slang, )); self.prepend_item(ContextItem::VideoDriver(video_driver)); } /// Prepend default entries from the path of the preset. /// /// Any values added, either previously or afterwards will not be overridden. pub fn add_path_defaults(&mut self, path: impl AsRef) { let path = path.as_ref(); if let Some(preset_name) = path.file_stem() { let preset_name = preset_name.to_string_lossy(); self.prepend_item(ContextItem::Preset(preset_name.into())) } if let Some(preset_dir_name) = path.parent().and_then(|p| { if !p.is_dir() { return None; }; p.file_name() }) { let preset_dir_name = preset_dir_name.to_string_lossy(); self.prepend_item(ContextItem::PresetDirectory(preset_dir_name.into())) } } pub(crate) fn to_hashmap(mut self) -> FxHashMap { let mut map = FxHashMap::default(); let last_user_rot = self .0 .iter() .rfind(|i| matches!(i, ContextItem::UserRotation(_))); let last_core_rot = self .0 .iter() .rfind(|i| matches!(i, ContextItem::CoreRequestedRotation(_))); let final_rot = match (last_core_rot, last_user_rot) { (Some(ContextItem::UserRotation(u)), None) => Some(ContextItem::FinalRotation(*u)), (None, Some(ContextItem::CoreRequestedRotation(c))) => { Some(ContextItem::FinalRotation(*c)) } (Some(ContextItem::UserRotation(u)), Some(ContextItem::CoreRequestedRotation(c))) => { Some(ContextItem::FinalRotation(*u + *c)) } _ => None, }; if let Some(final_rot) = final_rot { self.prepend_item(final_rot); } for item in self.0 { map.insert(String::from(item.key()), item.to_string()); } map } } pub fn apply_context(path: &mut PathBuf, context: &FxHashMap) { static WILDCARD_REGEX: Lazy = Lazy::new(|| Regex::new("\\$([A-Z-_]+)\\$").unwrap()); if context.is_empty() { return; } // Don't want to do any extra work if there's no match. if !WILDCARD_REGEX.is_match(path.as_os_str().as_encoded_bytes()) { return; } let mut new_path = PathBuf::with_capacity(path.capacity()); for component in path.components() { match component { Component::Normal(path) => { let haystack = path.as_encoded_bytes(); let replaced = WILDCARD_REGEX.replace_all(haystack, |caps: ®ex::bytes::Captures| { let Some(name) = caps.get(1) else { return caps[0].to_vec(); }; let Ok(key) = std::str::from_utf8(name.as_bytes()) else { return caps[0].to_vec(); }; if let Some(replacement) = context.get(key) { return OsString::from(replacement.to_string()).into_encoded_bytes(); } return caps[0].to_vec(); }); // SAFETY: The original source is valid encoded bytes, and our replacement is // valid encoded bytes. This upholds the safety requirements of `from_encoded_bytes_unchecked`. new_path.push(unsafe { OsStr::from_encoded_bytes_unchecked(&replaced.as_ref()) }) } _ => new_path.push(component), } } // If no wildcards are found within the path, or the path after replacing the wildcards does not exist on disk, the path returned will be unaffected. if let Ok(true) = new_path.try_exists() { *path = new_path; } }