From 74e26c9fd65095478099ec5770ae21ab6d5d573b Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Tue, 28 May 2024 19:03:54 +0200 Subject: [PATCH] Implement an Event System This adds an event system. The events usually come from the hotkey system, an auto splitter, the UI, or through a network connection. The UI usually provides the implementation for the so called event sink, forwarding all the events to the actual timer. It is able to intercept the events and for example ask the user for confirmation before applying them. Other handling is possible such as automatically saving the splits or notifying a server about changes happening in the run. --- Cargo.toml | 2 +- capi/Cargo.toml | 4 +- capi/src/auto_splitting_runtime.rs | 2 +- capi/src/delta_component.rs | 10 +- capi/src/event_sink.rs | 110 ++++++++ capi/src/hotkey_system.rs | 14 +- capi/src/lib.rs | 3 + capi/src/run.rs | 6 + capi/src/segment.rs | 2 +- capi/src/timer.rs | 20 +- capi/src/web_event_sink.rs | 219 ++++++++++++++++ capi/src/web_rendering.rs | 3 +- .../src/runtime/mod.rs | 6 +- src/auto_splitting/mod.rs | 43 +-- src/component/delta/mod.rs | 6 +- src/event.rs | 245 ++++++++++++++++++ src/hotkey_system.rs | 49 ++-- src/lib.rs | 1 + src/timing/timer/mod.rs | 24 +- 19 files changed, 676 insertions(+), 93 deletions(-) create mode 100644 capi/src/event_sink.rs create mode 100644 capi/src/web_event_sink.rs create mode 100644 src/event.rs diff --git a/Cargo.toml b/Cargo.toml index ddb3ac40..57b92cd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ bytemuck = { version = "1.9.1", default-features = false } bytemuck_derive = { version = "1.4.1", default_features = false } cfg-if = "1.0.0" itoa = { version = "1.0.3", default-features = false } -time = { version = "0.3.3", default-features = false } +time = { version = "0.3.36", default-features = false } hashbrown = "0.14.0" libm = "0.2.1" livesplit-hotkey = { path = "crates/livesplit-hotkey", version = "0.7.0", default-features = false } diff --git a/capi/Cargo.toml b/capi/Cargo.toml index c8e88d1f..4db4de73 100644 --- a/capi/Cargo.toml +++ b/capi/Cargo.toml @@ -21,7 +21,7 @@ web-sys = { version = "0.3.28", optional = true } default = ["image-shrinking"] image-shrinking = ["livesplit-core/image-shrinking"] software-rendering = ["livesplit-core/software-rendering"] -wasm-web = ["livesplit-core/wasm-web"] +wasm-web = ["livesplit-core/wasm-web", "wasm-bindgen", "web-sys"] auto-splitting = ["livesplit-core/auto-splitting"] assume-str-parameters-are-utf8 = [] -web-rendering = ["wasm-web", "livesplit-core/web-rendering", "wasm-bindgen", "web-sys"] +web-rendering = ["wasm-web", "livesplit-core/web-rendering"] diff --git a/capi/src/auto_splitting_runtime.rs b/capi/src/auto_splitting_runtime.rs index 68f66cdf..1717195f 100644 --- a/capi/src/auto_splitting_runtime.rs +++ b/capi/src/auto_splitting_runtime.rs @@ -6,7 +6,7 @@ use crate::shared_timer::OwnedSharedTimer; use std::{os::raw::c_char, path::PathBuf}; #[cfg(feature = "auto-splitting")] -use livesplit_core::auto_splitting::Runtime as AutoSplittingRuntime; +type AutoSplittingRuntime = livesplit_core::auto_splitting::Runtime; #[cfg(not(feature = "auto-splitting"))] use livesplit_core::SharedTimer; diff --git a/capi/src/delta_component.rs b/capi/src/delta_component.rs index 060ede7c..972db436 100644 --- a/capi/src/delta_component.rs +++ b/capi/src/delta_component.rs @@ -1,11 +1,9 @@ -//! The Delta Component is a component that shows the how far ahead or behind -//! the current attempt is compared to the chosen comparison. +//! The Delta Component is a component that shows how far ahead or behind the +//! current attempt is compared to the chosen comparison. use super::{output_vec, Json}; -use crate::component::OwnedComponent; -use crate::key_value_component_state::OwnedKeyValueComponentState; -use livesplit_core::component::delta::Component as DeltaComponent; -use livesplit_core::{GeneralLayoutSettings, Timer}; +use crate::{component::OwnedComponent, key_value_component_state::OwnedKeyValueComponentState}; +use livesplit_core::{component::delta::Component as DeltaComponent, GeneralLayoutSettings, Timer}; /// type pub type OwnedDeltaComponent = Box; diff --git a/capi/src/event_sink.rs b/capi/src/event_sink.rs new file mode 100644 index 00000000..5d99a572 --- /dev/null +++ b/capi/src/event_sink.rs @@ -0,0 +1,110 @@ +//! An event sink accepts events that are meant to be passed to the timer. The +//! events usually come from the hotkey system, an auto splitter, the UI, or +//! through a network connection. The UI usually provides the implementation for +//! this, forwarding all the events to the actual timer. It is able to intercept +//! the events and for example ask the user for confirmation before applying +//! them. Other handling is possible such as automatically saving the splits or +//! notifying a server about changes happening in the run. + +use std::sync::Arc; + +use crate::shared_timer::OwnedSharedTimer; + +/// type +#[derive(Clone)] +pub struct EventSink(pub(crate) Arc); + +/// type +pub type OwnedEventSink = Box; + +/// Creates a new Event Sink. +#[no_mangle] +pub extern "C" fn EventSink_from_timer(timer: OwnedSharedTimer) -> OwnedEventSink { + Box::new(EventSink(Arc::new(*timer))) +} + +/// drop +#[no_mangle] +pub extern "C" fn EventSink_drop(this: OwnedEventSink) { + drop(this); +} + +pub(crate) trait EventSinkAndQuery: + livesplit_core::event::Sink + livesplit_core::event::TimerQuery + Send + Sync + 'static +{ +} + +impl EventSinkAndQuery for T where + T: livesplit_core::event::Sink + livesplit_core::event::TimerQuery + Send + Sync + 'static +{ +} + +impl livesplit_core::event::Sink for EventSink { + fn start(&self) { + self.0.start() + } + + fn split(&self) { + self.0.split() + } + + fn split_or_start(&self) { + self.0.split_or_start() + } + + fn reset(&self, save_attempt: Option) { + self.0.reset(save_attempt) + } + + fn undo_split(&self) { + self.0.undo_split() + } + + fn skip_split(&self) { + self.0.skip_split() + } + + fn toggle_pause_or_start(&self) { + self.0.toggle_pause_or_start() + } + + fn pause(&self) { + self.0.pause() + } + + fn resume(&self) { + self.0.resume() + } + + fn undo_all_pauses(&self) { + self.0.undo_all_pauses() + } + + fn switch_to_previous_comparison(&self) { + self.0.switch_to_previous_comparison() + } + + fn switch_to_next_comparison(&self) { + self.0.switch_to_next_comparison() + } + + fn toggle_timing_method(&self) { + self.0.toggle_timing_method() + } + + fn set_game_time(&self, time: livesplit_core::TimeSpan) { + self.0.set_game_time(time) + } + + fn pause_game_time(&self) { + self.0.pause_game_time() + } + + fn resume_game_time(&self) { + self.0.resume_game_time() + } + + fn set_custom_variable(&self, name: &str, value: &str) { + self.0.set_custom_variable(name, value) + } +} diff --git a/capi/src/hotkey_system.rs b/capi/src/hotkey_system.rs index 6d8d5a50..ea3e01e7 100644 --- a/capi/src/hotkey_system.rs +++ b/capi/src/hotkey_system.rs @@ -6,8 +6,10 @@ use std::{os::raw::c_char, str::FromStr}; -use crate::{hotkey_config::OwnedHotkeyConfig, output_str, shared_timer::OwnedSharedTimer, str}; -use livesplit_core::{hotkey::KeyCode, HotkeySystem}; +use crate::{event_sink::EventSink, hotkey_config::OwnedHotkeyConfig, output_str, str}; +use livesplit_core::hotkey::KeyCode; + +type HotkeySystem = livesplit_core::HotkeySystem; /// type pub type OwnedHotkeySystem = Box; @@ -16,18 +18,18 @@ pub type NullableOwnedHotkeySystem = Option; /// Creates a new Hotkey System for a Timer with the default hotkeys. #[no_mangle] -pub extern "C" fn HotkeySystem_new(shared_timer: OwnedSharedTimer) -> NullableOwnedHotkeySystem { - HotkeySystem::new(*shared_timer).ok().map(Box::new) +pub extern "C" fn HotkeySystem_new(event_sink: &EventSink) -> NullableOwnedHotkeySystem { + HotkeySystem::new(event_sink.clone()).ok().map(Box::new) } /// Creates a new Hotkey System for a Timer with a custom configuration for the /// hotkeys. #[no_mangle] pub extern "C" fn HotkeySystem_with_config( - shared_timer: OwnedSharedTimer, + event_sink: &EventSink, config: OwnedHotkeyConfig, ) -> NullableOwnedHotkeySystem { - HotkeySystem::with_config(*shared_timer, *config) + HotkeySystem::with_config(event_sink.clone(), *config) .ok() .map(Box::new) } diff --git a/capi/src/lib.rs b/capi/src/lib.rs index 92cfa05b..bc106bd2 100644 --- a/capi/src/lib.rs +++ b/capi/src/lib.rs @@ -32,6 +32,7 @@ pub mod current_pace_component; pub mod delta_component; pub mod detailed_timer_component; pub mod detailed_timer_component_state; +pub mod event_sink; pub mod fuzzy_list; pub mod general_layout_settings; pub mod graph_component; @@ -83,6 +84,8 @@ pub mod timer_write_lock; pub mod title_component; pub mod title_component_state; pub mod total_playtime_component; +#[cfg(all(target_family = "wasm", feature = "wasm-web"))] +pub mod web_event_sink; #[cfg(all(target_family = "wasm", feature = "web-rendering"))] pub mod web_rendering; diff --git a/capi/src/run.rs b/capi/src/run.rs index 474ff4a6..030d2ca4 100644 --- a/capi/src/run.rs +++ b/capi/src/run.rs @@ -234,6 +234,12 @@ pub extern "C" fn Run_segment(this: &Run, index: usize) -> &Segment { this.segment(index) } +/// Returns the amount of segments in this Run. +#[no_mangle] +pub extern "C" fn Run_segments_len(this: &Run) -> usize { + this.segments().len() +} + /// Returns the amount attempt history elements are stored in this Run. #[no_mangle] pub extern "C" fn Run_attempt_history_len(this: &Run) -> usize { diff --git a/capi/src/segment.rs b/capi/src/segment.rs index 75165726..3cd78770 100644 --- a/capi/src/segment.rs +++ b/capi/src/segment.rs @@ -11,7 +11,7 @@ pub type OwnedSegment = Box; /// Creates a new Segment with the name given. #[no_mangle] -pub unsafe extern "C" fn Segment_new(name: &c_char) -> OwnedSegment { +pub unsafe extern "C" fn Segment_new(name: *const c_char) -> OwnedSegment { Box::new(Segment::new(str(name))) } diff --git a/capi/src/timer.rs b/capi/src/timer.rs index 1b9739d4..44c05424 100644 --- a/capi/src/timer.rs +++ b/capi/src/timer.rs @@ -1,6 +1,6 @@ //! A Timer provides all the capabilities necessary for doing speedrun attempts. -use super::{output_str, output_time, output_time_span, output_vec}; +use super::{output_str, output_time, output_time_span, output_vec, str}; use crate::{ run::{NullableOwnedRun, OwnedRun}, shared_timer::OwnedSharedTimer, @@ -204,6 +204,12 @@ pub extern "C" fn Timer_set_current_timing_method(this: &mut Timer, method: Timi this.set_current_timing_method(method); } +/// Toggles between the Real Time and Game Time timing methods. +#[no_mangle] +pub extern "C" fn Timer_toggle_timing_method(this: &mut Timer) { + this.toggle_timing_method(); +} + /// Returns the current comparison that is being compared against. This may /// be a custom comparison or one of the Comparison Generators. #[no_mangle] @@ -287,6 +293,18 @@ pub extern "C" fn Timer_set_loading_times(this: &mut Timer, time: &TimeSpan) { this.set_loading_times(*time); } +/// Sets the value of a custom variable with the name specified. If the variable +/// does not exist, a temporary variable gets created that will not be stored in +/// the splits file. +#[no_mangle] +pub unsafe extern "C" fn Timer_set_custom_variable( + this: &mut Timer, + name: *const c_char, + value: *const c_char, +) { + this.set_custom_variable(str(name), str(value)); +} + /// Returns the current Timer Phase. #[no_mangle] pub extern "C" fn Timer_current_phase(this: &Timer) -> TimerPhase { diff --git a/capi/src/web_event_sink.rs b/capi/src/web_event_sink.rs new file mode 100644 index 00000000..94adf56d --- /dev/null +++ b/capi/src/web_event_sink.rs @@ -0,0 +1,219 @@ +//! Provides an event sink specifically for the web. This allows you to provide +//! a JavaScript object that implements the necessary functions to handle the +//! timer events. All of them are optional except for `currentPhase`. + +use core::ptr; +use std::sync::Arc; + +use livesplit_core::{ + event::{Sink, TimerQuery}, + TimeSpan, TimerPhase, +}; +use wasm_bindgen::prelude::*; +use web_sys::js_sys::{Function, Reflect}; + +use crate::event_sink; + +/// An event sink specifically for the web. This allows you to provide a +/// JavaScript object that implements the necessary functions to handle the +/// timer events. All of them are optional except for `currentPhase`. +#[wasm_bindgen] +pub struct WebEventSink { + obj: JsValue, + start: Option, + split: Option, + split_or_start: Option, + reset: Option, + undo_split: Option, + skip_split: Option, + toggle_pause_or_start: Option, + pause: Option, + resume: Option, + undo_all_pauses: Option, + switch_to_previous_comparison: Option, + switch_to_next_comparison: Option, + toggle_timing_method: Option, + set_game_time: Option, + pause_game_time: Option, + resume_game_time: Option, + set_custom_variable: Option, + current_phase: Function, +} + +#[wasm_bindgen] +impl WebEventSink { + /// Creates a new web event sink with the provided JavaScript object. + #[wasm_bindgen(constructor)] + pub fn new(obj: JsValue) -> Self { + Self { + start: get_func(&obj, "start"), + split: get_func(&obj, "split"), + split_or_start: get_func(&obj, "splitOrStart"), + reset: get_func(&obj, "reset"), + undo_split: get_func(&obj, "undoSplit"), + skip_split: get_func(&obj, "skipSplit"), + toggle_pause_or_start: get_func(&obj, "togglePauseOrStart"), + pause: get_func(&obj, "pause"), + resume: get_func(&obj, "resume"), + undo_all_pauses: get_func(&obj, "undoAllPauses"), + switch_to_previous_comparison: get_func(&obj, "switchToPreviousComparison"), + switch_to_next_comparison: get_func(&obj, "switchToNextComparison"), + toggle_timing_method: get_func(&obj, "toggleTimingMethod"), + set_game_time: get_func(&obj, "setGameTime"), + pause_game_time: get_func(&obj, "pauseGameTime"), + resume_game_time: get_func(&obj, "resumeGameTime"), + set_custom_variable: get_func(&obj, "setCustomVariable"), + current_phase: get_func(&obj, "currentPhase").unwrap(), + obj, + } + } + + /// Converts the web event sink into a generic event sink that can be used + /// by the hotkey system and others. + pub fn intoGeneric(self) -> usize { + let owned_event_sink: event_sink::OwnedEventSink = + Box::new(event_sink::EventSink(Arc::new(self))); + Box::into_raw(owned_event_sink) as usize + } +} + +fn get_func(obj: &JsValue, func_name: &str) -> Option { + Reflect::get(obj, &JsValue::from_str(func_name)) + .ok()? + .dyn_into() + .ok() +} + +unsafe impl Send for WebEventSink {} +unsafe impl Sync for WebEventSink {} + +impl Sink for WebEventSink { + fn start(&self) { + if let Some(func) = &self.start { + let _ = func.call0(&self.obj); + } + } + + fn split(&self) { + if let Some(func) = &self.split { + let _ = func.call0(&self.obj); + } + } + + fn split_or_start(&self) { + if let Some(func) = &self.split_or_start { + let _ = func.call0(&self.obj); + } + } + + fn reset(&self, save_attempt: Option) { + if let Some(func) = &self.reset { + let _ = func.call1( + &self.obj, + &match save_attempt { + Some(true) => JsValue::TRUE, + Some(false) => JsValue::FALSE, + None => JsValue::UNDEFINED, + }, + ); + } + } + + fn undo_split(&self) { + if let Some(func) = &self.undo_split { + let _ = func.call0(&self.obj); + } + } + + fn skip_split(&self) { + if let Some(func) = &self.skip_split { + let _ = func.call0(&self.obj); + } + } + + fn toggle_pause_or_start(&self) { + if let Some(func) = &self.toggle_pause_or_start { + let _ = func.call0(&self.obj); + } + } + + fn pause(&self) { + if let Some(func) = &self.pause { + let _ = func.call0(&self.obj); + } + } + + fn resume(&self) { + if let Some(func) = &self.resume { + let _ = func.call0(&self.obj); + } + } + + fn undo_all_pauses(&self) { + if let Some(func) = &self.undo_all_pauses { + let _ = func.call0(&self.obj); + } + } + + fn switch_to_previous_comparison(&self) { + if let Some(func) = &self.switch_to_previous_comparison { + let _ = func.call0(&self.obj); + } + } + + fn switch_to_next_comparison(&self) { + if let Some(func) = &self.switch_to_next_comparison { + let _ = func.call0(&self.obj); + } + } + + fn toggle_timing_method(&self) { + if let Some(func) = &self.toggle_timing_method { + let _ = func.call0(&self.obj); + } + } + + fn set_game_time(&self, time: TimeSpan) { + if let Some(func) = &self.set_game_time { + let _ = func.call1( + &self.obj, + &JsValue::from_f64(ptr::addr_of!(time) as usize as f64), + ); + } + } + + fn pause_game_time(&self) { + if let Some(func) = &self.pause_game_time { + let _ = func.call0(&self.obj); + } + } + + fn resume_game_time(&self) { + if let Some(func) = &self.resume_game_time { + let _ = func.call0(&self.obj); + } + } + + fn set_custom_variable(&self, name: &str, value: &str) { + if let Some(func) = &self.set_custom_variable { + let _ = func.call2( + &self.obj, + &JsValue::from_str(name), + &JsValue::from_str(value), + ); + } + } +} + +impl TimerQuery for WebEventSink { + fn current_phase(&self) -> TimerPhase { + let phase = self.current_phase.call0(&self.obj).unwrap(); + match phase.as_f64().unwrap() as usize { + 0 => TimerPhase::NotRunning, + 1 => TimerPhase::Running, + 2 => TimerPhase::Paused, + 3 => TimerPhase::Ended, + _ => panic!("Unknown TimerPhase"), + } + } +} diff --git a/capi/src/web_rendering.rs b/capi/src/web_rendering.rs index 356bc8fd..684bfb79 100644 --- a/capi/src/web_rendering.rs +++ b/capi/src/web_rendering.rs @@ -1,6 +1,6 @@ //! Provides a renderer for the web that renders into a canvas. The element can //! then be attached anywhere in the DOM with any desired positioning and size. -//! + use livesplit_core::{layout::LayoutState, rendering::web, settings::ImageCache}; use wasm_bindgen::prelude::*; use web_sys::Element; @@ -21,6 +21,7 @@ impl WebRenderer { /// fully loaded before creating the renderer as otherwise information about /// a fallback font is cached instead. #[allow(clippy::new_without_default)] + #[wasm_bindgen(constructor)] pub fn new() -> Self { Self { inner: web::Renderer::new(), diff --git a/crates/livesplit-auto-splitting/src/runtime/mod.rs b/crates/livesplit-auto-splitting/src/runtime/mod.rs index 2f1a0c81..f9466b41 100644 --- a/crates/livesplit-auto-splitting/src/runtime/mod.rs +++ b/crates/livesplit-auto-splitting/src/runtime/mod.rs @@ -79,7 +79,7 @@ slotmap::new_key_type! { struct SettingValueKey; } -pub struct Context { +pub struct Context { processes: SlotMap, settings_maps: SlotMap, settings_lists: SlotMap, @@ -210,7 +210,7 @@ struct SharedData { tick_rate: AtomicU64, } -struct ExclusiveData { +struct ExclusiveData { trapped: bool, store: Store>, update: TypedFunc<(), ()>, @@ -224,7 +224,7 @@ struct ExclusiveData { /// available on the auto splitter are generally thread-safe and don't block. /// This allows other threads to access and modify information such as settings /// without needing to worry that those threads get blocked. -pub struct AutoSplitter { +pub struct AutoSplitter { exclusive_data: Mutex>, engine: Engine, settings_widgets: ArcSwap>, diff --git a/src/auto_splitting/mod.rs b/src/auto_splitting/mod.rs index 8d4e6bc4..04c6f55e 100644 --- a/src/auto_splitting/mod.rs +++ b/src/auto_splitting/mod.rs @@ -542,8 +542,9 @@ //! - There is no threading. use crate::{ + event::{self, TimerQuery}, platform::Arc, - timing::{SharedTimer, TimerPhase}, + timing::TimerPhase, }; pub use livesplit_auto_splitting::{settings, wasi_path}; use livesplit_auto_splitting::{ @@ -582,13 +583,13 @@ pub enum Error { /// An auto splitter runtime that allows using an auto splitter provided as a /// WebAssembly module to control a timer. -pub struct Runtime { +pub struct Runtime { interrupt_receiver: watch::Receiver>, - auto_splitter: watch::Sender>>, + auto_splitter: watch::Sender>>>, runtime: livesplit_auto_splitting::Runtime, } -impl Drop for Runtime { +impl Drop for Runtime { fn drop(&mut self) { if let Some(handle) = &*self.interrupt_receiver.borrow() { handle.interrupt(); @@ -596,13 +597,13 @@ impl Drop for Runtime { } } -impl Default for Runtime { +impl Default for Runtime { fn default() -> Self { Self::new() } } -impl Runtime { +impl Runtime { /// Starts the runtime. Doesn't actually load an auto splitter until /// [`load`][Runtime::load] is called. pub fn new() -> Self { @@ -644,7 +645,7 @@ impl Runtime { } /// Attempts to load a wasm file containing an auto splitter module. - pub fn load(&self, path: PathBuf, timer: SharedTimer) -> Result<(), Error> { + pub fn load(&self, path: PathBuf, timer: T) -> Result<(), Error> { let data = fs::read(path).map_err(|e| Error::ReadFileFailed { source: e })?; let auto_splitter = self @@ -716,11 +717,11 @@ impl Runtime { // This newtype is required because [`SharedTimer`](crate::timing::SharedTimer) // is an Arc>, so we can't implement the trait directly on it. -struct Timer(SharedTimer); +struct Timer(E); -impl AutoSplitTimer for Timer { +impl AutoSplitTimer for Timer { fn state(&self) -> TimerState { - match self.0.read().unwrap().current_phase() { + match self.0.current_phase() { TimerPhase::NotRunning => TimerState::NotRunning, TimerPhase::Running => TimerState::Running, TimerPhase::Paused => TimerState::Paused, @@ -729,39 +730,39 @@ impl AutoSplitTimer for Timer { } fn start(&mut self) { - self.0.write().unwrap().start() + self.0.start() } fn split(&mut self) { - self.0.write().unwrap().split() + self.0.split() } fn skip_split(&mut self) { - self.0.write().unwrap().skip_split() + self.0.skip_split() } fn undo_split(&mut self) { - self.0.write().unwrap().undo_split() + self.0.undo_split() } fn reset(&mut self) { - self.0.write().unwrap().reset(true) + self.0.reset(None) } fn set_game_time(&mut self, time: time::Duration) { - self.0.write().unwrap().set_game_time(time.into()); + self.0.set_game_time(time.into()); } fn pause_game_time(&mut self) { - self.0.write().unwrap().pause_game_time() + self.0.pause_game_time() } fn resume_game_time(&mut self) { - self.0.write().unwrap().resume_game_time() + self.0.resume_game_time() } fn set_variable(&mut self, name: &str, value: &str) { - self.0.write().unwrap().set_custom_variable(name, value) + self.0.set_custom_variable(name, value) } fn log(&mut self, message: fmt::Arguments<'_>) { @@ -769,8 +770,8 @@ impl AutoSplitTimer for Timer { } } -async fn run( - mut auto_splitter: watch::Receiver>>, +async fn run( + mut auto_splitter: watch::Receiver>>>, timeout_sender: watch::Sender>, interrupt_sender: watch::Sender>, ) { diff --git a/src/component/delta/mod.rs b/src/component/delta/mod.rs index 9153bd92..25cc8663 100644 --- a/src/component/delta/mod.rs +++ b/src/component/delta/mod.rs @@ -1,5 +1,5 @@ //! Provides the Delta Component and relevant types for using it. The Delta -//! Component is a component that shows the how far ahead or behind the current +//! Component is a component that shows how far ahead or behind the current //! attempt is compared to the chosen comparison. use super::key_value; @@ -21,8 +21,8 @@ use serde_derive::{Deserialize, Serialize}; #[cfg(test)] mod tests; -/// The Delta Component is a component that shows the how far ahead or behind -/// the current attempt is compared to the chosen comparison. +/// The Delta Component is a component that shows how far ahead or behind the +/// current attempt is compared to the chosen comparison. #[derive(Default, Clone)] pub struct Component { settings: Settings, diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 00000000..e8c44fa4 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,245 @@ +//! The `event` module provides functionality for forwarding events to the +//! timer. The events usually come from the hotkey system, an auto splitter, the +//! UI, or through a network connection. The UI usually provides the +//! implementation for this, forwarding all the events to the actual timer. It +//! is able to intercept the events and for example ask the user for +//! confirmation before applying them. Other handling is possible such as +//! automatically saving the splits or notifying a server about changes +//! happening in the run. + +use alloc::sync::Arc; + +use crate::{TimeSpan, TimerPhase}; + +/// An event sink accepts events that are meant to be passed to the timer. The +/// events usually come from the hotkey system, an auto splitter, the UI, or +/// through a network connection. The UI usually provides the implementation for +/// this, forwarding all the events to the actual timer. It is able to intercept +/// the events and for example ask the user for confirmation before applying +/// them. Other handling is possible such as automatically saving the splits or +/// notifying a server about changes happening in the run. +pub trait Sink { + /// Starts the timer if there is no attempt in progress. If that's not the + /// case, nothing happens. + fn start(&self); + /// If an attempt is in progress, stores the current time as the time of the + /// current split. The attempt ends if the last split time is stored. + fn split(&self); + /// Starts a new attempt or stores the current time as the time of the + /// current split. The attempt ends if the last split time is stored. + fn split_or_start(&self); + /// Resets the current attempt if there is one in progress. If the splits + /// are to be updated, all the information of the current attempt is stored + /// in the run's history. Otherwise the current attempt's information is + /// discarded. + fn reset(&self, save_attempt: Option); + /// Removes the split time from the last split if an attempt is in progress + /// and there is a previous split. The Timer Phase also switches to + /// [`Running`](TimerPhase::Running) if it previously was + /// [`Ended`](TimerPhase::Ended). + fn undo_split(&self); + /// Skips the current split if an attempt is in progress and the + /// current split is not the last split. + fn skip_split(&self); + /// Toggles an active attempt between [`Paused`](TimerPhase::Paused) and + /// [`Running`](TimerPhase::Paused) or starts an attempt if there's none in + /// progress. + fn toggle_pause_or_start(&self); + /// Pauses an active attempt that is not paused. + fn pause(&self); + /// Resumes an attempt that is paused. + fn resume(&self); + /// Removes all the pause times from the current time. If the current + /// attempt is paused, it also resumes that attempt. Additionally, if the + /// attempt is finished, the final split time is adjusted to not include the + /// pause times as well. + /// + /// # Warning + /// + /// This behavior is not entirely optimal, as generally only the final split + /// time is modified, while all other split times are left unmodified, which + /// may not be what actually happened during the run. + fn undo_all_pauses(&self); + /// Switches the current comparison to the previous comparison in the list. + fn switch_to_previous_comparison(&self); + /// Switches the current comparison to the next comparison in the list. + fn switch_to_next_comparison(&self); + /// Toggles between the `Real Time` and `Game Time` timing methods. + fn toggle_timing_method(&self); + /// Sets the game time to the time specified. This also works if the game + /// time is paused, which can be used as a way of updating the game timer + /// periodically without it automatically moving forward. This ensures that + /// the game timer never shows any time that is not coming from the game. + fn set_game_time(&self, time: TimeSpan); + /// Pauses the game timer such that it doesn't automatically increment + /// similar to real time. + fn pause_game_time(&self); + /// Resumes the game timer such that it automatically increments similar to + /// real time, starting from the game time it was paused at. + fn resume_game_time(&self); + /// Sets the value of a custom variable with the name specified. If the + /// variable does not exist, a temporary variable gets created that will not + /// be stored in the splits file. + fn set_custom_variable(&self, name: &str, value: &str); +} + +/// This trait provides functionality for querying the current state of the +/// timer. +pub trait TimerQuery { + /// Returns the current Timer Phase. + fn current_phase(&self) -> TimerPhase; +} + +#[cfg(feature = "std")] +impl Sink for crate::SharedTimer { + fn start(&self) { + self.write().unwrap().start(); + } + + fn split(&self) { + self.write().unwrap().split(); + } + + fn split_or_start(&self) { + self.write().unwrap().split_or_start(); + } + + fn reset(&self, save_attempt: Option) { + self.write().unwrap().reset(save_attempt != Some(false)); + } + + fn undo_split(&self) { + self.write().unwrap().undo_split(); + } + + fn skip_split(&self) { + self.write().unwrap().skip_split(); + } + + fn toggle_pause_or_start(&self) { + self.write().unwrap().toggle_pause_or_start(); + } + + fn pause(&self) { + self.write().unwrap().pause(); + } + + fn resume(&self) { + self.write().unwrap().resume(); + } + + fn undo_all_pauses(&self) { + self.write().unwrap().undo_all_pauses(); + } + + fn switch_to_previous_comparison(&self) { + self.write().unwrap().switch_to_previous_comparison(); + } + + fn switch_to_next_comparison(&self) { + self.write().unwrap().switch_to_next_comparison(); + } + + fn toggle_timing_method(&self) { + self.write().unwrap().toggle_timing_method(); + } + + fn set_game_time(&self, time: TimeSpan) { + self.write().unwrap().set_game_time(time); + } + + fn pause_game_time(&self) { + self.write().unwrap().pause_game_time(); + } + + fn resume_game_time(&self) { + self.write().unwrap().resume_game_time(); + } + + fn set_custom_variable(&self, name: &str, value: &str) { + self.write().unwrap().set_custom_variable(name, value); + } +} + +#[cfg(feature = "std")] +impl TimerQuery for crate::SharedTimer { + fn current_phase(&self) -> TimerPhase { + self.read().unwrap().current_phase() + } +} + +impl Sink for Arc { + fn start(&self) { + Sink::start(&**self) + } + + fn split(&self) { + Sink::split(&**self) + } + + fn split_or_start(&self) { + Sink::split_or_start(&**self) + } + + fn reset(&self, save_attempt: Option) { + Sink::reset(&**self, save_attempt) + } + + fn undo_split(&self) { + Sink::undo_split(&**self) + } + + fn skip_split(&self) { + Sink::skip_split(&**self) + } + + fn toggle_pause_or_start(&self) { + Sink::toggle_pause_or_start(&**self) + } + + fn pause(&self) { + Sink::pause(&**self) + } + + fn resume(&self) { + Sink::resume(&**self) + } + + fn undo_all_pauses(&self) { + Sink::undo_all_pauses(&**self) + } + + fn switch_to_previous_comparison(&self) { + Sink::switch_to_previous_comparison(&**self) + } + + fn switch_to_next_comparison(&self) { + Sink::switch_to_next_comparison(&**self) + } + + fn toggle_timing_method(&self) { + Sink::toggle_timing_method(&**self) + } + + fn set_game_time(&self, time: TimeSpan) { + Sink::set_game_time(&**self, time) + } + + fn pause_game_time(&self) { + Sink::pause_game_time(&**self) + } + + fn resume_game_time(&self) { + Sink::resume_game_time(&**self) + } + + fn set_custom_variable(&self, name: &str, value: &str) { + Sink::set_custom_variable(&**self, name, value) + } +} + +impl TimerQuery for Arc { + fn current_phase(&self) -> TimerPhase { + TimerQuery::current_phase(&**self) + } +} diff --git a/src/hotkey_system.rs b/src/hotkey_system.rs index 9eff448f..0dce390f 100644 --- a/src/hotkey_system.rs +++ b/src/hotkey_system.rs @@ -1,9 +1,9 @@ use alloc::borrow::Cow; -use livesplit_hotkey::{ConsumePreference, KeyCode}; use crate::{ - hotkey::{Hook, Hotkey}, - HotkeyConfig, SharedTimer, + event, + hotkey::{ConsumePreference, Hook, Hotkey, KeyCode}, + HotkeyConfig, }; pub use crate::hotkey::Result; @@ -61,23 +61,22 @@ impl Action { } } - fn callback(self, timer: SharedTimer) -> Box { + fn callback( + self, + event_sink: E, + ) -> Box { match self { - Action::Split => Box::new(move || timer.write().unwrap().split_or_start()), - Action::Reset => Box::new(move || timer.write().unwrap().reset(true)), - Action::Undo => Box::new(move || timer.write().unwrap().undo_split()), - Action::Skip => Box::new(move || timer.write().unwrap().skip_split()), - Action::Pause => Box::new(move || timer.write().unwrap().toggle_pause_or_start()), - Action::UndoAllPauses => Box::new(move || timer.write().unwrap().undo_all_pauses()), + Action::Split => Box::new(move || event_sink.split_or_start()), + Action::Reset => Box::new(move || event_sink.reset(None)), + Action::Undo => Box::new(move || event_sink.undo_split()), + Action::Skip => Box::new(move || event_sink.skip_split()), + Action::Pause => Box::new(move || event_sink.toggle_pause_or_start()), + Action::UndoAllPauses => Box::new(move || event_sink.undo_all_pauses()), Action::PreviousComparison => { - Box::new(move || timer.write().unwrap().switch_to_previous_comparison()) - } - Action::NextComparison => { - Box::new(move || timer.write().unwrap().switch_to_next_comparison()) - } - Action::ToggleTimingMethod => { - Box::new(move || timer.write().unwrap().toggle_timing_method()) + Box::new(move || event_sink.switch_to_previous_comparison()) } + Action::NextComparison => Box::new(move || event_sink.switch_to_next_comparison()), + Action::ToggleTimingMethod => Box::new(move || event_sink.toggle_timing_method()), } } } @@ -87,26 +86,26 @@ impl Action { /// focus. The behavior of the hotkeys depends on the platform and is stubbed /// out on platforms that don't support hotkeys. You can turn off a `HotkeySystem` /// temporarily. By default the `HotkeySystem` is activated. -pub struct HotkeySystem { +pub struct HotkeySystem { config: HotkeyConfig, hook: Hook, - timer: SharedTimer, + event_sink: E, is_active: bool, } -impl HotkeySystem { +impl HotkeySystem { /// Creates a new Hotkey System for a Timer with the default hotkeys. - pub fn new(timer: SharedTimer) -> Result { - Self::with_config(timer, Default::default()) + pub fn new(event_sink: E) -> Result { + Self::with_config(event_sink, Default::default()) } /// Creates a new Hotkey System for a Timer with a custom configuration for /// the hotkeys. - pub fn with_config(timer: SharedTimer, config: HotkeyConfig) -> Result { + pub fn with_config(event_sink: E, config: HotkeyConfig) -> Result { let mut hotkey_system = Self { config, hook: Hook::with_consume_preference(ConsumePreference::PreferNoConsume)?, - timer, + event_sink, is_active: false, }; hotkey_system.activate()?; @@ -116,7 +115,7 @@ impl HotkeySystem { // This method should never be public, because it might mess up the internal // state and we might leak a registered hotkey fn register_inner(&mut self, action: Action) -> Result<()> { - let inner = self.timer.clone(); + let inner = self.event_sink.clone(); if let Some(hotkey) = action.get_hotkey(&self.config) { self.hook.register(hotkey, action.callback(inner))?; } diff --git a/src/lib.rs b/src/lib.rs index 692a9fe8..ec89ea38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,7 @@ pub mod analysis; pub mod auto_splitting; pub mod comparison; pub mod component; +pub mod event; #[cfg(feature = "std")] mod hotkey_config; #[cfg(feature = "std")] diff --git a/src/timing/timer/mod.rs b/src/timing/timer/mod.rs index 7da2bfe2..3ec5a621 100644 --- a/src/timing/timer/mod.rs +++ b/src/timing/timer/mod.rs @@ -307,8 +307,6 @@ impl Timer { self.time_paused_at = self.run.offset(); self.deinitialize_game_time(); self.run.start_next_run(); - - // FIXME: OnStart } } @@ -339,8 +337,6 @@ impl Timer { self.attempt_ended = Some(AtomicDateTime::now()); } self.run.mark_as_modified(); - - // FIXME: OnSplit } } @@ -364,8 +360,6 @@ impl Timer { self.current_split_index = self.current_split_index.map(|i| i + 1); self.run.mark_as_modified(); - - // FIXME: OnSkipSplit } } @@ -382,8 +376,6 @@ impl Timer { self.current_split_mut().unwrap().clear_split_info(); self.run.mark_as_modified(); - - // FIXME: OnUndoSplit } } @@ -479,8 +471,6 @@ impl Timer { segment.clear_split_info(); } - // FIXME: OnReset - self.run.fix_splits(); self.run.regenerate_comparisons(); } @@ -490,8 +480,6 @@ impl Timer { if self.phase == Running { self.time_paused_at = self.current_time().real_time.unwrap(); self.phase = Paused; - - // FIXME: OnPause } } @@ -500,8 +488,6 @@ impl Timer { if self.phase == Paused { self.adjusted_start_time = TimeStamp::now() - self.time_paused_at; self.phase = Running; - - // FIXME: OnResume } } @@ -514,8 +500,8 @@ impl Timer { } } - /// Toggles an active attempt between `Paused` and `Running` or starts an - /// attempt if there's none in progress. + /// Toggles an active attempt between [`Paused`] and [`Running`] or starts + /// an attempt if there's none in progress. pub fn toggle_pause_or_start(&mut self) { match self.phase { Running => self.pause(), @@ -557,8 +543,6 @@ impl Timer { } self.adjusted_start_time = self.start_time_with_offset; - - // FIXME: OnUndoAllPauses } /// Switches the current comparison to the next comparison in the list. @@ -574,8 +558,6 @@ impl Timer { .nth(index) .unwrap() .populate(&mut self.current_comparison); - - // FIXME: OnNextComparison } /// Switches the current comparison to the previous comparison in the list. @@ -591,8 +573,6 @@ impl Timer { .nth(index) .unwrap() .populate(&mut self.current_comparison); - - // FIXME: OnPreviousComparison } /// Returns the total duration of the current attempt. This is not affected