From 82e2f8e271c80eb6ef9453dded76de24e7332b6f Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Sat, 29 Jun 2024 01:15:12 +0200 Subject: [PATCH] Update to the latest version of `livesplit-core` (#55) This brings the new event system, which fixes the auto saving to consistently occur and also logs when the plugin crashes. --- .github/workflows/rust.yml | 3 +- Cargo.lock | 51 +++--- Cargo.toml | 4 +- README.md | 3 +- src/ffi.rs | 1 - src/lib.rs | 355 +++++++++++++++++++++++++------------ 6 files changed, 271 insertions(+), 146 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cb5663c..3fbcb25 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -151,7 +151,8 @@ jobs: tar -xzf cross-x86_64-unknown-linux-gnu.tar.gz - name: Build Shared Library - run: sh .github/workflows/build_shared.sh + shell: bash + run: .github/workflows/build_shared.sh env: TARGET: ${{ matrix.target }} SKIP_CROSS: ${{ matrix.cross }} diff --git a/Cargo.lock b/Cargo.lock index 3abf06d..d861f37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64-simd" version = "0.8.0" @@ -1183,7 +1189,7 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "livesplit-auto-splitting" version = "0.1.0" -source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" +source = "git+https://github.com/LiveSplit/livesplit-core#bb8cb41fe60ce25e5147342bb3df2eeb27087524" dependencies = [ "anyhow", "arc-swap", @@ -1205,7 +1211,7 @@ dependencies = [ [[package]] name = "livesplit-core" version = "0.13.0" -source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" +source = "git+https://github.com/LiveSplit/livesplit-core#bb8cb41fe60ce25e5147342bb3df2eeb27087524" dependencies = [ "base64-simd", "bytemuck", @@ -1234,14 +1240,13 @@ dependencies = [ "tiny-skia", "tiny-skia-path", "tokio", - "unicase", "windows-sys 0.52.0", ] [[package]] name = "livesplit-hotkey" version = "0.7.0" -source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" +source = "git+https://github.com/LiveSplit/livesplit-core#bb8cb41fe60ce25e5147342bb3df2eeb27087524" dependencies = [ "bitflags 2.5.0", "cfg-if", @@ -1258,10 +1263,7 @@ dependencies = [ [[package]] name = "livesplit-title-abbreviations" version = "0.3.0" -source = "git+https://github.com/LiveSplit/livesplit-core#a13ea1b3b0f15fb8e4474a755cbb5fbb6cdb1ecc" -dependencies = [ - "unicase", -] +source = "git+https://github.com/LiveSplit/livesplit-core#bb8cb41fe60ce25e5147342bb3df2eeb27087524" [[package]] name = "log" @@ -1646,9 +1648,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" dependencies = [ "memchr", "serde", @@ -1801,11 +1803,11 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.12.1" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e333b1eb9fe677f6893a9efcb0d277a2d3edd83f358a236b657c32301dc6e5f6" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1826,7 +1828,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-native-certs", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -1906,28 +1908,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.1", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64", -] - [[package]] name = "rustls-pemfile" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" dependencies = [ - "base64", + "base64 0.21.7", "rustls-pki-types", ] @@ -2327,9 +2320,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "libc", @@ -3204,9 +3197,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", diff --git a/Cargo.toml b/Cargo.toml index a0421ba..e7fa5a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,11 +23,11 @@ serde = "1.0.188" serde_derive = "1.0.188" serde_json = "1.0.105" -# crates needed for the auto splitter management code to work as expected` +# crates needed for the auto splitter management code to work as expected anyhow = { version = "1.0.75", optional = true } open = { version = "5.0.0", optional = true } percent-encoding = { version = "2.3.0", optional = true } -quick-xml = { version = "0.31.0", features = [ +quick-xml = { version = "0.34.0", features = [ "serialize", "overlapped-lists", ], optional = true } diff --git a/README.md b/README.md index 932a732..0dd02f4 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ Download the latest release for your operating system from the ### Windows -- Extract the `obs-livesplit-one.dll` to `C:\Program Files -(x86)\obs-studio\obs-plugins\64bit` or equivalent install directory. +- Extract the `obs-livesplit-one.dll` to `C:\Program Files\obs-studio\obs-plugins\64bit` or equivalent install directory. ### Linux diff --git a/src/ffi.rs b/src/ffi.rs index abd1fd7..bea94a8 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -169,7 +169,6 @@ extern "C" { module: *mut obs_module_t, file: *const c_char, ) -> *const c_char; - #[cfg(feature = "auto-splitting")] pub fn obs_data_set_bool(data: *mut obs_data_t, name: *const c_char, val: bool); #[cfg(feature = "auto-splitting")] pub fn obs_data_set_string(data: *mut obs_data_t, name: *const c_char, val: *const c_char); diff --git a/src/lib.rs b/src/lib.rs index 8ed81a1..91315fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use std::{ cmp::Ordering, ffi::{c_void, CStr}, fs::{self, File}, + future::Future, io::{BufWriter, Cursor}, mem, os::raw::{c_char, c_int}, @@ -10,8 +11,8 @@ use std::{ process::Command, ptr, sync::{ - atomic::{self, AtomicPtr}, - Arc, Mutex, Weak, + atomic::{self, AtomicBool, AtomicPtr}, + Arc, Mutex, RwLock, RwLockReadGuard, Weak, }, }; @@ -24,16 +25,16 @@ use ffi::{ gs_technique_end, gs_technique_end_pass, gs_texture_create, gs_texture_destroy, gs_texture_set_image, gs_texture_t, obs_data_array_count, obs_data_array_item, obs_data_array_release, obs_data_get_array, obs_data_get_bool, obs_data_get_int, - obs_data_get_json, obs_data_get_string, obs_data_release, 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_mouse_event, - obs_properties_add_bool, obs_properties_add_button, obs_properties_add_editable_list, - obs_properties_add_int, obs_properties_add_path, obs_properties_add_text, - obs_properties_create, obs_properties_get, obs_property_set_modified_callback2, - obs_property_set_visible, obs_property_t, obs_register_source_s, obs_source_info, obs_source_t, - GS_DYNAMIC, GS_RGBA, LOG_WARNING, OBS_EDITABLE_LIST_TYPE_STRINGS, - OBS_EFFECT_PREMULTIPLIED_ALPHA, OBS_ICON_TYPE_GAME_CAPTURE, OBS_PATH_FILE, - OBS_SOURCE_CONTROLLABLE_MEDIA, OBS_SOURCE_CUSTOM_DRAW, OBS_SOURCE_INTERACTION, + obs_data_get_json, obs_data_get_string, obs_data_release, obs_data_set_bool, + 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_mouse_event, obs_properties_add_bool, obs_properties_add_button, + obs_properties_add_editable_list, obs_properties_add_int, obs_properties_add_path, + obs_properties_add_text, obs_properties_create, obs_properties_get, + obs_property_set_modified_callback2, obs_property_set_visible, obs_property_t, + obs_register_source_s, obs_source_info, obs_source_t, GS_DYNAMIC, GS_RGBA, LOG_WARNING, + OBS_EDITABLE_LIST_TYPE_STRINGS, 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, }; use ffi_types::{ @@ -43,6 +44,7 @@ use ffi_types::{ }; use livesplit_core::{ + event::{CommandSink, Event, Result, TimerQuery}, layout::{self, LayoutSettings, LayoutState}, rendering::software::Renderer, run::{ @@ -50,28 +52,26 @@ use livesplit_core::{ saver::livesplit::{save_timer, IoWrite}, }, settings::ImageCache, - Layout, Run, Segment, SharedTimer, Timer, TimerPhase, + Layout, Run, Segment, TimeSpan, Timer, TimerPhase, TimingMethod, }; -use log::{debug, info, warn, Level, LevelFilter, Log, Metadata, Record}; +use log::{debug, error, info, warn, Level, LevelFilter, Log, Metadata, Record}; use serde_derive::Deserialize; use serde_json::from_str; #[cfg(feature = "auto-splitting")] use { self::ffi::{ - obs_data_erase, obs_data_set_bool, obs_data_set_default_string, obs_data_set_string, - obs_properties_add_group, obs_properties_add_list, obs_property_list_add_string, - obs_property_set_description, obs_property_set_enabled, obs_property_set_long_description, - obs_source_update_properties, OBS_COMBO_FORMAT_STRING, OBS_COMBO_TYPE_LIST, - OBS_GROUP_NORMAL, OBS_TEXT_INFO, + obs_data_erase, obs_data_set_default_string, obs_data_set_string, obs_properties_add_group, + obs_properties_add_list, obs_property_list_add_string, obs_property_set_description, + obs_property_set_enabled, obs_property_set_long_description, obs_source_update_properties, + OBS_COMBO_FORMAT_STRING, OBS_COMBO_TYPE_LIST, OBS_GROUP_NORMAL, OBS_TEXT_INFO, }, livesplit_core::auto_splitting::{ self, settings::{self, FileFilter, Value, Widget, WidgetKind}, wasi_path, }, - log::error, - std::{ffi::CString, sync::atomic::AtomicBool}, + std::ffi::CString, }; macro_rules! cstr { @@ -103,15 +103,169 @@ unsafe impl Sync for UnsafeMultiThread {} unsafe impl Send for UnsafeMultiThread {} struct GlobalTimer { - path: PathBuf, - can_save_splits: bool, - timer: SharedTimer, + timer: Arc, #[cfg(feature = "auto-splitting")] - auto_splitter: auto_splitting::Runtime, + auto_splitter: auto_splitting::Runtime>, #[cfg(feature = "auto-splitting")] auto_splitter_is_enabled: AtomicBool, } +struct InnerTimer { + path: PathBuf, + can_save_splits: bool, + timer: RwLock, + auto_save: AtomicBool, +} + +impl InnerTimer { + fn save(&self) { + if self.can_save_splits { + if let Ok(file) = File::create(&self.path) { + let _ = save_timer(&self.get_timer(), IoWrite(BufWriter::new(file))); + info!("Saved splits to `{}`.", self.path.display()); + } + } + } +} + +impl CommandSink for InnerTimer { + fn start(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().start(); + async move { result } + } + + fn split(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().split(); + async move { result } + } + + fn split_or_start(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().split_or_start(); + async move { result } + } + + fn reset(&self, save_attempt: Option) -> impl Future + 'static { + let result = self + .timer + .write() + .unwrap() + .reset(save_attempt.unwrap_or(true)); + + if result.is_ok() && self.auto_save.load(atomic::Ordering::Relaxed) { + self.save(); + } + + async move { result } + } + + fn undo_split(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().undo_split(); + async move { result } + } + + fn skip_split(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().skip_split(); + async move { result } + } + + fn toggle_pause_or_start(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().toggle_pause_or_start(); + async move { result } + } + + fn pause(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().pause(); + async move { result } + } + + fn resume(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().resume(); + async move { result } + } + + fn undo_all_pauses(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().undo_all_pauses(); + async move { result } + } + + fn switch_to_previous_comparison(&self) -> impl Future + 'static { + self.timer.write().unwrap().switch_to_previous_comparison(); + async { Ok(Event::ComparisonChanged) } + } + + fn switch_to_next_comparison(&self) -> impl Future + 'static { + self.timer.write().unwrap().switch_to_next_comparison(); + async { Ok(Event::ComparisonChanged) } + } + + fn toggle_timing_method(&self) -> impl Future + 'static { + self.timer.write().unwrap().toggle_timing_method(); + async move { Ok(Event::TimingMethodChanged) } + } + + fn set_game_time(&self, time: TimeSpan) -> impl Future + 'static { + let result = self.timer.write().unwrap().set_game_time(time); + async move { result } + } + + fn pause_game_time(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().pause_game_time(); + async move { result } + } + + fn resume_game_time(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().resume_game_time(); + async move { result } + } + + fn set_custom_variable( + &self, + name: &str, + value: &str, + ) -> impl Future + 'static { + self.timer.write().unwrap().set_custom_variable(name, value); + async { Ok(Event::CustomVariableSet) } + } + + fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + let result = self + .timer + .write() + .unwrap() + .set_current_comparison(comparison); + async move { result } + } + + fn set_current_timing_method( + &self, + method: TimingMethod, + ) -> impl Future + 'static { + self.timer + .write() + .unwrap() + .set_current_timing_method(method); + async { Ok(Event::TimingMethodChanged) } + } + + fn initialize_game_time(&self) -> impl Future + 'static { + let result = self.timer.write().unwrap().initialize_game_time(); + async move { result } + } + + fn set_loading_times(&self, time: TimeSpan) -> impl Future + 'static { + let result = self.timer.write().unwrap().set_loading_times(time); + async move { result } + } +} + +impl TimerQuery for InnerTimer { + type Guard<'a> = RwLockReadGuard<'a, Timer>; + + fn get_timer(&self) -> Self::Guard<'_> { + self.timer.read().unwrap() + } +} + static TIMERS: Mutex>> = Mutex::new(Vec::new()); struct State { @@ -123,7 +277,6 @@ struct State { game_environment_vars: Vec<(String, String)>, game_path: PathBuf, global_timer: Arc, - auto_save: bool, layout: Layout, state: LayoutState, image_cache: ImageCache, @@ -132,13 +285,12 @@ struct State { width: u32, height: u32, activated: bool, + obs_settings: *mut obs_data_t, #[cfg(feature = "auto-splitting")] auto_splitter_widgets: Arc>, #[cfg(feature = "auto-splitting")] auto_splitter_map: settings::Map, #[cfg(feature = "auto-splitting")] - obs_settings: *mut obs_data_t, - #[cfg(feature = "auto-splitting")] source: *mut obs_source_t, } @@ -212,16 +364,6 @@ fn parse_layout(path: &CStr) -> Option { layout::parser::parse(&file_data).ok() } -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 get_game_environment_vars(settings: *mut obs_data_t) -> Vec<(String, String)> { let environment_list = obs_data_get_array(settings, SETTINGS_GAME_ENVIRONMENT_LIST); let count = obs_data_array_count(environment_list); @@ -335,11 +477,15 @@ impl State { height, }: Settings, _source: *mut obs_source_t, - #[cfg(feature = "auto-splitting")] obs_settings: *mut obs_data_t, + obs_settings: *mut obs_data_t, ) -> Self { debug!("Loading settings."); let global_timer = get_global_timer(splits_path); + global_timer + .timer + .auto_save + .store(auto_save, atomic::Ordering::Relaxed); let state = LayoutState::default(); let renderer = Renderer::new(); @@ -362,7 +508,6 @@ impl State { game_environment_vars, game_path, global_timer, - auto_save, layout, state, image_cache: ImageCache::new(), @@ -371,13 +516,12 @@ impl State { width, height, activated: false, + obs_settings, #[cfg(feature = "auto-splitting")] auto_splitter_widgets: Arc::default(), #[cfg(feature = "auto-splitting")] auto_splitter_map: settings::Map::new(), #[cfg(feature = "auto-splitting")] - obs_settings, - #[cfg(feature = "auto-splitting")] source: _source, } } @@ -386,7 +530,7 @@ impl State { self.layout.update_state( &mut self.state, &mut self.image_cache, - &self.global_timer.timer.read().unwrap().snapshot(), + &self.global_timer.timer.get_timer().snapshot(), ); self.renderer @@ -446,7 +590,7 @@ unsafe extern "C" fn split( return; } - state.global_timer.timer.write().unwrap().split_or_start(); + drop(state.global_timer.timer.split_or_start()); } unsafe extern "C" fn reset( @@ -464,11 +608,7 @@ unsafe extern "C" fn reset( return; } - state.global_timer.timer.write().unwrap().reset(true); - - if state.auto_save { - save_splits_file(state); - } + drop(state.global_timer.timer.reset(None)); } unsafe extern "C" fn undo( @@ -486,7 +626,7 @@ unsafe extern "C" fn undo( return; } - state.global_timer.timer.write().unwrap().undo_split(); + drop(state.global_timer.timer.undo_split()); } unsafe extern "C" fn skip( @@ -504,7 +644,7 @@ unsafe extern "C" fn skip( return; } - state.global_timer.timer.write().unwrap().skip_split(); + drop(state.global_timer.timer.skip_split()); } unsafe extern "C" fn pause( @@ -522,12 +662,7 @@ unsafe extern "C" fn pause( return; } - state - .global_timer - .timer - .write() - .unwrap() - .toggle_pause_or_start(); + drop(state.global_timer.timer.toggle_pause_or_start()); } unsafe extern "C" fn undo_all_pauses( @@ -545,7 +680,7 @@ unsafe extern "C" fn undo_all_pauses( return; } - state.global_timer.timer.write().unwrap().undo_all_pauses(); + drop(state.global_timer.timer.undo_all_pauses()); } unsafe extern "C" fn previous_comparison( @@ -563,12 +698,7 @@ unsafe extern "C" fn previous_comparison( return; } - state - .global_timer - .timer - .write() - .unwrap() - .switch_to_previous_comparison(); + drop(state.global_timer.timer.switch_to_previous_comparison()); } unsafe extern "C" fn next_comparison( @@ -586,12 +716,7 @@ unsafe extern "C" fn next_comparison( return; } - state - .global_timer - .timer - .write() - .unwrap() - .switch_to_next_comparison(); + drop(state.global_timer.timer.switch_to_next_comparison()); } unsafe extern "C" fn toggle_timing_method( @@ -609,19 +734,13 @@ unsafe extern "C" fn toggle_timing_method( return; } - state - .global_timer - .timer - .write() - .unwrap() - .toggle_timing_method(); + drop(state.global_timer.timer.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(Mutex::new(State::new( parse_settings(settings), source, - #[cfg(feature = "auto-splitting")] settings, )))) .cast(); @@ -755,7 +874,8 @@ unsafe extern "C" fn save_splits( data: *mut c_void, ) -> bool { let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - save_splits_file(state) + state.global_timer.timer.save(); + false } unsafe extern "C" fn use_game_arguments_modified( @@ -901,7 +1021,7 @@ unsafe extern "C" fn use_local_auto_splitter_modified( auto_splitter_info, auto_splitter_website, auto_splitter_activate, - state.global_timer.timer.read().unwrap().run().game_name(), + state.global_timer.timer.get_timer().run().game_name(), ); true @@ -930,7 +1050,7 @@ unsafe extern "C" fn splits_path_modified( info_text, website_button, activate_button, - state.global_timer.timer.read().unwrap().run().game_name(), + state.global_timer.timer.get_timer().run().game_name(), ); auto_splitter_update_activation_label(activate_button, state); } @@ -1023,7 +1143,7 @@ unsafe extern "C" fn auto_splitter_activate_clicked( { if let Some(auto_splitter_path) = auto_splitters::get_downloader().download_for_game( auto_splitters::get_list(), - state.global_timer.timer.read().unwrap().run().game_name(), + state.global_timer.timer.get_timer().run().game_name(), auto_splitters::get_path(), ) { auto_splitter_load(&state.global_timer, auto_splitter_path); @@ -1066,7 +1186,7 @@ unsafe extern "C" fn auto_splitter_open_website( let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); let website = auto_splitters::get_list() - .get_website_for_game(state.global_timer.timer.read().unwrap().run().game_name()); + .get_website_for_game(state.global_timer.timer.get_timer().run().game_name()); match website { Some(website) => { @@ -1088,7 +1208,7 @@ unsafe extern "C" fn auto_splitter_open_website( unsafe extern "C" fn media_get_state(data: *mut c_void) -> obs_media_state { let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - let phase = state.global_timer.timer.read().unwrap().current_phase(); + let phase = state.global_timer.timer.get_timer().current_phase(); match phase { TimerPhase::NotRunning => OBS_MEDIA_STATE_STOPPED, TimerPhase::Running => OBS_MEDIA_STATE_PLAYING, @@ -1099,22 +1219,22 @@ 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::>()).lock().unwrap(); - let mut timer = state.global_timer.timer.write().unwrap(); - match timer.current_phase() { + let phase = state.global_timer.timer.get_timer().current_phase(); + match phase { TimerPhase::NotRunning => { if !pause { - timer.start() + drop(state.global_timer.timer.start()); } } TimerPhase::Running => { if pause { - timer.pause() + drop(state.global_timer.timer.pause()); } } TimerPhase::Ended => {} TimerPhase::Paused => { if !pause { - timer.resume() + drop(state.global_timer.timer.resume()); } } } @@ -1122,35 +1242,28 @@ unsafe extern "C" fn media_play_pause(data: *mut c_void, pause: bool) { unsafe extern "C" fn media_restart(data: *mut c_void) { let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - if state.auto_save { - save_splits_file(state); - } - let mut timer = state.global_timer.timer.write().unwrap(); - timer.reset(true); - timer.start(); + drop(state.global_timer.timer.reset(None)); + drop(state.global_timer.timer.start()); } unsafe extern "C" fn media_stop(data: *mut c_void) { let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - state.global_timer.timer.write().unwrap().reset(true); - if state.auto_save { - save_splits_file(state); - } + drop(state.global_timer.timer.reset(None)); } unsafe extern "C" fn media_next(data: *mut c_void) { let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - state.global_timer.timer.write().unwrap().split(); + drop(state.global_timer.timer.split()); } unsafe extern "C" fn media_previous(data: *mut c_void) { let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - state.global_timer.timer.write().unwrap().undo_split(); + drop(state.global_timer.timer.undo_split()); } unsafe extern "C" fn media_get_time(data: *mut c_void) -> i64 { let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - let timer = state.global_timer.timer.read().unwrap(); + let timer = state.global_timer.timer.get_timer(); 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 @@ -1158,7 +1271,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::>()).lock().unwrap(); - let timer = state.global_timer.timer.read().unwrap(); + let timer = state.global_timer.timer.get_timer(); let time = timer .run() .segments() @@ -1194,6 +1307,8 @@ const SETTINGS_LAYOUT_PATH: *const c_char = cstr!(c"layout_path"); const SETTINGS_SAVE_SPLITS: *const c_char = cstr!(c"save_splits"); unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t { + let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); + let props = obs_properties_create(); obs_properties_add_int(props, SETTINGS_WIDTH, cstr!(c"Width"), 10, 8200, 10); obs_properties_add_int(props, SETTINGS_HEIGHT, cstr!(c"Height"), 10, 8200, 10); @@ -1206,6 +1321,15 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t cstr!(c"LiveSplit Splits (*.lss)"), ptr::null(), ); + obs_data_set_bool( + state.obs_settings, + SETTINGS_AUTO_SAVE, + state + .global_timer + .timer + .auto_save + .load(atomic::Ordering::Relaxed), + ); obs_properties_add_bool( props, SETTINGS_AUTO_SAVE, @@ -1270,8 +1394,6 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t ptr::null(), ); - let state: &mut State = &mut (*data.cast::>()).lock().unwrap(); - let uses_game_arguments = state.use_game_arguments; obs_property_set_visible(game_arguments, uses_game_arguments); obs_property_set_visible(game_working_directory, uses_game_arguments); @@ -1343,7 +1465,7 @@ unsafe extern "C" fn get_properties(data: *mut c_void) -> *mut obs_properties_t info_text, website_button, activate_button, - state.global_timer.timer.read().unwrap().run().game_name(), + state.global_timer.timer.get_timer().run().game_name(), ); let uses_local_auto_splitter = state.local_auto_splitter.is_some(); @@ -1585,7 +1707,11 @@ unsafe extern "C" fn update(data: *mut c_void, settings_obj: *mut obs_data_t) { state.game_path = settings.game_path; - state.auto_save = settings.auto_save; + state + .global_timer + .timer + .auto_save + .store(settings.auto_save, atomic::Ordering::Relaxed); state.layout = settings.layout; #[cfg(feature = "auto-splitting")] @@ -1692,7 +1818,7 @@ fn get_global_timer(splits_path: PathBuf) -> Arc { 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 { + if timer.timer.path == splits_path { Some(timer) } else { None @@ -1703,13 +1829,16 @@ fn get_global_timer(splits_path: PathBuf) -> Arc { } 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(); + let timer = Timer::new(run).unwrap(); #[cfg(feature = "auto-splitting")] let auto_splitter = auto_splitting::Runtime::new(); let global_timer = Arc::new(GlobalTimer { - path: splits_path, - can_save_splits, - timer, + timer: Arc::new(InnerTimer { + timer: RwLock::new(timer), + auto_save: AtomicBool::new(false), + path: splits_path, + can_save_splits, + }), #[cfg(feature = "auto-splitting")] auto_splitter, #[cfg(feature = "auto-splitting")] @@ -1796,6 +1925,10 @@ pub extern "C" fn obs_module_load() -> bool { let _ = log::set_logger(&ObsLog); log::set_max_level(LevelFilter::Debug); + std::panic::set_hook(Box::new(|info| { + error!("obs-livesplit-one crashed, please report this:\n{info}"); + })); + let source_info: &obs_source_info = &SOURCE_INFO.0; unsafe {