From 60bbe7558910fb1e70a20533c6771b4a5ea0471a Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Tue, 15 Aug 2023 20:23:16 +0200 Subject: [PATCH] Some more cleanup --- Cargo.lock | 86 +++-- Cargo.toml | 29 +- README.md | 2 +- obs/src/lib.rs | 16 +- src/auto_splitters.rs | 212 +++++++++++ src/autosplitters.rs | 255 ------------- src/ffi.rs | 21 +- src/lib.rs | 807 +++++++++++++++++++++--------------------- 8 files changed, 715 insertions(+), 713 deletions(-) create mode 100644 src/auto_splitters.rs delete mode 100644 src/autosplitters.rs diff --git a/Cargo.lock b/Cargo.lock index 74eacac..d0be68c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -1490,7 +1500,6 @@ name = "obs-livesplit-one" version = "0.1.0" dependencies = [ "hyperx", - "lazy_static", "livesplit-core", "log", "obs", @@ -1517,6 +1526,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "outref" version = "0.5.1" @@ -1626,9 +1641,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", "serde", @@ -1804,6 +1819,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", + "rustls-native-certs", "rustls-pemfile", "serde", "serde_json", @@ -1815,7 +1831,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", "winreg", ] @@ -1901,6 +1916,18 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.3" @@ -1943,6 +1970,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1959,6 +1995,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.183" @@ -2834,25 +2893,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - [[package]] name = "weezl" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index e8b31dc..fda68c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,20 +14,35 @@ crate-type = ["cdylib"] [dependencies] obs = { path = "obs" } -livesplit-core = { git = "https://github.com/LiveSplit/livesplit-core", features = ["software-rendering", "font-loading"] } +livesplit-core = { git = "https://github.com/LiveSplit/livesplit-core", features = [ + "software-rendering", + "font-loading", +] } log = { version = "0.4.6", features = ["serde"] } -lazy_static = "1.4.0" -# crates needed for the auto splitter management code to work as expected -reqwest = { version = "0.11.18", features = ["blocking", "rustls-tls"], default-features = false, optional = true } +# crates needed for the auto splitter management code to work as expected hyperx = { version = "1.4.0", optional = true } -quick-xml = { version = "0.29.0", features = ["serialize", "overlapped-lists"], optional = true } -serde = { version = "1.0.166", features = ["serde_derive"], optional = true } open = { version = "5.0.0", optional = true } +quick-xml = { version = "0.30.0", features = [ + "serialize", + "overlapped-lists", +], optional = true } +reqwest = { version = "0.11.18", features = [ + "blocking", + "rustls-tls-native-roots", +], default-features = false, optional = true } +serde = { version = "1.0.166", features = ["derive"], optional = true } [features] default = ["auto-splitting"] -auto-splitting = ["livesplit-core/auto-splitting", "dep:reqwest", "dep:hyperx", "dep:quick-xml", "dep:serde", "dep:open"] +auto-splitting = [ + "livesplit-core/auto-splitting", + "dep:hyperx", + "dep:open", + "dep:quick-xml", + "dep:reqwest", + "dep:serde", +] [profile.max-opt] inherits = "release" diff --git a/README.md b/README.md index c933787..c6a5398 100644 --- a/README.md +++ b/README.md @@ -39,5 +39,5 @@ name, where you can set hotkeys for the various actions. If you add multiple sources that each use the same splits, but different layouts, they all share the same state. This allows for a lot more complex -layouts than what is traditionally possible where could for example show the +layouts than what is traditionally possible where you could for example show the splits on a completely different part of your stream than the timer itself. diff --git a/obs/src/lib.rs b/obs/src/lib.rs index f619184..6a51900 100644 --- a/obs/src/lib.rs +++ b/obs/src/lib.rs @@ -208,14 +208,14 @@ pub extern "C" fn obs_properties_add_text( _name: *const c_char, _description: *const c_char, _text_type: obs_text_type, -) -> *mut obs_property_t { - panic!() +) -> *mut obs_property_t { + panic!() } #[no_mangle] pub extern "C" fn obs_property_set_modified_callback2( _prop: *mut obs_property_t, _modified2_callback: obs_property_modified2_t, - _private: *mut c_void + _private: *mut c_void, ) { panic!() } @@ -227,10 +227,7 @@ pub extern "C" fn obs_property_set_description( panic!() } #[no_mangle] -pub extern "C" fn obs_property_set_enabled( - _prop: *mut obs_property_t, - _enabled: bool -) { +pub extern "C" fn obs_property_set_enabled(_prop: *mut obs_property_t, _enabled: bool) { panic!() } #[no_mangle] @@ -241,6 +238,9 @@ pub extern "C" fn obs_properties_get( panic!() } #[no_mangle] -pub extern "C" fn obs_module_get_config_path(_module: *mut obs_module_t, _file: *const c_char) -> *const c_char { +pub extern "C" fn obs_module_get_config_path( + _module: *mut obs_module_t, + _file: *const c_char, +) -> *const c_char { panic!() } diff --git a/src/auto_splitters.rs b/src/auto_splitters.rs new file mode 100644 index 0000000..a32f7af --- /dev/null +++ b/src/auto_splitters.rs @@ -0,0 +1,212 @@ +use hyperx::header::{DispositionParam, Header}; +use log::warn; +use quick_xml::{de, DeError}; +use reqwest::header::{HeaderMap, CONTENT_DISPOSITION}; +use serde::Deserialize; +use std::{ + fs, + path::{Path, PathBuf}, + str, +}; + +const LIST_FILE_NAME: &str = "LiveSplit.AutoSplitters.xml"; + +pub struct ListManager { + client: reqwest::blocking::Client, + list: Result, + list_xml_string: Option, +} + +pub enum GetListFromGithubError { + NetError(reqwest::Error), + DeserializationError(DeError), +} + +pub enum GetListFromFileError { + IoError(std::io::Error), + DeserializationError(DeError), +} + +#[derive(Deserialize, Clone)] +pub struct List { + #[serde(rename = "AutoSplitter")] + pub auto_splitters: Vec, +} + +#[derive(Deserialize, Clone)] +pub struct AutoSplitter { + #[serde(rename = "Games")] + pub games: Games, + #[serde(rename = "URLs")] + pub urls: Urls, + #[serde(rename = "Type")] + pub module_type: String, + #[serde(rename = "ScriptType")] + pub script_type: Option, + #[serde(rename = "Description")] + pub description: String, + #[serde(rename = "Website")] + pub website: Option, +} + +#[derive(Deserialize, Clone)] +pub struct Games { + #[serde(rename = "Game")] + pub games: Vec, +} + +#[derive(Deserialize, Clone)] +pub struct Urls { + #[serde(rename = "URL")] + pub urls: Vec, +} + +impl ListManager { + pub fn new(folder: &Path) -> Self { + let client = reqwest::blocking::Client::new(); + + let result = Self::get_list(&client, folder); + + let (list, list_xml_string) = match result { + Ok((string, list)) => (Ok(list), Some(string)), + Err(e) => (Err(e), None), + }; + + Self { + client, + list, + list_xml_string, + } + } + + pub fn get_result(&self) -> Result<(), &(GetListFromGithubError, GetListFromFileError)> { + match &self.list { + Ok(_) => Ok(()), + Err(err) => Err(err), + } + } + + pub fn save_list_to_disk(&self, folder: &Path) -> bool { + if let Some(xml_string) = &self.list_xml_string { + fs::write(folder.join(LIST_FILE_NAME), xml_string).is_ok() + } else { + false + } + } + + pub fn get_website_for_game(&self, game_name: &str) -> Option<&str> { + self.get_for_game(game_name)?.website.as_deref() + } + + pub fn get_for_game(&self, game_name: &str) -> Option<&AutoSplitter> { + self.list + .as_ref() + .ok()? + .auto_splitters + .iter() + .find(|x| x.games.games.iter().any(|g| g == game_name)) + } + + //todo + pub fn download_for_game(&self, game_name: &str, folder: &Path) -> Option { + self.download(self.get_for_game(game_name)?, folder) + } + + pub fn download(&self, auto_splitter: &AutoSplitter, folder: &Path) -> Option { + let mut file_paths = Vec::new(); + + for url in &auto_splitter.urls.urls { + let Ok(response) = self.client.get(url).send() else { + continue; + }; + + let file_name = Self::get_requested_file_name(response.headers()).unwrap_or_else(|| { + warn!("Couldn't get name for auto splitter file: {url}, defaulting to 'Unknown.wasm'"); + "Unknown.wasm".into() + }); + + if let Ok(bytes) = response.bytes() { + let file_path = folder.join(&*file_name); + + match fs::write(&file_path, bytes) { + Ok(_) => { + file_paths.push(file_path); + } + Err(e) => { + warn!("Something went wrong when downloading and saving auto splitter file: {url}: {e}") + } + } + } + } + + file_paths + .into_iter() + .find(|path| path.extension().is_some_and(|e| e == "wasm")) + } + + pub fn is_using_auto_splitting_runtime(auto_splitter: &AutoSplitter) -> bool { + auto_splitter + .script_type + .as_ref() + .is_some_and(|t| t == "AutoSplittingRuntime") + } + + fn get_list( + client: &reqwest::blocking::Client, + folder: &Path, + ) -> Result<(String, List), (GetListFromGithubError, GetListFromFileError)> { + let from_github_error = match Self::get_list_from_github(client) { + Ok(auto_splitters) => return Ok(auto_splitters), + Err(e) => e, + }; + + let from_file_error = match Self::get_list_from_file(folder) { + Ok(auto_splitters) => return Ok(auto_splitters), + Err(e) => e, + }; + + Err((from_github_error, from_file_error)) + } + + fn get_list_from_github( + client: &reqwest::blocking::Client, + ) -> Result<(String, List), GetListFromGithubError> { + let url = "https://raw.githubusercontent.com/LiveSplit/LiveSplit.AutoSplitters/master/LiveSplit.AutoSplitters.xml"; + + let body = client + .get(url) + .send() + .map_err(GetListFromGithubError::NetError)? + .text() + .map_err(GetListFromGithubError::NetError)?; + + match de::from_str(&body) { + Ok(auto_splitters) => Ok((body, auto_splitters)), + Err(e) => Err(GetListFromGithubError::DeserializationError(e)), + } + } + + fn get_list_from_file(folder: &Path) -> Result<(String, List), GetListFromFileError> { + let buffer = fs::read_to_string(folder.join(LIST_FILE_NAME)) + .map_err(GetListFromFileError::IoError)?; + + match de::from_str::(&buffer) { + Ok(auto_splitters) => Ok((buffer, auto_splitters)), + Err(e) => Err(GetListFromFileError::DeserializationError(e)), + } + } + + // TODO: Make this return Result and improve support + fn get_requested_file_name(header_map: &HeaderMap) -> Option> { + hyperx::header::ContentDisposition::parse_header(&header_map.get(CONTENT_DISPOSITION)?) + .ok()? + .parameters + .into_iter() + .find_map(|param| { + let DispositionParam::Filename(_, _, bytes) = param else { + return None; + }; + Some(str::from_utf8(&bytes).ok()?.into()) + }) + } +} diff --git a/src/autosplitters.rs b/src/autosplitters.rs deleted file mode 100644 index 2ef3fe7..0000000 --- a/src/autosplitters.rs +++ /dev/null @@ -1,255 +0,0 @@ -use std::{fs::File, io::{Write, Read}, path::{PathBuf, Path}, str::from_utf8}; -use quick_xml::{de::from_str, DeError}; -use serde::Deserialize; -use reqwest::{blocking::get, header::{CONTENT_DISPOSITION, HeaderMap}}; -use hyperx::header::{DispositionParam, Header}; - -const AUTO_SPLITTER_LIST_FILE_NAME: &str = "LiveSplit.AutoSplitters.xml"; - -pub struct AutoSplitterListManager { - list: Result, - list_xml_string: Option -} - -pub enum GetAutoSplitterListFromGithubError { - NetError(reqwest::Error), - DeserializationError(DeError) -} - -pub enum GetAutoSplitterListFromFileError { - IoError(std::io::Error), - DeserializationError(DeError) -} - -#[derive(Deserialize, Clone)] -pub struct AutoSplitterList { - #[serde(rename = "AutoSplitter")] - pub auto_splitters: Vec -} - -#[derive(Deserialize, Clone)] -pub struct AutoSplitter { - #[serde(rename = "Games")] - pub games: Games, - #[serde(rename = "URLs")] - pub urls: Urls, - #[serde(rename = "Type")] - pub module_type: String, - #[serde(rename = "ScriptType")] - pub script_type: Option, - #[serde(rename = "Description")] - pub description: String, - #[serde(rename = "Website")] - pub website: Option -} - -#[derive(Deserialize, Clone)] -pub struct Games { - #[serde(rename = "Game")] - pub games: Vec -} - -#[derive(Deserialize, Clone)] -pub struct Urls { - #[serde(rename = "URL")] - pub urls: Vec -} - -impl AutoSplitterListManager { - pub fn new() -> Self { - let result = Self::get_auto_splitter_list(); - - let (list, list_xml_string) = match result { - Ok(value) => { (Ok(value.1), Some(value.0)) } - Err(e) => { (Err(e), None) } - }; - - Self { - list, - list_xml_string - } - } - - pub fn is_ok(&self) -> Result<(), &(GetAutoSplitterListFromGithubError, GetAutoSplitterListFromFileError)> { - match &self.list { - Ok(_) => { Ok(()) } - Err(err) => { Err(err) } - } - } - - pub fn save_auto_splitter_list_to_disk(&self) -> bool { - let file = File::create(Path::join(&*super::OBS_MODULE_CONFIG_PATH, AUTO_SPLITTER_LIST_FILE_NAME)); - - if let (Some(xml_string), Ok(mut file)) = (self.list_xml_string.clone(), file) { - return match file.write_all(xml_string.as_bytes()) { - Ok(_) => { true } - Err(_) => { false } - }; - }; - - false - } - - pub fn get_auto_splitter_website_for_game(&self, game_name: String) -> Option { - match &self.list { - Ok(result) => { - match result.auto_splitters.iter().find( - |&x| x.games.games.contains(&game_name)) { - Some(auto_splitter) => { auto_splitter.website.clone() } - None => { None } - } - } - Err(_) => { None } - } - } - - pub fn get_auto_splitter_for_game(&self, game_name: String) -> Option<&AutoSplitter> { - match &self.list { - Ok(result) => { - return result.auto_splitters.iter().find(|&x| x.games.games.contains(&game_name)) - } - Err(_) => { None } - } - } - - //todo - pub fn download_auto_splitter_for_game(&self, game_name: String) -> Option { - match &self.list { - Ok(result) => { - return match result.auto_splitters.iter().find(|&x| x.games.games.contains(&game_name)) { - Some(auto_splitter) => { Self::download_auto_splitter(auto_splitter) } - None => { None } - } - } - Err(_) => { None } - } - } - - pub fn download_auto_splitter(auto_splitter: &AutoSplitter) -> Option { - - let file_paths = &mut Vec::::new(); - - for url in &auto_splitter.urls.urls { - let response = match get(url) { - Ok(response) => { response } - Err(_) => { continue } - }; - - let file_name = match Self::get_requested_file_name(response.headers()) { - Some(file_name) => { file_name } - None => { - log::warn!("Couldn't get name for auto splitter file: {}, defaulting to 'Unknown.wasm'", url); - String::from("Unknown.wasm") - } - }; - - let file_path = Path::join(&*super::AUTO_SPLITTERS_PATH, file_name.clone()); - - let file = File::create(file_path.clone()); - - if let (Ok(bytes), Ok(mut file)) = (response.bytes(), file) { - match file.write_all(bytes.as_ref()) { - Ok(_) => { file_paths.push(file_path); } - Err(e) => { log::warn!("Something went wrong when downloading and saving auto splitter file: {}: {}", url, e) } - }; - } - }; - - let auto_splitter_file_path = file_paths.iter().find(|path| { - if let Some(extension) = path.extension() { - return extension == "wasm"; - } - - false - }); - - match auto_splitter_file_path { - Some(path) => { return Some(path.clone()) } - None => { None } - } - } - - pub fn is_using_auto_splitting_runtime(auto_splitter: &AutoSplitter) -> bool { - return auto_splitter.script_type.is_some() && auto_splitter.script_type.as_ref().unwrap() == "AutoSplittingRuntime"; - } - - fn get_auto_splitter_list() -> Result<(String, AutoSplitterList), (GetAutoSplitterListFromGithubError, GetAutoSplitterListFromFileError)> { - let from_github_result = Self::get_auto_splitter_list_from_github(); - - let from_github_error = match from_github_result { - Ok(auto_splitters) => { return Ok(auto_splitters) } - Err(e) => { e } - }; - - let from_file_result = Self::get_auto_splitter_list_from_file(); - - let from_file_error = match from_file_result { - Ok(auto_splitters) => { return Ok(auto_splitters) } - Err(e) => { e } - }; - - Err((from_github_error, from_file_error)) - } - - fn get_auto_splitter_list_from_github() -> Result<(String, AutoSplitterList), GetAutoSplitterListFromGithubError> { - let url = "https://raw.githubusercontent.com/LiveSplit/LiveSplit.AutoSplitters/master/LiveSplit.AutoSplitters.xml"; - - let response = match get(url) { - Ok(response) => { response } - Err(e) => { return Err(GetAutoSplitterListFromGithubError::NetError(e)); } - }; - - let body = match response.text() { - Ok(body) => { body } - Err(e) => { return Err(GetAutoSplitterListFromGithubError::NetError(e)); } - }; - - match from_str::(body.as_str()) { - Ok(auto_splitters) => { Ok((body, auto_splitters)) } - Err(e) => { return Err(GetAutoSplitterListFromGithubError::DeserializationError(e)) } - } - } - - fn get_auto_splitter_list_from_file() -> Result<(String, AutoSplitterList), GetAutoSplitterListFromFileError> { - let mut file = match File::open(Path::join(&*super::OBS_MODULE_CONFIG_PATH, AUTO_SPLITTER_LIST_FILE_NAME)) { - Ok(file) => { file } - Err(e) => { return Err(GetAutoSplitterListFromFileError::IoError(e)) } - }; - - let mut buffer = String::new(); - - match file.read_to_string(&mut buffer) { - Ok(_) => { } - Err(e) => { return Err(GetAutoSplitterListFromFileError::IoError(e)) } - } - - match from_str::(buffer.as_str()) { - Ok(auto_splitters) => { Ok((buffer, auto_splitters)) } - Err(e) => { return Err(GetAutoSplitterListFromFileError::DeserializationError(e)) } - } - } - - // TODO: Make this return Result and improve support - fn get_requested_file_name(header_map: &HeaderMap) -> Option { - return match header_map.get(CONTENT_DISPOSITION) { - Some(content_disposition_header) => { - let content_disposition_params = hyperx::header::ContentDisposition::parse_header(&content_disposition_header).ok()?.parameters; - - for param in content_disposition_params { - match param { - DispositionParam::Filename(_, _, bytes) => { - match from_utf8(bytes.as_slice()) { - Ok(file_name) => { return Some(file_name.to_string()); } - Err(_) => { } - }; - } - DispositionParam::Ext(_, _) => { } - } - } - - return None; - } - None => { None } - } - } -} \ No newline at end of file diff --git a/src/ffi.rs b/src/ffi.rs index 3373ecf..5199cad 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -58,6 +58,7 @@ extern "C" { description: *const c_char, ) -> *mut obs_property_t; pub fn obs_data_get_bool(data: *mut obs_data_t, name: *const c_char) -> bool; + #[cfg(feature = "auto-splitting")] pub fn obs_properties_add_text( props: *mut obs_properties_t, name: *const c_char, @@ -100,19 +101,19 @@ extern "C" { pub fn obs_property_set_modified_callback2( prop: *mut obs_property_t, modified2_callback: obs_property_modified2_t, - private: *mut c_void - ); - pub fn obs_property_set_description( - prop: *mut obs_property_t, - description: *const c_char, - ); - pub fn obs_property_set_enabled( - prop: *mut obs_property_t, - enabled: bool + private: *mut c_void, ); + #[cfg(feature = "auto-splitting")] + pub fn obs_property_set_description(prop: *mut obs_property_t, description: *const c_char); + #[cfg(feature = "auto-splitting")] + pub fn obs_property_set_enabled(prop: *mut obs_property_t, enabled: bool); + #[cfg(feature = "auto-splitting")] pub fn obs_properties_get( props: *mut obs_properties_t, prop: *const c_char, ) -> *mut obs_property_t; - pub fn obs_module_get_config_path(module: *mut obs_module_t, file: *const c_char) -> *const c_char; + pub fn obs_module_get_config_path( + module: *mut obs_module_t, + file: *const c_char, + ) -> *const c_char; } diff --git a/src/lib.rs b/src/lib.rs index f148fda..8320a70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,15 +7,18 @@ use std::{ mem, os::raw::{c_char, c_int}, path::{Path, PathBuf}, + process::Command, ptr, - sync::{Arc, Mutex, RwLock, Weak}, + sync::{ + atomic::{self, AtomicPtr}, + Arc, Mutex, OnceLock, Weak, + }, }; -use lazy_static::lazy_static; +#[cfg(feature = "auto-splitting")] +mod auto_splitters; mod ffi; mod ffi_types; -#[cfg(feature = "auto-splitting")] -mod autosplitters; use ffi::{ blog, gs_draw_sprite, gs_effect_get_param_by_name, gs_effect_get_technique, @@ -24,34 +27,43 @@ use ffi::{ gs_texture_set_image, gs_texture_t, obs_data_get_bool, obs_data_get_int, obs_data_get_string, obs_data_set_default_bool, obs_data_set_default_int, obs_data_t, obs_enter_graphics, obs_get_base_effect, obs_hotkey_id, obs_hotkey_register_source, obs_hotkey_t, - obs_leave_graphics, obs_module_t, obs_mouse_event, obs_properties_add_bool, - obs_properties_add_button, obs_properties_add_int, obs_properties_add_path, - obs_module_get_config_path, obs_properties_add_text, obs_properties_get, - obs_property_set_description, obs_property_set_enabled, obs_property_set_modified_callback2, - obs_properties_create, obs_properties_t, obs_property_t, obs_register_source_s, - obs_source_info, obs_source_t, GS_DYNAMIC, GS_RGBA, LOG_WARNING, - OBS_EFFECT_PREMULTIPLIED_ALPHA, OBS_ICON_TYPE_GAME_CAPTURE, OBS_PATH_FILE, + obs_leave_graphics, obs_module_get_config_path, obs_module_t, obs_mouse_event, + obs_properties_add_bool, obs_properties_add_button, obs_properties_add_int, + obs_properties_add_path, obs_properties_create, obs_property_set_modified_callback2, + obs_property_t, obs_register_source_s, obs_source_info, obs_source_t, GS_DYNAMIC, GS_RGBA, + LOG_WARNING, OBS_EFFECT_PREMULTIPLIED_ALPHA, OBS_ICON_TYPE_GAME_CAPTURE, OBS_PATH_FILE, OBS_SOURCE_CONTROLLABLE_MEDIA, OBS_SOURCE_CUSTOM_DRAW, OBS_SOURCE_INTERACTION, - OBS_SOURCE_TYPE_INPUT, OBS_SOURCE_VIDEO, OBS_TEXT_INFO + OBS_SOURCE_TYPE_INPUT, OBS_SOURCE_VIDEO, }; use ffi_types::{ - obs_media_state, LOG_DEBUG, LOG_ERROR, LOG_INFO, OBS_MEDIA_STATE_ENDED, OBS_MEDIA_STATE_PAUSED, - OBS_MEDIA_STATE_PLAYING, OBS_MEDIA_STATE_STOPPED, + obs_media_state, obs_properties_t, LOG_DEBUG, LOG_ERROR, LOG_INFO, OBS_MEDIA_STATE_ENDED, + OBS_MEDIA_STATE_PAUSED, OBS_MEDIA_STATE_PLAYING, OBS_MEDIA_STATE_STOPPED, }; -#[cfg(feature = "auto-splitting")] -use livesplit_core::auto_splitting; + use livesplit_core::{ layout::{self, LayoutSettings, LayoutState}, - rendering::software::{image::EncodableLayout, Renderer}, + rendering::software::Renderer, run::{ parser::{composite, TimerKind}, saver::livesplit::{save_timer, IoWrite}, }, Layout, Run, Segment, SharedTimer, Timer, TimerPhase, }; -use log::{Level, LevelFilter, Log, Metadata, Record}; +use log::{debug, info, warn, Level, LevelFilter, Log, Metadata, Record}; + #[cfg(feature = "auto-splitting")] -use autosplitters::{AutoSplitterListManager, GetAutoSplitterListFromFileError, GetAutoSplitterListFromGithubError}; +use { + self::{ + auto_splitters::{GetListFromFileError, GetListFromGithubError}, + ffi::{ + obs_properties_add_text, obs_properties_get, obs_property_set_description, + obs_property_set_enabled, OBS_TEXT_INFO, + }, + }, + livesplit_core::auto_splitting, + log::error, + std::sync::atomic::AtomicBool, +}; macro_rules! cstr { ($f:literal) => { @@ -59,49 +71,46 @@ macro_rules! cstr { }; } -static mut OBS_MODULE_POINTER: *mut obs_module_t = ptr::null_mut(); +static OBS_MODULE_POINTER: AtomicPtr = AtomicPtr::new(ptr::null_mut()); -#[cfg(feature = "auto-splitting")] -const AUTO_SPLITTERS_FOLDER_NAME: &str = "Components"; +fn get_module_config_path() -> &'static PathBuf { + static OBS_MODULE_CONFIG_PATH: OnceLock = OnceLock::new(); -lazy_static! { - static ref OBS_MODULE_CONFIG_PATH: PathBuf = get_module_config_path(); -} + OBS_MODULE_CONFIG_PATH.get_or_init(|| { + let mut buffer = PathBuf::new(); -#[cfg(feature = "auto-splitting")] -lazy_static! { - static ref AUTO_SPLITTER_LIST_MANAGER: AutoSplitterListManager = AutoSplitterListManager::new(); - - static ref AUTO_SPLITTERS_PATH: PathBuf = get_auto_splitters_path(); -} + unsafe { + let config_path_ptr = obs_module_get_config_path( + OBS_MODULE_POINTER.load(atomic::Ordering::Relaxed), + cstr!(""), + ); + if let Ok(config_path) = CStr::from_ptr(config_path_ptr).to_str() { + buffer.push(config_path); + } + } -fn get_module_config_path() -> PathBuf { - let mut buffer = PathBuf::new(); + buffer + }) +} - unsafe { - let config_path_ptr = obs_module_get_config_path(OBS_MODULE_POINTER, cstr!("")); - match CStr::from_ptr(config_path_ptr).to_str() { - Ok(config_path) => { buffer.push(config_path.to_string()) } - Err(_) => { } - } - } +#[cfg(feature = "auto-splitting")] +fn get_auto_splitter_list_manager() -> &'static auto_splitters::ListManager { + static AUTO_SPLITTER_LIST_MANAGER: OnceLock = OnceLock::new(); - buffer + AUTO_SPLITTER_LIST_MANAGER + .get_or_init(|| auto_splitters::ListManager::new(get_module_config_path())) } #[cfg(feature = "auto-splitting")] -fn get_auto_splitters_path() -> PathBuf { - let mut buffer = PathBuf::new(); - buffer.push(&*OBS_MODULE_CONFIG_PATH); - buffer.push(AUTO_SPLITTERS_FOLDER_NAME); - buffer +fn get_auto_splitters_path() -> &'static PathBuf { + static AUTO_SPLITTERS_PATH: OnceLock = OnceLock::new(); + + AUTO_SPLITTERS_PATH.get_or_init(|| get_module_config_path().join("Components")) } #[no_mangle] pub extern "C" fn obs_module_set_pointer(module: *mut obs_module_t) { - unsafe { - OBS_MODULE_POINTER = module; - } + OBS_MODULE_POINTER.store(module, atomic::Ordering::Relaxed); } #[no_mangle] @@ -114,32 +123,33 @@ struct UnsafeMultiThread(T); unsafe impl Sync for UnsafeMultiThread {} unsafe impl Send for UnsafeMultiThread {} -static TIMERS: Mutex>)>> = Mutex::new(Vec::new()); - -struct State { - game_path: PathBuf, - timer: SharedTimer, - splits_path: PathBuf, +struct GlobalTimer { + path: PathBuf, can_save_splits: bool, - auto_save: bool, - already_parsed_settings: Option<(Run, bool, PathBuf)>, + timer: SharedTimer, #[cfg(feature = "auto-splitting")] auto_splitter: auto_splitting::Runtime, #[cfg(feature = "auto-splitting")] - auto_splitter_is_enabled: bool, + auto_splitter_is_enabled: AtomicBool, +} + +static TIMERS: Mutex>> = Mutex::new(Vec::new()); + +struct State { + game_path: PathBuf, + global_timer: Arc, + auto_save: bool, layout: Layout, state: LayoutState, renderer: Renderer, texture: *mut gs_texture_t, width: u32, - height: u32 + height: u32, } struct Settings { game_path: PathBuf, - run: Run, splits_path: PathBuf, - can_save_splits: bool, auto_save: bool, layout: Layout, width: u32, @@ -164,7 +174,7 @@ fn log(level: Level, target: &str, args: &fmt::Arguments<'_>) { Level::Debug | Level::Trace => LOG_DEBUG, }; unsafe { - blog(level as _, b"%s\0".as_ptr().cast(), str.as_ptr()); + blog(level as _, cstr!("%s"), str.as_ptr()); } } @@ -182,29 +192,22 @@ fn parse_layout(path: &CStr) -> Option { layout::parser::parse(&file_data).ok() } -fn save_splits_file(state: &mut State) -> bool { - if state.can_save_splits { - let timer = state.timer.read().unwrap(); - if let Ok(file) = File::create(&state.splits_path) { +fn save_splits_file(state: &State) -> bool { + if state.global_timer.can_save_splits { + let timer = state.global_timer.timer.read().unwrap(); + if let Ok(file) = File::create(&state.global_timer.path) { let _ = save_timer(&timer, IoWrite(BufWriter::new(file))); } } false } -unsafe fn parse_settings(settings: *mut obs_data_t, run_save_and_path: Option<(Run, bool, PathBuf)>) -> Settings { +unsafe fn parse_settings(settings: *mut obs_data_t) -> Settings { let game_path = CStr::from_ptr(obs_data_get_string(settings, SETTINGS_GAME_PATH).cast()); let game_path = PathBuf::from(game_path.to_string_lossy().into_owned()); - let (run, can_save_splits, splits_path) = match run_save_and_path { - Some(value) => { value } - None => { - let splits_path = CStr::from_ptr(obs_data_get_string(settings, SETTINGS_SPLITS_PATH).cast()); - let splits_path = PathBuf::from(splits_path.to_string_lossy().into_owned()); - let (run, can_save_splits) = parse_run(&splits_path).unwrap_or_else(default_run); - (run, can_save_splits, splits_path) - } - }; + let splits_path = CStr::from_ptr(obs_data_get_string(settings, SETTINGS_SPLITS_PATH).cast()); + let splits_path = PathBuf::from(splits_path.to_string_lossy().into_owned()); let auto_save = obs_data_get_bool(settings, SETTINGS_AUTO_SAVE); @@ -216,9 +219,7 @@ unsafe fn parse_settings(settings: *mut obs_data_t, run_save_and_path: Option<(R Settings { game_path, - run, splits_path, - can_save_splits, auto_save, layout, width, @@ -230,39 +231,16 @@ impl State { unsafe fn new( Settings { game_path, - run, splits_path, - can_save_splits, auto_save, layout, width, height, }: Settings, ) -> Self { - log::info!("Loading settings."); - - let timer = { - let mut timers = TIMERS.lock().unwrap(); - timers.retain(|(_, timer)| timer.strong_count() > 0); - if let Some(timer) = timers.iter().find_map(|(path, timer)| { - if path == &splits_path { - timer.upgrade() - } else { - None - } - }) { - log::debug!("Found timer to reuse."); - timer - } else { - log::debug!("Storing timer for reuse."); - let timer = Timer::new(run).unwrap().into_shared(); - timers.push((splits_path.clone(), Arc::downgrade(&timer))); - timer - } - }; + debug!("Loading settings."); - #[cfg(feature = "auto-splitting")] - let auto_splitter = auto_splitting::Runtime::new(timer.clone()); + let global_timer = get_global_timer(splits_path); let state = LayoutState::default(); let renderer = Renderer::new(); @@ -273,16 +251,9 @@ impl State { Self { game_path, - timer, - splits_path, - can_save_splits, + global_timer, auto_save, - already_parsed_settings: None, layout, - #[cfg(feature = "auto-splitting")] - auto_splitter, - #[cfg(feature = "auto-splitting")] - auto_splitter_is_enabled: false, state, renderer, texture, @@ -291,9 +262,11 @@ impl State { } } - unsafe fn update(&mut self) { - self.layout - .update_state(&mut self.state, &self.timer.read().unwrap().snapshot()); + unsafe fn render(&mut self) { + self.layout.update_state( + &mut self.state, + &self.global_timer.timer.read().unwrap().snapshot(), + ); self.renderer.render(&self.state, [self.width, self.height]); gs_texture_set_image( @@ -317,7 +290,7 @@ unsafe extern "C" fn split( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().split_or_start(); + state.global_timer.timer.write().unwrap().split_or_start(); } } @@ -329,7 +302,7 @@ unsafe extern "C" fn reset( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().reset(true); + state.global_timer.timer.write().unwrap().reset(true); if state.auto_save { save_splits_file(state); @@ -345,7 +318,7 @@ unsafe extern "C" fn undo( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().undo_split(); + state.global_timer.timer.write().unwrap().undo_split(); } } @@ -357,7 +330,7 @@ unsafe extern "C" fn skip( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().skip_split(); + state.global_timer.timer.write().unwrap().skip_split(); } } @@ -369,7 +342,12 @@ unsafe extern "C" fn pause( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().toggle_pause_or_start(); + state + .global_timer + .timer + .write() + .unwrap() + .toggle_pause_or_start(); } } @@ -381,7 +359,7 @@ unsafe extern "C" fn undo_all_pauses( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().undo_all_pauses(); + state.global_timer.timer.write().unwrap().undo_all_pauses(); } } @@ -393,7 +371,12 @@ unsafe extern "C" fn previous_comparison( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().switch_to_previous_comparison(); + state + .global_timer + .timer + .write() + .unwrap() + .switch_to_previous_comparison(); } } @@ -405,7 +388,12 @@ unsafe extern "C" fn next_comparison( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().switch_to_next_comparison(); + state + .global_timer + .timer + .write() + .unwrap() + .switch_to_next_comparison(); } } @@ -417,12 +405,17 @@ unsafe extern "C" fn toggle_timing_method( ) { if pressed { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().toggle_timing_method(); + state + .global_timer + .timer + .write() + .unwrap() + .toggle_timing_method(); } } unsafe extern "C" fn create(settings: *mut obs_data_t, source: *mut obs_source_t) -> *mut c_void { - let data = Box::into_raw(Box::new(State::new(parse_settings(settings, None)))).cast(); + let data = Box::into_raw(Box::new(State::new(parse_settings(settings)))).cast(); obs_hotkey_register_source( source, @@ -518,7 +511,7 @@ unsafe extern "C" fn get_height(data: *mut c_void) -> u32 { unsafe extern "C" fn video_render(data: *mut c_void, _: *mut gs_effect_t) { let state: &mut State = &mut *data.cast(); - state.update(); + state.render(); let effect = obs_get_base_effect(OBS_EFFECT_PREMULTIPLIED_ALPHA); let tech = gs_effect_get_technique(effect, cstr!("Draw")); @@ -559,50 +552,30 @@ unsafe extern "C" fn save_splits( save_splits_file(state) } -unsafe extern "C" fn game_path_clicked( +unsafe extern "C" fn start_game_clicked( _props: *mut obs_properties_t, _prop: *mut obs_property_t, data: *mut c_void, ) -> bool { let state: &mut State = &mut *data.cast(); - + if state.game_path.exists() { - log::info!("Starting game"); - - let mut process = std::process::Command::new(state.game_path.clone()); + info!("Starting game..."); + + let mut process = Command::new(state.game_path.clone()); process.spawn().ok(); - return false + return false; } - log::info!("No path provided to start a game"); - - false -} - -unsafe extern "C" fn game_path_modified( - data: *mut c_void, - _props: *mut obs_properties_t, - _prop: *mut obs_property_t, - settings: *mut obs_data_t, -) -> bool { - let game_path = CStr::from_ptr(obs_data_get_string(settings, SETTINGS_GAME_PATH).cast()); - let game_path = PathBuf::from(game_path.to_string_lossy().into_owned()); - - let state: &mut State = &mut *data.cast(); + warn!("No path provided to start a game."); - if game_path == state.game_path { - return false; - } - - state.game_path = game_path; - - true + false } unsafe extern "C" fn splits_path_modified( data: *mut c_void, - props: *mut obs_properties_t, + _props: *mut obs_properties_t, _prop: *mut obs_property_t, settings: *mut obs_data_t, ) -> bool { @@ -611,29 +584,22 @@ unsafe extern "C" fn splits_path_modified( let state: &mut State = &mut *data.cast(); - // We only need to do the rest if splits path was changed - if splits_path == state.splits_path { - return false; - } - - let (run, can_save_splits) = parse_run(&splits_path).unwrap_or_else(default_run); - // Store the parsed run and it's related settings for later use by the update function - state.already_parsed_settings = Some((run.clone(), can_save_splits, splits_path.clone())); - - #[cfg(feature = "auto-splitting")] - let info_text = obs_properties_get(props, SETTINGS_AUTO_SPLITTER_INFO); - #[cfg(feature = "auto-splitting")] - let website_button = obs_properties_get(props, SETTINGS_AUTO_SPLITTER_WEBSITE); - #[cfg(feature = "auto-splitting")] - let activate_button = obs_properties_get(props, SETTINGS_AUTO_SPLITTER_ACTIVATE); + handle_splits_path_change(state, splits_path); #[cfg(feature = "auto-splitting")] - update_auto_splitter_ui(info_text, website_button, activate_button, run.game_name().to_string()); - - #[cfg(feature = "auto-splitting")] - auto_splitter_deactivate_ui(activate_button, data); - #[cfg(feature = "auto-splitting")] - auto_splitter_unload(data); + { + let info_text = obs_properties_get(_props, SETTINGS_AUTO_SPLITTER_INFO); + let website_button = obs_properties_get(_props, SETTINGS_AUTO_SPLITTER_WEBSITE); + let activate_button = obs_properties_get(_props, SETTINGS_AUTO_SPLITTER_ACTIVATE); + + update_auto_splitter_ui( + info_text, + website_button, + activate_button, + state.global_timer.timer.read().unwrap().run().game_name(), + ); + auto_splitter_update_activation_label(activate_button, state); + } true } @@ -643,40 +609,48 @@ unsafe fn update_auto_splitter_ui( info_text: *mut obs_property_t, website_button: *mut obs_property_t, activate_button: *mut obs_property_t, - game_name: String + game_name: &str, ) { - match AUTO_SPLITTER_LIST_MANAGER.get_auto_splitter_for_game(game_name) { + match get_auto_splitter_list_manager().get_for_game(game_name) { Some(auto_splitter) => { - match auto_splitter.website { - Some(_) => { obs_property_set_enabled(website_button, true); } - None => { obs_property_set_enabled(website_button, false); } - } + obs_property_set_enabled(website_button, auto_splitter.website.is_some()); - if !AutoSplitterListManager::is_using_auto_splitting_runtime(auto_splitter) { + if !auto_splitters::ListManager::is_using_auto_splitting_runtime(auto_splitter) { obs_property_set_enabled(activate_button, false); - obs_property_set_description(info_text, AUTO_SPLITTER_NOT_COMPATIBLE_TEXT); - } - else { + obs_property_set_description( + info_text, + cstr!("This game's auto splitter is incompatible with LiveSplit One."), + ); + } else { obs_property_set_enabled(activate_button, true); - let mut auto_splitter_description_vec = auto_splitter.description.clone().into_bytes(); - auto_splitter_description_vec.push(0); + let mut auto_splitter_description = auto_splitter.description.as_bytes().to_vec(); + auto_splitter_description.push(0); - obs_property_set_description(info_text, CStr::from_bytes_with_nul(auto_splitter_description_vec.as_bytes()).unwrap().as_ptr()); + obs_property_set_description( + info_text, + auto_splitter_description.as_ptr().cast::(), + ); } - }, + } None => { obs_property_set_enabled(activate_button, false); obs_property_set_enabled(website_button, false); - obs_property_set_description(info_text, AUTO_SPLITTER_NO_AUTO_SPLITTER_TEXT); + obs_property_set_description( + info_text, + cstr!("No auto splitter available for this game."), + ); } } } #[cfg(feature = "auto-splitting")] -unsafe fn auto_splitter_unload(data: *mut c_void) { - let state: &mut State = &mut *data.cast(); - state.auto_splitter.unload_script_blocking().ok(); +fn auto_splitter_unload(state: &mut State) { + state + .global_timer + .auto_splitter + .unload_script_blocking() + .ok(); } #[cfg(feature = "auto-splitting")] @@ -686,100 +660,93 @@ unsafe extern "C" fn auto_splitter_activate_clicked( data: *mut c_void, ) -> bool { let state: &mut State = &mut *data.cast(); - - auto_splitter_toggle_ui(prop, data); - - if state.auto_splitter_is_enabled { - match state.timer.read() { - Ok(timer) => { - let run = timer.clone().into_run(false); - - match &AUTO_SPLITTER_LIST_MANAGER.download_auto_splitter_for_game(String::from(run.game_name())) { - Some(auto_splitter_path) => { - state - .auto_splitter - .load_script_blocking(PathBuf::from(auto_splitter_path.clone())) - .ok(); - } - None => { log::warn!("Couldn't download the auto splitter files") } - } + + state + .global_timer + .auto_splitter_is_enabled + .fetch_xor(true, atomic::Ordering::Relaxed); + + auto_splitter_update_activation_label(prop, state); + + if state + .global_timer + .auto_splitter_is_enabled + .load(atomic::Ordering::Relaxed) + { + match get_auto_splitter_list_manager().download_for_game( + state.global_timer.timer.read().unwrap().run().game_name(), + get_auto_splitters_path(), + ) { + Some(auto_splitter_path) => { + state + .global_timer + .auto_splitter + .load_script_blocking(auto_splitter_path.clone()) + .ok(); + } + None => { + error!("Couldn't download the auto splitter files.") } - Err(e) => { log::warn!("Something went wrong when trying to get the auto splitter's files {e}") } } + } else { + auto_splitter_unload(state); } - else { - auto_splitter_unload(data); - } - - true -} - -#[cfg(feature = "auto-splitting")] -unsafe fn auto_splitter_activate_ui( - activate_button_prop: *mut obs_property_t, - data: *mut c_void, -) { - let state: &mut State = &mut *data.cast(); - - state.auto_splitter_is_enabled = true; - obs_property_set_description(activate_button_prop, AUTO_SPLITTER_BUTTON_DEACTIVATE_TEXT); -} -#[cfg(feature = "auto-splitting")] -unsafe fn auto_splitter_deactivate_ui( - activate_button_prop: *mut obs_property_t, - data: *mut c_void, -) { - let state: &mut State = &mut *data.cast(); - - state.auto_splitter_is_enabled = false; - obs_property_set_description(activate_button_prop, AUTO_SPLITTER_BUTTON_ACTIVATE_TEXT); + true } #[cfg(feature = "auto-splitting")] -unsafe fn auto_splitter_toggle_ui( +unsafe fn auto_splitter_update_activation_label( activate_button_prop: *mut obs_property_t, - data: *mut c_void + state: &mut State, ) { - let state: &mut State = &mut *data.cast(); - match state.auto_splitter_is_enabled { - true => { auto_splitter_deactivate_ui(activate_button_prop, data) } - false => { auto_splitter_activate_ui(activate_button_prop, data) } - } + let is_active = state + .global_timer + .auto_splitter_is_enabled + .load(atomic::Ordering::Relaxed); + + obs_property_set_description( + activate_button_prop, + if !is_active { + cstr!("Activate") + } else { + cstr!("Deactivate") + }, + ); } #[cfg(feature = "auto-splitting")] -unsafe extern "C" fn auto_splitter_website( +unsafe extern "C" fn auto_splitter_open_website( _props: *mut obs_properties_t, _prop: *mut obs_property_t, data: *mut c_void, ) -> bool { let state: &mut State = &mut *data.cast(); - - match state.timer.read() { - Ok(timer) => { - let run = timer.clone().into_run(false); - - match &AUTO_SPLITTER_LIST_MANAGER.get_auto_splitter_website_for_game(String::from(run.game_name())) { - Some(website) => { - log::info!("Opening auto splitter website: {website}"); - match open::that(website) { - Ok(_) => { } - Err(e) => { log::warn!("Could not open website {e}") } - }; + + let website = get_auto_splitter_list_manager() + .get_website_for_game(state.global_timer.timer.read().unwrap().run().game_name()); + + match website { + Some(website) => { + info!("Opening auto splitter website: {website}"); + match open::that(website) { + Ok(_) => {} + Err(e) => { + error!("Could not open website {e}.") } - None => { log::warn!("This auto splitter does not have a website") } - } + }; + } + None => { + warn!("This auto splitter does not have a website.") } - Err(e) => { log::warn!("Something went wrong when trying to get the auto splitter website {e}") } } - + false } unsafe extern "C" fn media_get_state(data: *mut c_void) -> obs_media_state { let state: &mut State = &mut *data.cast(); - let phase = state.timer.read().unwrap().current_phase(); + let phase = state.global_timer.timer.read().unwrap().current_phase(); match phase { TimerPhase::NotRunning => OBS_MEDIA_STATE_STOPPED, TimerPhase::Running => OBS_MEDIA_STATE_PLAYING, @@ -790,7 +757,7 @@ unsafe extern "C" fn media_get_state(data: *mut c_void) -> obs_media_state { unsafe extern "C" fn media_play_pause(data: *mut c_void, pause: bool) { let state: &mut State = &mut *data.cast(); - let mut timer = state.timer.write().unwrap(); + let mut timer = state.global_timer.timer.write().unwrap(); match timer.current_phase() { TimerPhase::NotRunning => { if !pause { @@ -816,14 +783,14 @@ unsafe extern "C" fn media_restart(data: *mut c_void) { if state.auto_save { save_splits_file(state); } - let mut timer = state.timer.write().unwrap(); + let mut timer = state.global_timer.timer.write().unwrap(); timer.reset(true); timer.start(); } unsafe extern "C" fn media_stop(data: *mut c_void) { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().reset(true); + state.global_timer.timer.write().unwrap().reset(true); if state.auto_save { save_splits_file(state); } @@ -831,17 +798,17 @@ unsafe extern "C" fn media_stop(data: *mut c_void) { unsafe extern "C" fn media_next(data: *mut c_void) { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().split(); + state.global_timer.timer.write().unwrap().split(); } unsafe extern "C" fn media_previous(data: *mut c_void) { let state: &mut State = &mut *data.cast(); - state.timer.write().unwrap().undo_split(); + state.global_timer.timer.write().unwrap().undo_split(); } unsafe extern "C" fn media_get_time(data: *mut c_void) -> i64 { let state: &mut State = &mut *data.cast(); - let timer = state.timer.read().unwrap(); + let timer = state.global_timer.timer.read().unwrap(); let time = timer.snapshot().current_time()[timer.current_timing_method()].unwrap_or_default(); let (secs, nanos) = time.to_seconds_and_subsec_nanoseconds(); secs * 1000 + (nanos / 1_000_000) as i64 @@ -849,7 +816,7 @@ unsafe extern "C" fn media_get_time(data: *mut c_void) -> i64 { unsafe extern "C" fn media_get_duration(data: *mut c_void) -> i64 { let state: &mut State = &mut *data.cast(); - let timer = state.timer.read().unwrap(); + let timer = state.global_timer.timer.read().unwrap(); let time = timer .run() .segments() @@ -867,45 +834,22 @@ const SETTINGS_GAME_PATH: *const c_char = cstr!("game_path"); const SETTINGS_START_GAME: *const c_char = cstr!("start_game"); const SETTINGS_SPLITS_PATH: *const c_char = cstr!("splits_path"); const SETTINGS_AUTO_SAVE: *const c_char = cstr!("auto_save"); +#[cfg(feature = "auto-splitting")] const SETTINGS_AUTO_SPLITTER_INFO: *const c_char = cstr!("auto_splitter_info"); +#[cfg(feature = "auto-splitting")] const SETTINGS_AUTO_SPLITTER_ACTIVATE: *const c_char = cstr!("auto_splitter_activate"); +#[cfg(feature = "auto-splitting")] const SETTINGS_AUTO_SPLITTER_WEBSITE: *const c_char = cstr!("auto_splitter_website"); const SETTINGS_LAYOUT_PATH: *const c_char = cstr!("layout_path"); const SETTINGS_SAVE_SPLITS: *const c_char = cstr!("save_splits"); -#[cfg(feature = "auto-splitting")] -const AUTO_SPLITTER_NO_SPLITS_TEXT: *const c_char = cstr!("No splits loaded"); -#[cfg(feature = "auto-splitting")] -const AUTO_SPLITTER_NO_AUTO_SPLITTER_TEXT: *const c_char = cstr!("No auto splitter available for this game"); -#[cfg(feature = "auto-splitting")] -const AUTO_SPLITTER_NOT_COMPATIBLE_TEXT: *const c_char = cstr!("This game's auto splitter is incompatible for LiveSplit One"); - -#[cfg(feature = "auto-splitting")] -const AUTO_SPLITTER_BUTTON_ACTIVATE_TEXT: *const c_char = cstr!("Activate"); -#[cfg(feature = "auto-splitting")] -const AUTO_SPLITTER_BUTTON_DEACTIVATE_TEXT: *const c_char = cstr!("Deactivate"); - unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t { - log::info!("we are getting the properties!"); - - let state: &mut State = &mut *data.cast(); + debug!("We are getting the properties."); + let props = obs_properties_create(); obs_properties_add_int(props, SETTINGS_WIDTH, cstr!("Width"), 10, 8200, 10); obs_properties_add_int(props, SETTINGS_HEIGHT, cstr!("Height"), 10, 8200, 10); - let game_path = obs_properties_add_path( - props, - SETTINGS_GAME_PATH, - cstr!("Game path"), - OBS_PATH_FILE, - cstr!("Executable files (*)"), - ptr::null(), - ); - obs_properties_add_button( - props, - SETTINGS_START_GAME, - cstr!("Start game"), - Some(game_path_clicked) - ); + let splits_path = obs_properties_add_path( props, SETTINGS_SPLITS_PATH, @@ -914,36 +858,18 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t cstr!("LiveSplit Splits (*.lss)"), ptr::null(), ); - - #[cfg(feature = "auto-splitting")] - let info_text = obs_properties_add_text( - props, - SETTINGS_AUTO_SPLITTER_INFO, - AUTO_SPLITTER_NO_SPLITS_TEXT, - OBS_TEXT_INFO, - ); - - #[cfg(feature = "auto-splitting")] - let activate_button_text = match state.auto_splitter_is_enabled { - true => { AUTO_SPLITTER_BUTTON_DEACTIVATE_TEXT } - false => { AUTO_SPLITTER_BUTTON_ACTIVATE_TEXT } - }; - - #[cfg(feature = "auto-splitting")] - let activate_button = obs_properties_add_button( + obs_properties_add_bool( props, - SETTINGS_AUTO_SPLITTER_ACTIVATE, - activate_button_text, - Some(auto_splitter_activate_clicked), + SETTINGS_AUTO_SAVE, + cstr!("Automatically save splits file on reset"), ); - #[cfg(feature = "auto-splitting")] - let website_button = obs_properties_add_button( + obs_properties_add_button( props, - SETTINGS_AUTO_SPLITTER_WEBSITE, - cstr!("Website"), - Some(auto_splitter_website), + SETTINGS_SAVE_SPLITS, + cstr!("Save Splits"), + Some(save_splits), ); - + obs_properties_add_path( props, SETTINGS_LAYOUT_PATH, @@ -952,28 +878,66 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t cstr!("LiveSplit Layouts (*.lsl *.ls1l)"), ptr::null(), ); - obs_properties_add_bool( + + obs_properties_add_path( props, - SETTINGS_AUTO_SAVE, - cstr!("Automatically save splits file on reset"), + SETTINGS_GAME_PATH, + cstr!("Game path"), + OBS_PATH_FILE, + cstr!("Executable files (*)"), + ptr::null(), ); obs_properties_add_button( props, - SETTINGS_SAVE_SPLITS, - cstr!("Save Splits"), - Some(save_splits), + SETTINGS_START_GAME, + cstr!("Start game"), + Some(start_game_clicked), ); - obs_property_set_modified_callback2(game_path, Some(game_path_modified), data); - obs_property_set_modified_callback2(splits_path, Some(splits_path_modified), data); - let run = match state.timer.read() { - Ok(timer) => { timer.clone().into_run(false) } - Err(_) => { default_run().0 } - }; + obs_property_set_modified_callback2(splits_path, Some(splits_path_modified), data); #[cfg(feature = "auto-splitting")] - update_auto_splitter_ui(info_text, website_button, activate_button, run.game_name().to_string()); - + { + let state: &mut State = &mut *data.cast(); + + let info_text = obs_properties_add_text( + props, + SETTINGS_AUTO_SPLITTER_INFO, + cstr!("No splits loaded"), + OBS_TEXT_INFO, + ); + + let activate_button_text = match state + .global_timer + .auto_splitter_is_enabled + .load(atomic::Ordering::Relaxed) + { + true => cstr!("Deactivate"), + false => cstr!("Activate"), + }; + + let activate_button = obs_properties_add_button( + props, + SETTINGS_AUTO_SPLITTER_ACTIVATE, + activate_button_text, + Some(auto_splitter_activate_clicked), + ); + + let website_button = obs_properties_add_button( + props, + SETTINGS_AUTO_SPLITTER_WEBSITE, + cstr!("Website"), + Some(auto_splitter_open_website), + ); + + update_auto_splitter_ui( + info_text, + website_button, + activate_button, + state.global_timer.timer.read().unwrap().run().game_name(), + ); + } + props } @@ -990,44 +954,14 @@ fn default_run() -> (Run, bool) { } unsafe extern "C" fn update(data: *mut c_void, settings: *mut obs_data_t) { - log::info!("Reloading settings."); + debug!("Reloading settings."); let state: &mut State = &mut *data.cast(); - let settings = parse_settings(settings, state.already_parsed_settings.to_owned()); - - // We are done using the previously computed settings, we can reset them back to None - state.already_parsed_settings = None; - - let timer = { - let mut timers = TIMERS.lock().unwrap(); - timers.retain(|(_, timer)| timer.strong_count() > 0); - if let Some(timer) = timers.iter().find_map(|(path, timer)| { - if path == &settings.splits_path { - timer.upgrade() - } else { - None - } - }) { - log::debug!("Found timer to reuse."); - timer - } else { - log::debug!("Storing timer for reuse."); - let timer = Timer::new(settings.run).unwrap().into_shared(); - timers.push((settings.splits_path.clone(), Arc::downgrade(&timer))); - timer - } - }; + let settings = parse_settings(settings); + + handle_splits_path_change(state, settings.splits_path); - #[cfg(feature = "auto-splitting")] - if state.splits_path != settings.splits_path { - state.auto_splitter_is_enabled = false; - auto_splitter_unload(data); - } - - state.splits_path = settings.splits_path; - state.can_save_splits = settings.can_save_splits; state.auto_save = settings.auto_save; - state.timer = timer; state.layout = settings.layout; if state.width != settings.width || state.height != settings.height { @@ -1049,6 +983,43 @@ unsafe extern "C" fn update(data: *mut c_void, settings: *mut obs_data_t) { } } +fn handle_splits_path_change(state: &mut State, splits_path: PathBuf) { + state.global_timer = get_global_timer(splits_path); +} + +fn get_global_timer(splits_path: PathBuf) -> Arc { + let mut timers = TIMERS.lock().unwrap(); + timers.retain(|timer| timer.strong_count() > 0); + if let Some(timer) = timers.iter().find_map(|timer| { + let timer = timer.upgrade()?; + if timer.path == splits_path { + Some(timer) + } else { + None + } + }) { + debug!("Found timer to reuse."); + timer + } else { + debug!("Storing timer for reuse."); + let (run, can_save_splits) = parse_run(&splits_path).unwrap_or_else(default_run); + let timer = Timer::new(run).unwrap().into_shared(); + #[cfg(feature = "auto-splitting")] + let auto_splitter = auto_splitting::Runtime::new(timer.clone()); + let global_timer = Arc::new(GlobalTimer { + path: splits_path, + can_save_splits, + timer, + #[cfg(feature = "auto-splitting")] + auto_splitter, + #[cfg(feature = "auto-splitting")] + auto_splitter_is_enabled: AtomicBool::new(false), + }); + timers.push(Arc::downgrade(&global_timer)); + global_timer + } +} + struct ObsLog; impl Log for ObsLog { @@ -1130,51 +1101,69 @@ pub extern "C" fn obs_module_load() -> bool { unsafe { obs_register_source_s(source_info, mem::size_of_val(source_info) as _); } - - match OBS_MODULE_CONFIG_PATH.exists() { - true => { log::info!("Module config directory already exists") } - false => { - log::info!("{}", match fs::create_dir_all(&*OBS_MODULE_CONFIG_PATH) { - Ok(_) => { String::from("Created module config directory") } - Err(e) => { format!("Couldn't create / access the module config directory: {}", e) } - }); - } - } - #[cfg(feature = "auto-splitting")] - match AUTO_SPLITTERS_PATH.exists() { - true => { log::info!("Auto splitter files directory already exists") } - false => { - log::info!("{}", match fs::create_dir_all(&*AUTO_SPLITTERS_PATH) { - Ok(_) => { String::from("Created auto splitter files config directory") } - Err(e) => { format!("Couldn't create / access the auto splitter files directory: {}", e) } - }); + let module_config_path = get_module_config_path(); + if module_config_path.exists() { + info!("Module config directory already exists."); + } else { + match fs::create_dir_all(module_config_path) { + Ok(_) => { + info!("Created module config directory."); + } + Err(e) => { + info!("Couldn't create / access the module config directory: {e}"); + } } } #[cfg(feature = "auto-splitting")] - match &AUTO_SPLITTER_LIST_MANAGER.is_ok() { - Ok(_) => { - log::info!("{}", match AUTO_SPLITTER_LIST_MANAGER.save_auto_splitter_list_to_disk() { - true => { "Auto-splitter list loaded" } - false => { "Auto-splitter list loaded but it couldn't be written to disk" } - }); - } - Err(e) => { - let from_github_error_string = match &e.0 { - GetAutoSplitterListFromGithubError::NetError(e) => { e.to_string() } - GetAutoSplitterListFromGithubError::DeserializationError(e) => { e.to_string() } - }; + { + let auto_splitters_path = get_auto_splitters_path(); - let from_file_error_string = match &e.1 { - GetAutoSplitterListFromFileError::IoError(e) => { e.to_string() } - GetAutoSplitterListFromFileError::DeserializationError(e) => { e.to_string() } - }; + if auto_splitters_path.exists() { + info!("Auto splitter files directory already exists."); + } else { + match fs::create_dir_all(auto_splitters_path) { + Ok(_) => { + info!("Created auto splitter files config directory."); + } + Err(e) => { + error!("Couldn't create / access the auto splitter files directory: {e}"); + } + } + } - log::warn!("Something went wrong when downloading the list of auto-splitters: {}", from_github_error_string); - log::warn!("Something went wrong when loading the list of auto-splitters from disk: {}", from_file_error_string); + let auto_splitter_list_manager = get_auto_splitter_list_manager(); + match auto_splitter_list_manager.get_result() { + Ok(_) => { + if auto_splitter_list_manager.save_list_to_disk(auto_splitters_path) { + info!("Auto splitter list loaded."); + } else { + error!("Auto splitter list loaded but it couldn't be written to disk."); + } + } + Err(e) => { + let from_github_error_string = match &e.0 { + GetListFromGithubError::NetError(e) => e.to_string(), + GetListFromGithubError::DeserializationError(e) => e.to_string(), + }; + + let from_file_error_string = match &e.1 { + GetListFromFileError::IoError(e) => e.to_string(), + GetListFromFileError::DeserializationError(e) => e.to_string(), + }; + + error!( + "Something went wrong when downloading the list of auto splitters: {}", + from_github_error_string + ); + error!( + "Something went wrong when loading the list of auto splitters from disk: {}", + from_file_error_string + ); + } } - }; + } true }