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