From 922cc8475499f9c8d48877ff437661cefebfefd5 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Sat, 22 Jun 2024 12:58:14 +0200 Subject: [PATCH] Implement the Server Protocol (#819) This implements a new improved version of the server protocol. The following changes have been made: - The protocol is based on JSON messages. This allows for example for more structured commands where it's easier to provide multiple arguments for a command and even have optional arguments. - For each command, there is a corresponding response. It is either a `success` response with possibly the value that you requested, or an `error` response with an error `code`. - On top of the responses you also get sent `event` messages that indicate changes to the timer. These can either be changes triggered via a command that you sent or by changes that happened through other sources, such as the user directly interacting with the timer or an auto splitter. The protocol is still work in progress and we will evolve it into a protocol that fully allows synchronizing timers over the network. The event sink has now been renamed to command sink, because there is now a clear distinction between incoming commands and events that are the results of these commands. --- benches/balanced_pb.rs | 12 +- benches/layout_state.rs | 4 +- benches/scene_management.rs | 14 +- benches/software_rendering.rs | 14 +- benches/svg_rendering.rs | 14 +- capi/Cargo.toml | 5 +- capi/bind_gen/src/typescript.ts | 93 +++ capi/src/command_sink.rs | 250 ++++++ capi/src/event_sink.rs | 110 --- capi/src/hotkey_system.rs | 12 +- capi/src/lib.rs | 7 +- capi/src/server_protocol.rs | 29 + capi/src/timer.rs | 80 +- capi/src/web_command_sink.rs | 349 ++++++++ capi/src/web_event_sink.rs | 219 ----- src/analysis/pb_chance/tests.rs | 2 +- src/analysis/sum_of_segments/tests.rs | 4 +- src/analysis/tests/semantic_colors.rs | 2 +- src/auto_splitting/mod.rs | 28 +- src/component/detailed_timer/tests.rs | 24 +- src/component/segment_time/tests.rs | 2 +- src/component/splits/mod.rs | 2 +- src/component/splits/tests/column.rs | 86 +- src/component/splits/tests/mod.rs | 16 +- src/component/title/tests.rs | 6 +- src/event.rs | 476 ++++++++--- src/hotkey_system.rs | 58 +- src/lib.rs | 7 +- src/networking/mod.rs | 3 + src/networking/server_protocol.rs | 390 +++++++++ src/timing/mod.rs | 16 +- src/timing/timer/active_attempt.rs | 27 +- src/timing/timer/mod.rs | 238 +++--- src/timing/timer/tests/events.rs | 886 +++++++++++++++++++++ src/timing/timer/tests/mark_as_modified.rs | 52 +- src/timing/timer/tests/mod.rs | 59 +- src/timing/timer/tests/variables.rs | 6 +- src/timing/timer_phase.rs | 2 +- src/util/tests_helper.rs | 29 +- tests/rendering.rs | 14 +- tests/run_files/livesplit1.0.lss | 4 +- 41 files changed, 2845 insertions(+), 806 deletions(-) create mode 100644 capi/src/command_sink.rs delete mode 100644 capi/src/event_sink.rs create mode 100644 capi/src/server_protocol.rs create mode 100644 capi/src/web_command_sink.rs delete mode 100644 capi/src/web_event_sink.rs create mode 100644 src/networking/server_protocol.rs create mode 100644 src/timing/timer/tests/events.rs diff --git a/benches/balanced_pb.rs b/benches/balanced_pb.rs index 1067f98d..8bafac13 100644 --- a/benches/balanced_pb.rs +++ b/benches/balanced_pb.rs @@ -9,16 +9,16 @@ criterion_main!(benches); criterion_group!(benches, fake_splits, actual_splits); fn run_with_splits(timer: &mut Timer, splits: &[f64]) { - timer.start(); - timer.initialize_game_time(); - timer.pause_game_time(); + timer.start().unwrap(); + timer.initialize_game_time().unwrap(); + timer.pause_game_time().unwrap(); for &split in splits { - timer.set_game_time(TimeSpan::from_seconds(split)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(split)).unwrap(); + timer.split().unwrap(); } - timer.reset(true); + timer.reset(true).unwrap(); } fn fake_splits(c: &mut Criterion) { diff --git a/benches/layout_state.rs b/benches/layout_state.rs index d9a5fe69..a7ebc986 100644 --- a/benches/layout_state.rs +++ b/benches/layout_state.rs @@ -18,7 +18,7 @@ fn artificial() -> (Timer, Layout, ImageCache) { run.push_segment(Segment::new("Foo")); let mut timer = Timer::new(run).unwrap(); - timer.start(); + timer.start().unwrap(); (timer, Layout::default_layout(), ImageCache::new()) } @@ -28,7 +28,7 @@ fn real() -> (Timer, Layout, ImageCache) { let run = livesplit::parse(&buf).unwrap(); let mut timer = Timer::new(run).unwrap(); - timer.start(); + timer.start().unwrap(); (timer, Layout::default_layout(), ImageCache::new()) } diff --git a/benches/scene_management.rs b/benches/scene_management.rs index 8cba914f..2f8989e7 100644 --- a/benches/scene_management.rs +++ b/benches/scene_management.rs @@ -146,19 +146,19 @@ cfg_if::cfg_if! { fn start_run(timer: &mut Timer) { timer.set_current_timing_method(TimingMethod::GameTime); - timer.start(); - timer.initialize_game_time(); - timer.pause_game_time(); - timer.set_game_time(TimeSpan::zero()); + timer.start().unwrap(); + timer.initialize_game_time().unwrap(); + timer.pause_game_time().unwrap(); + timer.set_game_time(TimeSpan::zero()).unwrap(); } fn make_progress_run_with_splits_opt(timer: &mut Timer, splits: &[Option]) { for &split in splits { if let Some(split) = split { - timer.set_game_time(TimeSpan::from_seconds(split)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(split)).unwrap(); + timer.split().unwrap(); } else { - timer.skip_split(); + timer.skip_split().unwrap(); } } } diff --git a/benches/software_rendering.rs b/benches/software_rendering.rs index af0c3296..97e72947 100644 --- a/benches/software_rendering.rs +++ b/benches/software_rendering.rs @@ -74,19 +74,19 @@ cfg_if::cfg_if! { fn start_run(timer: &mut Timer) { timer.set_current_timing_method(TimingMethod::GameTime); - timer.start(); - timer.initialize_game_time(); - timer.pause_game_time(); - timer.set_game_time(TimeSpan::zero()); + timer.start().unwrap(); + timer.initialize_game_time().unwrap(); + timer.pause_game_time().unwrap(); + timer.set_game_time(TimeSpan::zero()).unwrap(); } fn make_progress_run_with_splits_opt(timer: &mut Timer, splits: &[Option]) { for &split in splits { if let Some(split) = split { - timer.set_game_time(TimeSpan::from_seconds(split)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(split)).unwrap(); + timer.split().unwrap(); } else { - timer.skip_split(); + timer.skip_split().unwrap(); } } } diff --git a/benches/svg_rendering.rs b/benches/svg_rendering.rs index d6dfbb7c..b3c176e7 100644 --- a/benches/svg_rendering.rs +++ b/benches/svg_rendering.rs @@ -82,19 +82,19 @@ cfg_if::cfg_if! { fn start_run(timer: &mut Timer) { timer.set_current_timing_method(TimingMethod::GameTime); - timer.start(); - timer.initialize_game_time(); - timer.pause_game_time(); - timer.set_game_time(TimeSpan::zero()); + timer.start().unwrap(); + timer.initialize_game_time().unwrap(); + timer.pause_game_time().unwrap(); + timer.set_game_time(TimeSpan::zero()).unwrap(); } fn make_progress_run_with_splits_opt(timer: &mut Timer, splits: &[Option]) { for &split in splits { if let Some(split) = split { - timer.set_game_time(TimeSpan::from_seconds(split)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(split)).unwrap(); + timer.split().unwrap(); } else { - timer.skip_split(); + timer.skip_split().unwrap(); } } } diff --git a/capi/Cargo.toml b/capi/Cargo.toml index 4db4de73..d619310a 100644 --- a/capi/Cargo.toml +++ b/capi/Cargo.toml @@ -2,7 +2,7 @@ name = "livesplit-core-capi" version = "0.11.0" authors = ["Christopher Serr "] -edition = "2018" +edition = "2021" [lib] name = "livesplit_core" @@ -15,13 +15,14 @@ time = { version = "0.3.4", default-features = false, features = ["formatting"] simdutf8 = { git = "https://github.com/CryZe/simdutf8", branch = "wasm-ub-panic", default-features = false } wasm-bindgen = { version = "0.2.78", optional = true } +wasm-bindgen-futures = { version = "0.4.28", optional = true } web-sys = { version = "0.3.28", optional = true } [features] default = ["image-shrinking"] image-shrinking = ["livesplit-core/image-shrinking"] software-rendering = ["livesplit-core/software-rendering"] -wasm-web = ["livesplit-core/wasm-web", "wasm-bindgen", "web-sys"] +wasm-web = ["livesplit-core/wasm-web", "wasm-bindgen", "wasm-bindgen-futures", "web-sys"] auto-splitting = ["livesplit-core/auto-splitting"] assume-str-parameters-are-utf8 = [] web-rendering = ["wasm-web", "livesplit-core/web-rendering"] diff --git a/capi/bind_gen/src/typescript.ts b/capi/bind_gen/src/typescript.ts index 13851478..434cf70d 100644 --- a/capi/bind_gen/src/typescript.ts +++ b/capi/bind_gen/src/typescript.ts @@ -216,6 +216,99 @@ export enum TimerPhase { Paused = 3, } +/** An event informs you about a change in the timer. */ +export enum Event { + /** The timer has been started. */ + Started = 0, + /** + * A split happened. Note that the final split is signaled by `Finished`. + */ + Splitted = 1, + /** + * The final split happened, the run is now finished, but has not been reset + * yet. + */ + Finished = 2, + /** The timer has been reset. */ + Reset = 3, + /** The previous split has been undone. */ + SplitUndone = 4, + /** The current split has been skipped. */ + SplitSkipped = 5, + /** The timer has been paused. */ + Paused = 6, + /** The timer has been resumed. */ + Resumed = 7, + /** All the pauses have been undone. */ + PausesUndone = 8, + /** All the pauses have been undone and the timer has been resumed. */ + PausesUndoneAndResumed = 9, + /** The comparison has been changed. */ + ComparisonChanged = 10, + /** The timing method has been changed. */ + TimingMethodChanged = 11, + /** The game time has been initialized. */ + GameTimeInitialized = 12, + /** The game time has been set. */ + GameTimeSet = 13, + /** The game time has been paused. */ + GameTimePaused = 14, + /** The game time has been resumed. */ + GameTimeResumed = 15, + /** The loading times have been set. */ + LoadingTimesSet = 16, + /** A custom variable has been set. */ + CustomVariableSet = 17, +} + +/** An error that occurred when a command was being processed. */ +export enum CommandError { + /** The operation is not supported. */ + Unsupported = -1, + /** The timer can't be interacted with at the moment. */ + Busy = -2, + /** There is already a run in progress. */ + RunAlreadyInProgress = -3, + /** There is no run in progress. */ + NoRunInProgress = -4, + /** The run is already finished. */ + RunFinished = -5, + /** The time is negative, you can't split yet. */ + NegativeTime = -6, + /** The last split can't be skipped. */ + CantSkipLastSplit = -7, + /** There is no split to undo. */ + CantUndoFirstSplit = -8, + /** The timer is already paused. */ + AlreadyPaused = -9, + /** The timer is not paused. */ + NotPaused = -10, + /** The requested comparison doesn't exist. */ + ComparisonDoesntExist = -11, + /** The game time is already initialized. */ + GameTimeAlreadyInitialized = -12, + /** The game time is already paused. */ + GameTimeAlreadyPaused = -13, + /** The game time is not paused. */ + GameTimeNotPaused = -14, + /** The time could not be parsed. */ + CouldNotParseTime = -15, + /** The timer is currently paused. */ + TimerPaused = -16, + /** The runner decided to not reset the run. */ + RunnerDecidedAgainstReset = -17, +} + +/** The result of a command that was processed. */ +export type CommandResult = Event | CommandError; + +/** + * Checks if the result of a command is a successful event instead of an error. + */ +export function isEvent(result: CommandResult): result is Event { + return result >= 0; +} + /** The state object describes the information to visualize for this component. */ export interface BlankSpaceComponentStateJson { /** The background shown behind the component. */ diff --git a/capi/src/command_sink.rs b/capi/src/command_sink.rs new file mode 100644 index 00000000..225240bc --- /dev/null +++ b/capi/src/command_sink.rs @@ -0,0 +1,250 @@ +//! A command sink accepts commands that are meant to be passed to the timer. +//! The commands 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 commands to the actual timer. It is able to +//! intercept the commands 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. After +//! processing a command, changes to the timer are reported as events. Various +//! error conditions can occur if the command couldn't be processed. + +use std::{future::Future, ops::Deref, pin::Pin, sync::Arc}; + +use livesplit_core::{ + event::{self, Result}, + TimeSpan, Timer, TimingMethod, +}; + +use crate::shared_timer::OwnedSharedTimer; + +/// type +#[derive(Clone)] +pub struct CommandSink(pub(crate) Arc); + +/// type +pub type OwnedCommandSink = Box; + +/// Creates a new Command Sink. +#[no_mangle] +pub extern "C" fn CommandSink_from_timer(timer: OwnedSharedTimer) -> OwnedCommandSink { + Box::new(CommandSink(Arc::new(*timer))) +} + +/// drop +#[no_mangle] +pub extern "C" fn CommandSink_drop(this: OwnedCommandSink) { + drop(this); +} + +pub(crate) trait CommandSinkAndQuery: Send + Sync + 'static { + fn dyn_query<'a>(&'a self) -> Box + 'a>; + fn dyn_start(&self) -> Fut; + fn dyn_split(&self) -> Fut; + fn dyn_split_or_start(&self) -> Fut; + fn dyn_reset(&self, save_attempt: Option) -> Fut; + fn dyn_undo_split(&self) -> Fut; + fn dyn_skip_split(&self) -> Fut; + fn dyn_toggle_pause_or_start(&self) -> Fut; + fn dyn_pause(&self) -> Fut; + fn dyn_resume(&self) -> Fut; + fn dyn_undo_all_pauses(&self) -> Fut; + fn dyn_switch_to_previous_comparison(&self) -> Fut; + fn dyn_switch_to_next_comparison(&self) -> Fut; + fn dyn_set_current_comparison(&self, comparison: &str) -> Fut; + fn dyn_toggle_timing_method(&self) -> Fut; + fn dyn_set_current_timing_method(&self, method: TimingMethod) -> Fut; + fn dyn_initialize_game_time(&self) -> Fut; + fn dyn_set_game_time(&self, time: TimeSpan) -> Fut; + fn dyn_pause_game_time(&self) -> Fut; + fn dyn_resume_game_time(&self) -> Fut; + fn dyn_set_loading_times(&self, time: TimeSpan) -> Fut; + fn dyn_set_custom_variable(&self, name: &str, value: &str) -> Fut; +} + +type Fut = Pin + 'static>>; + +impl CommandSinkAndQuery for T +where + T: event::CommandSink + event::TimerQuery + Send + Sync + 'static, +{ + fn dyn_query<'a>(&'a self) -> Box + 'a> { + Box::new(self.get_timer()) + } + + fn dyn_start(&self) -> Fut { + Box::pin(self.start()) + } + fn dyn_split(&self) -> Fut { + Box::pin(self.split()) + } + fn dyn_split_or_start(&self) -> Fut { + Box::pin(self.split_or_start()) + } + fn dyn_reset(&self, save_attempt: Option) -> Fut { + Box::pin(self.reset(save_attempt)) + } + fn dyn_undo_split(&self) -> Fut { + Box::pin(self.undo_split()) + } + fn dyn_skip_split(&self) -> Fut { + Box::pin(self.skip_split()) + } + fn dyn_toggle_pause_or_start(&self) -> Fut { + Box::pin(self.toggle_pause_or_start()) + } + fn dyn_pause(&self) -> Fut { + Box::pin(self.pause()) + } + fn dyn_resume(&self) -> Fut { + Box::pin(self.resume()) + } + fn dyn_undo_all_pauses(&self) -> Fut { + Box::pin(self.undo_all_pauses()) + } + fn dyn_switch_to_previous_comparison(&self) -> Fut { + Box::pin(self.switch_to_previous_comparison()) + } + fn dyn_switch_to_next_comparison(&self) -> Fut { + Box::pin(self.switch_to_next_comparison()) + } + fn dyn_set_current_comparison(&self, comparison: &str) -> Fut { + Box::pin(self.set_current_comparison(comparison)) + } + fn dyn_toggle_timing_method(&self) -> Fut { + Box::pin(self.toggle_timing_method()) + } + fn dyn_set_current_timing_method(&self, method: TimingMethod) -> Fut { + Box::pin(self.set_current_timing_method(method)) + } + fn dyn_initialize_game_time(&self) -> Fut { + Box::pin(self.initialize_game_time()) + } + fn dyn_set_game_time(&self, time: TimeSpan) -> Fut { + Box::pin(self.set_game_time(time)) + } + fn dyn_pause_game_time(&self) -> Fut { + Box::pin(self.pause_game_time()) + } + fn dyn_resume_game_time(&self) -> Fut { + Box::pin(self.resume_game_time()) + } + fn dyn_set_loading_times(&self, time: TimeSpan) -> Fut { + Box::pin(self.set_loading_times(time)) + } + fn dyn_set_custom_variable(&self, name: &str, value: &str) -> Fut { + Box::pin(self.set_custom_variable(name, value)) + } +} + +impl event::CommandSink for CommandSink { + fn start(&self) -> impl Future + 'static { + self.0.dyn_start() + } + + fn split(&self) -> impl Future + 'static { + self.0.dyn_split() + } + + fn split_or_start(&self) -> impl Future + 'static { + self.0.dyn_split_or_start() + } + + fn reset(&self, save_attempt: Option) -> impl Future + 'static { + self.0.dyn_reset(save_attempt) + } + + fn undo_split(&self) -> impl Future + 'static { + self.0.dyn_undo_split() + } + + fn skip_split(&self) -> impl Future + 'static { + self.0.dyn_skip_split() + } + + fn toggle_pause_or_start(&self) -> impl Future + 'static { + self.0.dyn_toggle_pause_or_start() + } + + fn pause(&self) -> impl Future + 'static { + self.0.dyn_pause() + } + + fn resume(&self) -> impl Future + 'static { + self.0.dyn_resume() + } + + fn undo_all_pauses(&self) -> impl Future + 'static { + self.0.dyn_undo_all_pauses() + } + + fn switch_to_previous_comparison(&self) -> impl Future + 'static { + self.0.dyn_switch_to_previous_comparison() + } + + fn switch_to_next_comparison(&self) -> impl Future + 'static { + self.0.dyn_switch_to_next_comparison() + } + + fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + self.0.dyn_set_current_comparison(comparison) + } + + fn toggle_timing_method(&self) -> impl Future + 'static { + self.0.dyn_toggle_timing_method() + } + + fn set_current_timing_method( + &self, + method: TimingMethod, + ) -> impl Future + 'static { + self.0.dyn_set_current_timing_method(method) + } + + fn initialize_game_time(&self) -> impl Future + 'static { + self.0.dyn_initialize_game_time() + } + + fn set_game_time(&self, time: TimeSpan) -> impl Future + 'static { + self.0.dyn_set_game_time(time) + } + + fn pause_game_time(&self) -> impl Future + 'static { + self.0.dyn_pause_game_time() + } + + fn resume_game_time(&self) -> impl Future + 'static { + self.0.dyn_resume_game_time() + } + + fn set_loading_times(&self, time: TimeSpan) -> impl Future + 'static { + self.0.dyn_set_loading_times(time) + } + + fn set_custom_variable( + &self, + name: &str, + value: &str, + ) -> impl Future + 'static { + self.0.dyn_set_custom_variable(name, value) + } +} + +impl event::TimerQuery for CommandSink { + type Guard<'a> = TimerGuard<'a>; + + fn get_timer(&self) -> Self::Guard<'_> { + TimerGuard(self.0.dyn_query()) + } +} + +/// type +#[repr(transparent)] +pub struct TimerGuard<'a>(Box + 'a>); + +impl std::ops::Deref for TimerGuard<'_> { + type Target = Timer; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/capi/src/event_sink.rs b/capi/src/event_sink.rs deleted file mode 100644 index 5d99a572..00000000 --- a/capi/src/event_sink.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! 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 ea3e01e7..a9ba0c4b 100644 --- a/capi/src/hotkey_system.rs +++ b/capi/src/hotkey_system.rs @@ -6,10 +6,10 @@ use std::{os::raw::c_char, str::FromStr}; -use crate::{event_sink::EventSink, hotkey_config::OwnedHotkeyConfig, output_str, str}; +use crate::{command_sink::CommandSink, hotkey_config::OwnedHotkeyConfig, output_str, str}; use livesplit_core::hotkey::KeyCode; -type HotkeySystem = livesplit_core::HotkeySystem; +type HotkeySystem = livesplit_core::HotkeySystem; /// type pub type OwnedHotkeySystem = Box; @@ -18,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(event_sink: &EventSink) -> NullableOwnedHotkeySystem { - HotkeySystem::new(event_sink.clone()).ok().map(Box::new) +pub extern "C" fn HotkeySystem_new(command_sink: &CommandSink) -> NullableOwnedHotkeySystem { + HotkeySystem::new(command_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( - event_sink: &EventSink, + command_sink: &CommandSink, config: OwnedHotkeyConfig, ) -> NullableOwnedHotkeySystem { - HotkeySystem::with_config(event_sink.clone(), *config) + HotkeySystem::with_config(command_sink.clone(), *config) .ok() .map(Box::new) } diff --git a/capi/src/lib.rs b/capi/src/lib.rs index bc106bd2..96dc24c3 100644 --- a/capi/src/lib.rs +++ b/capi/src/lib.rs @@ -26,13 +26,13 @@ pub mod attempt; pub mod auto_splitting_runtime; pub mod blank_space_component; pub mod blank_space_component_state; +pub mod command_sink; pub mod component; pub mod current_comparison_component; 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; @@ -65,6 +65,8 @@ pub mod segment_history_iter; pub mod segment_time_component; pub mod separator_component; pub mod separator_component_state; +#[cfg(all(target_family = "wasm", feature = "wasm-web"))] +pub mod server_protocol; pub mod setting_value; pub mod shared_timer; pub mod software_renderer; @@ -85,7 +87,7 @@ 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; +pub mod web_command_sink; #[cfg(all(target_family = "wasm", feature = "web-rendering"))] pub mod web_rendering; @@ -99,6 +101,7 @@ use livesplit_core::{Time, TimeSpan}; /// type pub type Json = *const c_char; /// type +#[allow(non_camel_case_types)] pub type Nullablec_char = c_char; thread_local! { diff --git a/capi/src/server_protocol.rs b/capi/src/server_protocol.rs new file mode 100644 index 00000000..f0e457f2 --- /dev/null +++ b/capi/src/server_protocol.rs @@ -0,0 +1,29 @@ +//! The server protocol is an experimental JSON based protocol that is used to +//! remotely control the timer. Every command that you send has a response in +//! the form of a JSON object indicating whether the command was successful or +//! not. + +use livesplit_core::networking::server_protocol; +use wasm_bindgen::prelude::*; + +use crate::command_sink::CommandSink; + +/// The server protocol is an experimental JSON based protocol that is used to +/// remotely control the timer. Every command that you send has a response in +/// the form of a JSON object indicating whether the command was successful or +/// not. +#[wasm_bindgen] +pub struct ServerProtocol {} + +#[wasm_bindgen] +impl ServerProtocol { + /// Handles an incoming command and returns the response to be sent. + pub async unsafe fn handleCommand(command: &str, commandSink: *const CommandSink) -> String { + server_protocol::handle_command(command, &*commandSink).await + } + + /// Encodes an event that happened to be sent. + pub fn encodeEvent(event: u32) -> Option { + Some(server_protocol::encode_event(event.try_into().ok()?)) + } +} diff --git a/capi/src/timer.rs b/capi/src/timer.rs index 4d4ab66a..34850025 100644 --- a/capi/src/timer.rs +++ b/capi/src/timer.rs @@ -6,6 +6,7 @@ use crate::{ shared_timer::OwnedSharedTimer, }; use livesplit_core::{ + event::{Error, Event}, run::saver::{self, livesplit::IoWrite}, Run, Time, TimeSpan, Timer, TimerPhase, TimingMethod, }; @@ -90,40 +91,47 @@ pub extern "C" fn Timer_current_split_index(this: &Timer) -> isize { this.current_split_index().map_or(-1, |i| i as isize) } +fn convert(result: Result) -> i32 { + match result { + Ok(a) => a as i32, + Err(e) => -1 - (e as i32), + } +} + /// Starts the Timer if there is no attempt in progress. If that's not the /// case, nothing happens. #[no_mangle] -pub extern "C" fn Timer_start(this: &mut Timer) { - this.start(); +pub extern "C" fn Timer_start(this: &mut Timer) -> i32 { + convert(this.start()) } /// 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. #[no_mangle] -pub extern "C" fn Timer_split(this: &mut Timer) { - this.split(); +pub extern "C" fn Timer_split(this: &mut Timer) -> i32 { + convert(this.split()) } /// 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. #[no_mangle] -pub extern "C" fn Timer_split_or_start(this: &mut Timer) { - this.split_or_start(); +pub extern "C" fn Timer_split_or_start(this: &mut Timer) -> i32 { + convert(this.split_or_start()) } /// Skips the current split if an attempt is in progress and the /// current split is not the last split. #[no_mangle] -pub extern "C" fn Timer_skip_split(this: &mut Timer) { - this.skip_split(); +pub extern "C" fn Timer_skip_split(this: &mut Timer) -> i32 { + convert(this.skip_split()) } /// 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` if it previously was `Ended`. #[no_mangle] -pub extern "C" fn Timer_undo_split(this: &mut Timer) { - this.undo_split(); +pub extern "C" fn Timer_undo_split(this: &mut Timer) -> i32 { + convert(this.undo_split()) } /// Checks whether the current attempt has new best segment times in any of the @@ -140,41 +148,41 @@ pub extern "C" fn Timer_current_attempt_has_new_best_times(this: &Timer) -> bool /// in the Run's history. Otherwise the current attempt's information is /// discarded. #[no_mangle] -pub extern "C" fn Timer_reset(this: &mut Timer, update_splits: bool) { - this.reset(update_splits); +pub extern "C" fn Timer_reset(this: &mut Timer, update_splits: bool) -> i32 { + convert(this.reset(update_splits)) } /// Resets the current attempt if there is one in progress. The splits are /// updated such that the current attempt's split times are being stored as /// the new Personal Best. #[no_mangle] -pub extern "C" fn Timer_reset_and_set_attempt_as_pb(this: &mut Timer) { - this.reset_and_set_attempt_as_pb(); +pub extern "C" fn Timer_reset_and_set_attempt_as_pb(this: &mut Timer) -> i32 { + convert(this.reset_and_set_attempt_as_pb()) } /// Pauses an active attempt that is not paused. #[no_mangle] -pub extern "C" fn Timer_pause(this: &mut Timer) { - this.pause(); +pub extern "C" fn Timer_pause(this: &mut Timer) -> i32 { + convert(this.pause()) } /// Resumes an attempt that is paused. #[no_mangle] -pub extern "C" fn Timer_resume(this: &mut Timer) { - this.resume(); +pub extern "C" fn Timer_resume(this: &mut Timer) -> i32 { + convert(this.resume()) } /// Toggles an active attempt between `Paused` and `Running`. #[no_mangle] -pub extern "C" fn Timer_toggle_pause(this: &mut Timer) { - this.toggle_pause(); +pub extern "C" fn Timer_toggle_pause(this: &mut Timer) -> i32 { + convert(this.toggle_pause()) } /// Toggles an active attempt between `Paused` and `Running` or starts an /// attempt if there's none in progress. #[no_mangle] -pub extern "C" fn Timer_toggle_pause_or_start(this: &mut Timer) { - this.toggle_pause_or_start(); +pub extern "C" fn Timer_toggle_pause_or_start(this: &mut Timer) -> i32 { + convert(this.toggle_pause_or_start()) } /// Removes all the pause times from the current time. If the current @@ -188,8 +196,8 @@ pub extern "C" fn Timer_toggle_pause_or_start(this: &mut Timer) { /// time is modified, while all other split times are left unmodified, which /// may not be what actually happened during the run. #[no_mangle] -pub extern "C" fn Timer_undo_all_pauses(this: &mut Timer) { - this.undo_all_pauses(); +pub extern "C" fn Timer_undo_all_pauses(this: &mut Timer) -> i32 { + convert(this.undo_all_pauses()) } /// Returns the currently selected Timing Method. @@ -223,8 +231,8 @@ pub extern "C" fn Timer_current_comparison(this: &Timer) -> *const c_char { pub unsafe extern "C" fn Timer_set_current_comparison( this: &mut Timer, comparison: *const c_char, -) -> bool { - this.set_current_comparison(str(comparison)).is_ok() +) -> i32 { + convert(this.set_current_comparison(str(comparison))) } /// Switches the current comparison to the next comparison in the list. @@ -249,8 +257,8 @@ pub extern "C" fn Timer_is_game_time_initialized(this: &Timer) -> bool { /// Initializes Game Time for the current attempt. Game Time automatically /// gets uninitialized for each new attempt. #[no_mangle] -pub extern "C" fn Timer_initialize_game_time(this: &mut Timer) { - this.initialize_game_time(); +pub extern "C" fn Timer_initialize_game_time(this: &mut Timer) -> i32 { + convert(this.initialize_game_time()) } /// Deinitializes Game Time for the current attempt. @@ -269,15 +277,15 @@ pub extern "C" fn Timer_is_game_time_paused(this: &Timer) -> bool { /// Pauses the Game Timer such that it doesn't automatically increment /// similar to Real Time. #[no_mangle] -pub extern "C" fn Timer_pause_game_time(this: &mut Timer) { - this.pause_game_time(); +pub extern "C" fn Timer_pause_game_time(this: &mut Timer) -> i32 { + convert(this.pause_game_time()) } /// Resumes the Game Timer such that it automatically increments similar to /// Real Time, starting from the Game Time it was paused at. #[no_mangle] -pub extern "C" fn Timer_resume_game_time(this: &mut Timer) { - this.resume_game_time(); +pub extern "C" fn Timer_resume_game_time(this: &mut Timer) -> i32 { + convert(this.resume_game_time()) } /// Sets the Game Time to the time specified. This also works if the Game @@ -285,8 +293,8 @@ pub extern "C" fn Timer_resume_game_time(this: &mut Timer) { /// periodically without it automatically moving forward. This ensures that /// the Game Timer never shows any time that is not coming from the game. #[no_mangle] -pub extern "C" fn Timer_set_game_time(this: &mut Timer, time: &TimeSpan) { - this.set_game_time(*time); +pub extern "C" fn Timer_set_game_time(this: &mut Timer, time: &TimeSpan) -> i32 { + convert(this.set_game_time(*time)) } /// Accesses the loading times. Loading times are defined as Game Time - Real Time. @@ -299,8 +307,8 @@ pub extern "C" fn Timer_loading_times(this: &Timer) -> *const TimeSpan { /// just specify the amount of time the game has been loading. The Game Time /// is then automatically determined by Real Time - Loading Times. #[no_mangle] -pub extern "C" fn Timer_set_loading_times(this: &mut Timer, time: &TimeSpan) { - this.set_loading_times(*time); +pub extern "C" fn Timer_set_loading_times(this: &mut Timer, time: &TimeSpan) -> i32 { + convert(this.set_loading_times(*time)) } /// Sets the value of a custom variable with the name specified. If the variable diff --git a/capi/src/web_command_sink.rs b/capi/src/web_command_sink.rs new file mode 100644 index 00000000..96b85731 --- /dev/null +++ b/capi/src/web_command_sink.rs @@ -0,0 +1,349 @@ +//! Provides a command sink specifically for the web. This allows you to provide +//! a JavaScript object that implements the necessary functions to handle the +//! timer commands. All of them are optional except for `getTimer`. + +use core::ptr; +use std::{cell::Cell, convert::TryFrom, future::Future, sync::Arc}; + +use livesplit_core::{ + event::{CommandSink, Error, Event, Result, TimerQuery}, + TimeSpan, Timer, TimingMethod, +}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use web_sys::js_sys::{Function, Promise, Reflect}; + +use crate::command_sink; + +/// A command sink specifically for the web. This allows you to provide a +/// JavaScript object that implements the necessary functions to handle the +/// timer commands. All of them are optional except for `getTimer`. +#[wasm_bindgen] +pub struct WebCommandSink { + 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, + set_current_comparison: Option, + toggle_timing_method: Option, + set_current_timing_method: Option, + initialize_game_time: Option, + set_game_time: Option, + pause_game_time: Option, + resume_game_time: Option, + set_loading_times: Option, + set_custom_variable: Option, + + get_timer: Function, + locked: Cell, +} + +#[wasm_bindgen] +impl WebCommandSink { + /// Creates a new web command 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"), + set_current_comparison: get_func(&obj, "setCurrentComparison"), + toggle_timing_method: get_func(&obj, "toggleTimingMethod"), + set_current_timing_method: get_func(&obj, "setCurrentTimingMethod"), + initialize_game_time: get_func(&obj, "initializeGameTime"), + set_game_time: get_func(&obj, "setGameTime"), + pause_game_time: get_func(&obj, "pauseGameTime"), + resume_game_time: get_func(&obj, "resumeGameTime"), + set_loading_times: get_func(&obj, "setLoadingTimes"), + set_custom_variable: get_func(&obj, "setCustomVariable"), + + get_timer: get_func(&obj, "getTimer").unwrap(), + locked: Cell::new(false), + obj, + } + } + + /// Converts the web command sink into a generic command sink that can be + /// used by the hotkey system and others. + pub fn intoGeneric(self) -> usize { + let owned_command_sink: command_sink::OwnedCommandSink = + Box::new(command_sink::CommandSink(Arc::new(self))); + Box::into_raw(owned_command_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 WebCommandSink {} +unsafe impl Sync for WebCommandSink {} + +async fn handle_action_value(value: Option) -> Result { + if let Some(mut value) = value { + if let Ok(promise) = JsCast::dyn_into::(value.clone()) { + value = match JsFuture::from(promise).await { + Ok(value) | Err(value) => value, + }; + } + if let Some(value) = value.as_f64() { + let value = value as i32; + if value >= 0 { + Ok(Event::try_from(value as u32).map_err(|_| Error::Unknown)?) + } else { + Err(Error::from((-value - 1) as u32)) + } + } else { + Err(Error::Unknown) + } + } else { + Err(Error::Unsupported) + } +} + +impl CommandSink for WebCommandSink { + fn start(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.start.as_ref().and_then(|f| f.call0(&self.obj).ok())) + } + + fn split(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.split.as_ref().and_then(|f| f.call0(&self.obj).ok())) + } + + fn split_or_start(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.split_or_start + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn reset(&self, save_attempt: Option) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.reset.as_ref().and_then(|f| { + f.call1( + &self.obj, + &match save_attempt { + Some(true) => JsValue::TRUE, + Some(false) => JsValue::FALSE, + None => JsValue::UNDEFINED, + }, + ) + .ok() + })) + } + + fn undo_split(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.undo_split + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn skip_split(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.skip_split + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn toggle_pause_or_start(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.toggle_pause_or_start + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn pause(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.pause.as_ref().and_then(|f| f.call0(&self.obj).ok())) + } + + fn resume(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.resume.as_ref().and_then(|f| f.call0(&self.obj).ok())) + } + + fn undo_all_pauses(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.undo_all_pauses + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn switch_to_previous_comparison(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.switch_to_previous_comparison + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn switch_to_next_comparison(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.switch_to_next_comparison + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.set_current_comparison + .as_ref() + .and_then(|f| f.call1(&self.obj, &JsValue::from_str(comparison)).ok()), + ) + } + + fn toggle_timing_method(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.toggle_timing_method + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn set_current_timing_method( + &self, + timing_method: TimingMethod, + ) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.set_current_timing_method.as_ref().and_then(|f| { + f.call1(&self.obj, &JsValue::from_f64(timing_method as usize as f64)) + .ok() + })) + } + + fn initialize_game_time(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.initialize_game_time + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn set_game_time(&self, time: TimeSpan) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.set_game_time.as_ref().and_then(|f| { + f.call1( + &self.obj, + &JsValue::from_f64(ptr::addr_of!(time) as usize as f64), + ) + .ok() + })) + } + + fn pause_game_time(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.pause_game_time + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn resume_game_time(&self) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value( + self.resume_game_time + .as_ref() + .and_then(|f| f.call0(&self.obj).ok()), + ) + } + + fn set_loading_times(&self, time: TimeSpan) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.set_loading_times.as_ref().and_then(|f| { + f.call1( + &self.obj, + &JsValue::from_f64(ptr::addr_of!(time) as usize as f64), + ) + .ok() + })) + } + + fn set_custom_variable( + &self, + name: &str, + value: &str, + ) -> impl Future + 'static { + debug_assert!(!self.locked.get()); + handle_action_value(self.set_custom_variable.as_ref().and_then(|f| { + f.call2( + &self.obj, + &JsValue::from_str(name), + &JsValue::from_str(value), + ) + .ok() + })) + } +} + +/// type +pub struct WebGuard<'a>(&'a Timer, &'a Cell); + +impl Drop for WebGuard<'_> { + fn drop(&mut self) { + self.1.set(false); + } +} + +impl std::ops::Deref for WebGuard<'_> { + type Target = Timer; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl TimerQuery for WebCommandSink { + type Guard<'a> = WebGuard<'a>; + + fn get_timer(&self) -> Self::Guard<'_> { + debug_assert!(!self.locked.replace(true)); + unsafe { + WebGuard( + &*(self.get_timer.call0(&self.obj).unwrap().as_f64().unwrap() as usize + as *const Timer), + &self.locked, + ) + } + } +} diff --git a/capi/src/web_event_sink.rs b/capi/src/web_event_sink.rs deleted file mode 100644 index 94adf56d..00000000 --- a/capi/src/web_event_sink.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! 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/src/analysis/pb_chance/tests.rs b/src/analysis/pb_chance/tests.rs index 69a72346..6847ff25 100644 --- a/src/analysis/pb_chance/tests.rs +++ b/src/analysis/pb_chance/tests.rs @@ -162,7 +162,7 @@ fn is_0_percent_if_we_cant_pb_anymore() { run_with_splits(&mut timer, &[12.0, 20.0]); start_run(&mut timer); make_progress_run_with_splits_opt(&mut timer, &[Some(7.0)]); - timer.set_game_time(span(21.0)); + timer.set_game_time(span(21.0)).unwrap(); // We don't split yet, we are simply losing so much time that we can't PB anymore. assert_eq!(chance(&timer), 0); } diff --git a/src/analysis/sum_of_segments/tests.rs b/src/analysis/sum_of_segments/tests.rs index 118e1562..1d447fac 100644 --- a/src/analysis/sum_of_segments/tests.rs +++ b/src/analysis/sum_of_segments/tests.rs @@ -66,7 +66,7 @@ pub fn sum_of_best() { [(5.0, 0, true), (20.0, 1, true), (60.0, 2, true)], ); - run_with_splits_opt(&mut timer, &[None, Some(10.0), None]); + run_with_splits_opt(&mut timer, &[None, Some(10.0)]); predictions = [None; 4]; best::calculate( timer.run().segments(), @@ -96,7 +96,7 @@ pub fn sum_of_best() { [(5.0, 0, true), (10.0, 0, false), (25.0, 1, true)], ); - run_with_splits_opt(&mut timer, &[Some(7.0), Some(10.0), None]); + run_with_splits_opt(&mut timer, &[Some(7.0), Some(10.0)]); predictions = [None; 4]; best::calculate( timer.run().segments(), diff --git a/src/analysis/tests/semantic_colors.rs b/src/analysis/tests/semantic_colors.rs index 752ec958..43866f60 100644 --- a/src/analysis/tests/semantic_colors.rs +++ b/src/analysis/tests/semantic_colors.rs @@ -22,7 +22,7 @@ fn segment_colors_are_correct() { assert_eq!(color(&timer, 0.0), SemanticColor::Default); - timer.reset(false); + timer.reset(false).unwrap(); start_run(&mut timer); make_progress_run_with_splits_opt(&mut timer, &[Some(15.0)]); diff --git a/src/auto_splitting/mod.rs b/src/auto_splitting/mod.rs index 04c6f55e..5b665196 100644 --- a/src/auto_splitting/mod.rs +++ b/src/auto_splitting/mod.rs @@ -597,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 { @@ -719,9 +719,9 @@ impl Runtime { // is an Arc>, so we can't implement the trait directly on it. struct Timer(E); -impl AutoSplitTimer for Timer { +impl AutoSplitTimer for Timer { fn state(&self) -> TimerState { - match self.0.current_phase() { + match self.0.get_timer().current_phase() { TimerPhase::NotRunning => TimerState::NotRunning, TimerPhase::Running => TimerState::Running, TimerPhase::Paused => TimerState::Paused, @@ -730,39 +730,39 @@ impl AutoSplitTimer for Timer { } fn start(&mut self) { - self.0.start() + drop(self.0.start()); } fn split(&mut self) { - self.0.split() + drop(self.0.split()); } fn skip_split(&mut self) { - self.0.skip_split() + drop(self.0.skip_split()); } fn undo_split(&mut self) { - self.0.undo_split() + drop(self.0.undo_split()); } fn reset(&mut self) { - self.0.reset(None) + drop(self.0.reset(None)); } fn set_game_time(&mut self, time: time::Duration) { - self.0.set_game_time(time.into()); + drop(self.0.set_game_time(time.into())); } fn pause_game_time(&mut self) { - self.0.pause_game_time() + drop(self.0.pause_game_time()); } fn resume_game_time(&mut self) { - self.0.resume_game_time() + drop(self.0.resume_game_time()); } fn set_variable(&mut self, name: &str, value: &str) { - self.0.set_custom_variable(name, value) + drop(self.0.set_custom_variable(name, value)); } fn log(&mut self, message: fmt::Arguments<'_>) { @@ -770,7 +770,7 @@ impl AutoSplitTimer for Timer { } } -async fn run( +async fn run( mut auto_splitter: watch::Receiver>>>, timeout_sender: watch::Sender>, interrupt_sender: watch::Sender>, diff --git a/src/component/detailed_timer/tests.rs b/src/component/detailed_timer/tests.rs index 7c41b743..fe5f5cb0 100644 --- a/src/component/detailed_timer/tests.rs +++ b/src/component/detailed_timer/tests.rs @@ -41,7 +41,7 @@ fn doesnt_show_segment_name_outside_attempt() { fn shows_segment_name_during_attempt() { let (mut timer, component, layout_settings, mut image_cache) = prepare(); - timer.start(); + timer.start().unwrap(); assert_eq!( component @@ -56,8 +56,8 @@ fn shows_segment_name_during_attempt() { fn shows_segment_name_at_the_end_of_an_attempt() { let (mut timer, component, layout_settings, mut image_cache) = prepare(); - timer.start(); - timer.split(); + timer.start().unwrap(); + timer.split().unwrap(); assert_eq!( component @@ -72,9 +72,9 @@ fn shows_segment_name_at_the_end_of_an_attempt() { fn stops_showing_segment_name_when_resetting() { let (mut timer, component, layout_settings, mut image_cache) = prepare(); - timer.start(); - timer.split(); - timer.reset(true); + timer.start().unwrap(); + timer.split().unwrap(); + timer.reset(true).unwrap(); assert_eq!( component @@ -100,7 +100,7 @@ fn shows_icon_during_attempt() { component.state(&mut image_cache, &timer.snapshot(), &layout_settings); - timer.start(); + timer.start().unwrap(); assert!(!component .state(&mut image_cache, &timer.snapshot(), &layout_settings) @@ -114,11 +114,11 @@ fn still_shows_icon_of_last_segment_at_the_end_of_an_attempt() { component.state(&mut image_cache, &timer.snapshot(), &layout_settings); - timer.start(); + timer.start().unwrap(); component.state(&mut image_cache, &timer.snapshot(), &layout_settings); - timer.split(); + timer.split().unwrap(); assert!(!component .state(&mut image_cache, &timer.snapshot(), &layout_settings) @@ -132,15 +132,15 @@ fn stops_showing_icon_when_resetting() { component.state(&mut image_cache, &timer.snapshot(), &layout_settings); - timer.start(); + timer.start().unwrap(); component.state(&mut image_cache, &timer.snapshot(), &layout_settings); - timer.split(); + timer.split().unwrap(); component.state(&mut image_cache, &timer.snapshot(), &layout_settings); - timer.reset(true); + timer.reset(true).unwrap(); assert!(component .state(&mut image_cache, &timer.snapshot(), &layout_settings) diff --git a/src/component/segment_time/tests.rs b/src/component/segment_time/tests.rs index 17d53ea1..bdc260ae 100644 --- a/src/component/segment_time/tests.rs +++ b/src/component/segment_time/tests.rs @@ -34,7 +34,7 @@ fn is_not_empty_when_attempt_is_paused() { let mut timer = create_timer(); start_run(&mut timer); make_progress_run_with_splits_opt(&mut timer, &[Some(467.23)]); - timer.pause(); + timer.pause().unwrap(); let state = component.state(&timer); assert_eq!(&*state.value, "33.30"); } diff --git a/src/component/splits/mod.rs b/src/component/splits/mod.rs index ab9239d2..c6e60ca4 100644 --- a/src/component/splits/mod.rs +++ b/src/component/splits/mod.rs @@ -394,7 +394,7 @@ impl Component { index: 0, }); state.is_current_split = false; - state.index = (usize::max_value() ^ 1) - 2 * i; + state.index = (usize::MAX ^ 1) - 2 * i; } } diff --git a/src/component/splits/tests/column.rs b/src/component/splits/tests/column.rs index 0c362516..a21faae0 100644 --- a/src/component/splits/tests/column.rs +++ b/src/component/splits/tests/column.rs @@ -510,36 +510,36 @@ fn check_columns( let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_state(&state, 0, expected_values); - timer.set_game_time(TimeSpan::from_seconds(8.5)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(8.5)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_state(&state, 1, expected_values); - timer.skip_split(); + timer.skip_split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_state(&state, 2, expected_values); - timer.set_game_time(TimeSpan::from_seconds(10.0)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(10.0)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_state(&state, 3, expected_values); - timer.set_game_time(TimeSpan::from_seconds(17.5)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(17.5)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_state(&state, 4, expected_values); - timer.skip_split(); + timer.skip_split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_state(&state, 5, expected_values); - timer.set_game_time(TimeSpan::from_seconds(25.0)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(25.0)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_state(&state, 6, expected_values); @@ -1106,49 +1106,49 @@ fn check_columns_update_trigger( // Timer at 0, contextual shouldn't show live times yet check_column_state(&state, 0, expected_values); - timer.set_game_time(TimeSpan::from_seconds(2.0)); + timer.set_game_time(TimeSpan::from_seconds(2.0)).unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // Shorter than the best segment, contextual shouldn't show live times yet check_column_state(&state, 1, expected_values); - timer.set_game_time(TimeSpan::from_seconds(3.5)); + timer.set_game_time(TimeSpan::from_seconds(3.5)).unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // Longer than the best segment, contextual should show live times check_column_state(&state, 2, expected_values); - timer.set_game_time(TimeSpan::from_seconds(5.5)); + timer.set_game_time(TimeSpan::from_seconds(5.5)).unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // Behind the personal best, contextual should show live times check_column_state(&state, 3, expected_values); - timer.split(); - timer.skip_split(); - timer.set_game_time(TimeSpan::from_seconds(11.0)); + timer.split().unwrap(); + timer.skip_split().unwrap(); + timer.set_game_time(TimeSpan::from_seconds(11.0)).unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // Shorter than the combined best segment, contextual shouldn't show live // times yet check_column_state(&state, 4, expected_values); - timer.set_game_time(TimeSpan::from_seconds(13.0)); + timer.set_game_time(TimeSpan::from_seconds(13.0)).unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // Longer than the combined best segment, contextual should show live times check_column_state(&state, 5, expected_values); - timer.set_game_time(TimeSpan::from_seconds(18.0)); - timer.split(); - timer.skip_split(); - timer.set_game_time(TimeSpan::from_seconds(29.0)); + timer.set_game_time(TimeSpan::from_seconds(18.0)).unwrap(); + timer.split().unwrap(); + timer.skip_split().unwrap(); + timer.set_game_time(TimeSpan::from_seconds(29.0)).unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // Not behind the personal best, contextual should not show live times yet check_column_state(&state, 6, expected_values); - timer.set_game_time(TimeSpan::from_seconds(31.0)); + timer.set_game_time(TimeSpan::from_seconds(31.0)).unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // Behind the personal best, contextual should show live times only if a @@ -1187,22 +1187,22 @@ fn column_delta_best_segment_colors() { let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); check_column_color(&state, 0, Text); - timer.set_game_time(TimeSpan::from_seconds(5.1)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(5.1)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // 5.1 is longer than the best segment of 5.0, so this isn't a best segment check_column_color(&state, 0, Text); - timer.undo_split(); - timer.set_game_time(TimeSpan::from_seconds(4.9)); - timer.split(); + timer.undo_split().unwrap(); + timer.set_game_time(TimeSpan::from_seconds(4.9)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // 4.9 is shorter than the best segment of 5.0, so this is a best segment check_column_color(&state, 0, Best); - timer.skip_split(); + timer.skip_split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // After skipping a split, the first best segment should stay @@ -1210,8 +1210,8 @@ fn column_delta_best_segment_colors() { // The skipped split is not a best segment check_column_color(&state, 1, Text); - timer.set_game_time(TimeSpan::from_seconds(12.0)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(12.0)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // The first best segment should stay @@ -1222,9 +1222,9 @@ fn column_delta_best_segment_colors() { // 7.0, so this is not a best segment check_column_color(&state, 2, Text); - timer.undo_split(); - timer.set_game_time(TimeSpan::from_seconds(11.8)); - timer.split(); + timer.undo_split().unwrap(); + timer.set_game_time(TimeSpan::from_seconds(11.8)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // The first best segment should stay @@ -1235,23 +1235,23 @@ fn column_delta_best_segment_colors() { // 7.0, so this is a best segment check_column_color(&state, 2, Best); - timer.set_game_time(TimeSpan::from_seconds(21.0)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(21.0)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // The best segment is empty, so the segment of 9.2 is a best segment check_column_color(&state, 3, Best); - timer.set_game_time(TimeSpan::from_seconds(28.9)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(28.9)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // The segment of 7.9 is shorter than the best segment of 8.0, so this is a best segment check_column_color(&state, 4, Best); - timer.undo_split(); - timer.set_game_time(TimeSpan::from_seconds(29.1)); - timer.split(); + timer.undo_split().unwrap(); + timer.set_game_time(TimeSpan::from_seconds(29.1)).unwrap(); + timer.split().unwrap(); let state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); // The segment of 8.1 is longer than the best segment of 8.0, so this is not a best segment @@ -1303,7 +1303,7 @@ fn delta_or_split_time() { [Best, Best, Best, Best, Best, Best], )], ); - timer.reset(true); + timer.reset(true).unwrap(); // We do another run, but this time with the second and second to last split // being skipped. @@ -1320,7 +1320,7 @@ fn delta_or_split_time() { [Best, Text, Best, Best, Text, Best], )], ); - timer.reset(true); + timer.reset(true).unwrap(); // In this third run, we should have split times instead of deltas for the // two skipped splits we had before. The way we set them up, the second to @@ -1345,7 +1345,7 @@ fn delta_or_split_time() { [Best, Text, Best, Best, Best, Best], )], ); - timer.reset(true); + timer.reset(true).unwrap(); } fn check_column_color(state: &State, split_index: usize, expected_color: SemanticColor) { diff --git a/src/component/splits/tests/mod.rs b/src/component/splits/tests/mod.rs index fda0c1e9..ec7f767d 100644 --- a/src/component/splits/tests/mod.rs +++ b/src/component/splits/tests/mod.rs @@ -64,22 +64,22 @@ fn one_visual_split() { assert_eq!(state.splits[0].name, "A"); assert_eq!(state.splits.len(), 1); - timer.start(); + timer.start().unwrap(); state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); assert_eq!(state.splits[0].name, "A"); assert_eq!(state.splits.len(), 1); - timer.split(); + timer.split().unwrap(); state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); assert_eq!(state.splits[0].name, "B"); assert_eq!(state.splits.len(), 1); - timer.split(); + timer.split().unwrap(); state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); assert_eq!(state.splits[0].name, "C"); assert_eq!(state.splits.len(), 1); - timer.split(); + timer.split().unwrap(); state = component.state(&mut image_cache, &timer.snapshot(), &layout_settings); assert_eq!(state.splits[0].name, "C"); assert_eq!(state.splits.len(), 1); @@ -104,13 +104,13 @@ fn negative_segment_times() { ..Default::default() }); - timer.start(); + timer.start().unwrap(); // Emulate a negative offset through game time. timer.set_current_timing_method(TimingMethod::GameTime); - timer.initialize_game_time(); - timer.pause_game_time(); - timer.set_game_time(TimeSpan::from_seconds(-1.0)); + timer.initialize_game_time().unwrap(); + timer.pause_game_time().unwrap(); + timer.set_game_time(TimeSpan::from_seconds(-1.0)).unwrap(); let mut image_cache = ImageCache::new(); diff --git a/src/component/title/tests.rs b/src/component/title/tests.rs index 2594ada8..12caf506 100644 --- a/src/component/title/tests.rs +++ b/src/component/title/tests.rs @@ -21,21 +21,21 @@ fn finished_runs_and_attempt_count() { ); assert_eq!(component.state(&mut image_cache, &timer).attempts, Some(0)); - timer.start(); + timer.start().unwrap(); assert_eq!( component.state(&mut image_cache, &timer).finished_runs, Some(0) ); assert_eq!(component.state(&mut image_cache, &timer).attempts, Some(1)); - timer.split(); + timer.split().unwrap(); assert_eq!( component.state(&mut image_cache, &timer).finished_runs, Some(1) ); assert_eq!(component.state(&mut image_cache, &timer).attempts, Some(1)); - timer.reset(true); + timer.reset(true).unwrap(); assert_eq!( component.state(&mut image_cache, &timer).finished_runs, Some(1) diff --git a/src/event.rs b/src/event.rs index e8c44fa4..4e2db1e2 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,54 +1,226 @@ -//! 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 +//! The event module provides functionality for forwarding commands to the +//! timer. The commands 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 commands to the actual timer. It +//! is able to intercept the commands 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. +//! happening in the run. After processing a command, changes to the timer are +//! reported as [`Event`]s. Various [`Error`] conditions can occur if the +//! command couldn't be processed. + +use core::{future::Future, ops::Deref}; use alloc::sync::Arc; -use crate::{TimeSpan, TimerPhase}; +use crate::{TimeSpan, Timer, TimingMethod}; + +/// An event informs you about a change in the timer. +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, serde_derive::Serialize, serde_derive::Deserialize, +)] +#[non_exhaustive] +pub enum Event { + /// The timer has been started. + Started = 0, + /// A split happened. Note that the final split is signaled by + /// [`Finished`]. + Splitted = 1, + /// The final split happened, the run is now finished, but has not been + /// reset yet. + Finished = 2, + /// The timer has been reset. + Reset = 3, + /// The previous split has been undone. + SplitUndone = 4, + /// The current split has been skipped. + SplitSkipped = 5, + /// The timer has been paused. + Paused = 6, + /// The timer has been resumed. + Resumed = 7, + /// All the pauses have been undone. + PausesUndone = 8, + /// All the pauses have been undone and the timer has been resumed. + PausesUndoneAndResumed = 9, + /// The comparison has been changed. + ComparisonChanged = 10, + /// The timing method has been changed. + TimingMethodChanged = 11, + /// The game time has been initialized. + GameTimeInitialized = 12, + /// The game time has been set. + GameTimeSet = 13, + /// The game time has been paused. + GameTimePaused = 14, + /// The game time has been resumed. + GameTimeResumed = 15, + /// The loading times have been set. + LoadingTimesSet = 16, + /// A custom variable has been set. + CustomVariableSet = 17, +} + +impl TryFrom for Event { + type Error = (); + + fn try_from(value: u32) -> Result { + Ok(match value { + 0 => Event::Started, + 1 => Event::Splitted, + 2 => Event::Finished, + 3 => Event::Reset, + 4 => Event::SplitUndone, + 5 => Event::SplitSkipped, + 6 => Event::Paused, + 7 => Event::Resumed, + 8 => Event::PausesUndone, + 9 => Event::PausesUndoneAndResumed, + 10 => Event::ComparisonChanged, + 11 => Event::TimingMethodChanged, + 12 => Event::GameTimeInitialized, + 13 => Event::GameTimeSet, + 14 => Event::GameTimePaused, + 15 => Event::GameTimeResumed, + 16 => Event::LoadingTimesSet, + 17 => Event::CustomVariableSet, + _ => return Err(()), + }) + } +} + +/// An error that occurred when a command was being processed. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + snafu::Snafu, + serde_derive::Serialize, + serde_derive::Deserialize, +)] +#[snafu(context(suffix(false)))] +#[non_exhaustive] +pub enum Error { + /// The operation is not supported. + Unsupported = 0, + /// The timer can't be interacted with at the moment. + Busy = 1, + /// There is already a run in progress. + RunAlreadyInProgress = 2, + /// There is no run in progress. + NoRunInProgress = 3, + /// The run is already finished. + RunFinished = 4, + /// The time is negative, you can't split yet. + NegativeTime = 5, + /// The last split can't be skipped. + CantSkipLastSplit = 6, + /// There is no split to undo. + CantUndoFirstSplit = 7, + /// The timer is already paused. + AlreadyPaused = 8, + /// The timer is not paused. + NotPaused = 9, + /// The requested comparison doesn't exist. + ComparisonDoesntExist = 10, + /// The game time is already initialized. + GameTimeAlreadyInitialized = 11, + /// The game time is already paused. + GameTimeAlreadyPaused = 12, + /// The game time is not paused. + GameTimeNotPaused = 13, + /// The time could not be parsed. + CouldNotParseTime = 14, + /// The timer is currently paused. + TimerPaused = 15, + /// The runner decided to not reset the run. + RunnerDecidedAgainstReset = 16, + /// An unknown error occurred. + #[serde(other)] + Unknown, +} -/// 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 { +impl From for Error { + fn from(value: u32) -> Self { + match value { + 0 => Error::Unsupported, + 1 => Error::Busy, + 2 => Error::RunAlreadyInProgress, + 3 => Error::NoRunInProgress, + 4 => Error::RunFinished, + 5 => Error::NegativeTime, + 6 => Error::CantSkipLastSplit, + 7 => Error::CantUndoFirstSplit, + 8 => Error::AlreadyPaused, + 9 => Error::NotPaused, + 10 => Error::ComparisonDoesntExist, + 11 => Error::GameTimeAlreadyInitialized, + 12 => Error::GameTimeAlreadyPaused, + 13 => Error::GameTimeNotPaused, + 14 => Error::CouldNotParseTime, + 15 => Error::TimerPaused, + 16 => Error::RunnerDecidedAgainstReset, + _ => Error::Unknown, + } + } +} + +/// The result of a command that was processed. +pub type Result = core::result::Result; + +/// A command sink accepts commands that are meant to be passed to the timer. +/// The commands 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 commands to the actual timer. It is able to +/// intercept the commands 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. After +/// processing a command, changes to the timer are reported as [`Event`]s. +/// Various [`Error`] conditions can occur if the command couldn't be processed. +/// +/// # Asynchronous Events +/// +/// The events or the errors are returned asynchronously. This allows for +/// handling commands that may take some time to complete. However, there are +/// various sources of these commands such as the hotkey system and the auto +/// splitters that do not care about the result of each command. They +/// immediately drop the returned future. This means that the command sink needs +/// to be implemented in a way such that the commands reach their destination, +/// even if the future is never being polled. +pub trait CommandSink { /// Starts the timer if there is no attempt in progress. If that's not the /// case, nothing happens. - fn start(&self); + fn start(&self) -> impl Future + 'static; /// 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); + fn split(&self) -> impl Future + 'static; /// 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); + fn split_or_start(&self) -> impl Future + 'static; /// 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); + fn reset(&self, save_attempt: Option) -> impl Future + 'static; /// 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); + fn undo_split(&self) -> impl Future + 'static; /// Skips the current split if an attempt is in progress and the /// current split is not the last split. - fn skip_split(&self); + fn skip_split(&self) -> impl Future + 'static; /// 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); + fn toggle_pause_or_start(&self) -> impl Future + 'static; /// Pauses an active attempt that is not paused. - fn pause(&self); + fn pause(&self) -> impl Future + 'static; /// Resumes an attempt that is paused. - fn resume(&self); + fn resume(&self) -> impl Future + 'static; /// 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 @@ -59,187 +231,279 @@ pub trait Sink { /// 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); + fn undo_all_pauses(&self) -> impl Future + 'static; /// Switches the current comparison to the previous comparison in the list. - fn switch_to_previous_comparison(&self); + fn switch_to_previous_comparison(&self) -> impl Future + 'static; /// Switches the current comparison to the next comparison in the list. - fn switch_to_next_comparison(&self); + fn switch_to_next_comparison(&self) -> impl Future + 'static; + /// Tries to set the current comparison to the comparison specified. If the + /// comparison doesn't exist an error is returned. + fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static; /// Toggles between the `Real Time` and `Game Time` timing methods. - fn toggle_timing_method(&self); + fn toggle_timing_method(&self) -> impl Future + 'static; + /// Sets the current timing method to the timing method provided. + fn set_current_timing_method( + &self, + method: TimingMethod, + ) -> impl Future + 'static; + /// Initializes game time for the current attempt. Game time automatically + /// gets uninitialized for each new attempt. + fn initialize_game_time(&self) -> impl Future + 'static; /// 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); + fn set_game_time(&self, time: TimeSpan) -> impl Future + 'static; /// Pauses the game timer such that it doesn't automatically increment /// similar to real time. - fn pause_game_time(&self); + fn pause_game_time(&self) -> impl Future + 'static; /// 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); + fn resume_game_time(&self) -> impl Future + 'static; + /// Instead of setting the game time directly, this method can be used to + /// just specify the amount of time the game has been loading. The game time + /// is then automatically determined by Real Time - Loading Times. + fn set_loading_times(&self, time: TimeSpan) -> impl Future + 'static; /// 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); + fn set_custom_variable( + &self, + name: &str, + value: &str, + ) -> impl Future + 'static; } -/// This trait provides functionality for querying the current state of the -/// timer. +/// This trait provides functionality for querying information from the timer. pub trait TimerQuery { - /// Returns the current Timer Phase. - fn current_phase(&self) -> TimerPhase; + /// The timer can be protected by a guard. This could be a lock guard for + /// example. + type Guard<'a>: 'a + Deref + where + Self: 'a; + /// Accesses the timer to query information from it. + fn get_timer(&self) -> Self::Guard<'_>; } #[cfg(feature = "std")] -impl Sink for crate::SharedTimer { - fn start(&self) { - self.write().unwrap().start(); +impl CommandSink for crate::SharedTimer { + fn start(&self) -> impl Future + 'static { + let result = self.write().unwrap().start(); + async move { result } } - fn split(&self) { - self.write().unwrap().split(); + fn split(&self) -> impl Future + 'static { + let result = self.write().unwrap().split(); + async move { result } } - fn split_or_start(&self) { - self.write().unwrap().split_or_start(); + fn split_or_start(&self) -> impl Future + 'static { + let result = self.write().unwrap().split_or_start(); + async move { result } } - fn reset(&self, save_attempt: Option) { - self.write().unwrap().reset(save_attempt != Some(false)); + fn reset(&self, save_attempt: Option) -> impl Future + 'static { + let result = self.write().unwrap().reset(save_attempt != Some(false)); + async move { result } } - fn undo_split(&self) { - self.write().unwrap().undo_split(); + fn undo_split(&self) -> impl Future + 'static { + let result = self.write().unwrap().undo_split(); + async move { result } } - fn skip_split(&self) { - self.write().unwrap().skip_split(); + fn skip_split(&self) -> impl Future + 'static { + let result = self.write().unwrap().skip_split(); + async move { result } } - fn toggle_pause_or_start(&self) { - self.write().unwrap().toggle_pause_or_start(); + fn toggle_pause_or_start(&self) -> impl Future + 'static { + let result = self.write().unwrap().toggle_pause_or_start(); + async move { result } } - fn pause(&self) { - self.write().unwrap().pause(); + fn pause(&self) -> impl Future + 'static { + let result = self.write().unwrap().pause(); + async move { result } } - fn resume(&self) { - self.write().unwrap().resume(); + fn resume(&self) -> impl Future + 'static { + let result = self.write().unwrap().resume(); + async move { result } } - fn undo_all_pauses(&self) { - self.write().unwrap().undo_all_pauses(); + fn undo_all_pauses(&self) -> impl Future + 'static { + let result = self.write().unwrap().undo_all_pauses(); + async move { result } } - fn switch_to_previous_comparison(&self) { + fn switch_to_previous_comparison(&self) -> impl Future + 'static { self.write().unwrap().switch_to_previous_comparison(); + async { Ok(Event::ComparisonChanged) } } - fn switch_to_next_comparison(&self) { + fn switch_to_next_comparison(&self) -> impl Future + 'static { self.write().unwrap().switch_to_next_comparison(); + async { Ok(Event::ComparisonChanged) } } - fn toggle_timing_method(&self) { + fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + let result = self.write().unwrap().set_current_comparison(comparison); + async move { result } + } + + fn toggle_timing_method(&self) -> impl Future + 'static { self.write().unwrap().toggle_timing_method(); + async { Ok(Event::TimingMethodChanged) } + } + + fn set_current_timing_method( + &self, + method: TimingMethod, + ) -> impl Future + 'static { + self.write().unwrap().set_current_timing_method(method); + async { Ok(Event::TimingMethodChanged) } + } + + fn initialize_game_time(&self) -> impl Future + 'static { + let result = self.write().unwrap().initialize_game_time(); + async move { result } } - fn set_game_time(&self, time: TimeSpan) { - self.write().unwrap().set_game_time(time); + fn set_game_time(&self, time: TimeSpan) -> impl Future + 'static { + let result = self.write().unwrap().set_game_time(time); + async move { result } } - fn pause_game_time(&self) { - self.write().unwrap().pause_game_time(); + fn pause_game_time(&self) -> impl Future + 'static { + let result = self.write().unwrap().pause_game_time(); + async move { result } } - fn resume_game_time(&self) { - self.write().unwrap().resume_game_time(); + fn resume_game_time(&self) -> impl Future + 'static { + let result = self.write().unwrap().resume_game_time(); + async move { result } } - fn set_custom_variable(&self, name: &str, value: &str) { + fn set_loading_times(&self, time: TimeSpan) -> impl Future + 'static { + let result = self.write().unwrap().set_loading_times(time); + async move { result } + } + + fn set_custom_variable( + &self, + name: &str, + value: &str, + ) -> impl Future + 'static { self.write().unwrap().set_custom_variable(name, value); + async { Ok(Event::CustomVariableSet) } } } #[cfg(feature = "std")] impl TimerQuery for crate::SharedTimer { - fn current_phase(&self) -> TimerPhase { - self.read().unwrap().current_phase() + type Guard<'a> = std::sync::RwLockReadGuard<'a, Timer>; + fn get_timer(&self) -> Self::Guard<'_> { + self.read().unwrap() } } -impl Sink for Arc { - fn start(&self) { - Sink::start(&**self) +impl CommandSink for Arc { + fn start(&self) -> impl Future + 'static { + CommandSink::start(&**self) + } + + fn split(&self) -> impl Future + 'static { + CommandSink::split(&**self) + } + + fn split_or_start(&self) -> impl Future + 'static { + CommandSink::split_or_start(&**self) + } + + fn reset(&self, save_attempt: Option) -> impl Future + 'static { + CommandSink::reset(&**self, save_attempt) + } + + fn undo_split(&self) -> impl Future + 'static { + CommandSink::undo_split(&**self) } - fn split(&self) { - Sink::split(&**self) + fn skip_split(&self) -> impl Future + 'static { + CommandSink::skip_split(&**self) } - fn split_or_start(&self) { - Sink::split_or_start(&**self) + fn toggle_pause_or_start(&self) -> impl Future + 'static { + CommandSink::toggle_pause_or_start(&**self) } - fn reset(&self, save_attempt: Option) { - Sink::reset(&**self, save_attempt) + fn pause(&self) -> impl Future + 'static { + CommandSink::pause(&**self) } - fn undo_split(&self) { - Sink::undo_split(&**self) + fn resume(&self) -> impl Future + 'static { + CommandSink::resume(&**self) } - fn skip_split(&self) { - Sink::skip_split(&**self) + fn undo_all_pauses(&self) -> impl Future + 'static { + CommandSink::undo_all_pauses(&**self) } - fn toggle_pause_or_start(&self) { - Sink::toggle_pause_or_start(&**self) + fn switch_to_previous_comparison(&self) -> impl Future + 'static { + CommandSink::switch_to_previous_comparison(&**self) } - fn pause(&self) { - Sink::pause(&**self) + fn switch_to_next_comparison(&self) -> impl Future + 'static { + CommandSink::switch_to_next_comparison(&**self) } - fn resume(&self) { - Sink::resume(&**self) + fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + CommandSink::set_current_comparison(&**self, comparison) } - fn undo_all_pauses(&self) { - Sink::undo_all_pauses(&**self) + fn toggle_timing_method(&self) -> impl Future + 'static { + CommandSink::toggle_timing_method(&**self) } - fn switch_to_previous_comparison(&self) { - Sink::switch_to_previous_comparison(&**self) + fn set_current_timing_method( + &self, + method: TimingMethod, + ) -> impl Future + 'static { + CommandSink::set_current_timing_method(&**self, method) } - fn switch_to_next_comparison(&self) { - Sink::switch_to_next_comparison(&**self) + fn initialize_game_time(&self) -> impl Future + 'static { + CommandSink::initialize_game_time(&**self) } - fn toggle_timing_method(&self) { - Sink::toggle_timing_method(&**self) + fn set_game_time(&self, time: TimeSpan) -> impl Future + 'static { + CommandSink::set_game_time(&**self, time) } - fn set_game_time(&self, time: TimeSpan) { - Sink::set_game_time(&**self, time) + fn pause_game_time(&self) -> impl Future + 'static { + CommandSink::pause_game_time(&**self) } - fn pause_game_time(&self) { - Sink::pause_game_time(&**self) + fn resume_game_time(&self) -> impl Future + 'static { + CommandSink::resume_game_time(&**self) } - fn resume_game_time(&self) { - Sink::resume_game_time(&**self) + fn set_loading_times(&self, time: TimeSpan) -> impl Future + 'static { + CommandSink::set_loading_times(&**self, time) } - fn set_custom_variable(&self, name: &str, value: &str) { - Sink::set_custom_variable(&**self, name, value) + fn set_custom_variable( + &self, + name: &str, + value: &str, + ) -> impl Future + 'static { + CommandSink::set_custom_variable(&**self, name, value) } } impl TimerQuery for Arc { - fn current_phase(&self) -> TimerPhase { - TimerQuery::current_phase(&**self) + type Guard<'a> = T::Guard<'a> where T: 'a; + fn get_timer(&self) -> Self::Guard<'_> { + TimerQuery::get_timer(&**self) } } diff --git a/src/hotkey_system.rs b/src/hotkey_system.rs index 0dce390f..3a59bf84 100644 --- a/src/hotkey_system.rs +++ b/src/hotkey_system.rs @@ -61,22 +61,38 @@ impl Action { } } - fn callback( + fn callback( self, - event_sink: E, + command_sink: S, ) -> Box { match self { - 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 || 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()), + Action::Split => Box::new(move || { + drop(command_sink.split_or_start()); + }), + Action::Reset => Box::new(move || { + drop(command_sink.reset(None)); + }), + Action::Undo => Box::new(move || { + drop(command_sink.undo_split()); + }), + Action::Skip => Box::new(move || { + drop(command_sink.skip_split()); + }), + Action::Pause => Box::new(move || { + drop(command_sink.toggle_pause_or_start()); + }), + Action::UndoAllPauses => Box::new(move || { + drop(command_sink.undo_all_pauses()); + }), + Action::PreviousComparison => Box::new(move || { + drop(command_sink.switch_to_previous_comparison()); + }), + Action::NextComparison => Box::new(move || { + drop(command_sink.switch_to_next_comparison()); + }), + Action::ToggleTimingMethod => Box::new(move || { + drop(command_sink.toggle_timing_method()); + }), } } } @@ -86,26 +102,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, - event_sink: E, + command_sink: S, is_active: bool, } -impl HotkeySystem { +impl HotkeySystem { /// Creates a new Hotkey System for a Timer with the default hotkeys. - pub fn new(event_sink: E) -> Result { - Self::with_config(event_sink, Default::default()) + pub fn new(command_sink: S) -> Result { + Self::with_config(command_sink, Default::default()) } /// Creates a new Hotkey System for a Timer with a custom configuration for /// the hotkeys. - pub fn with_config(event_sink: E, config: HotkeyConfig) -> Result { + pub fn with_config(command_sink: S, config: HotkeyConfig) -> Result { let mut hotkey_system = Self { config, hook: Hook::with_consume_preference(ConsumePreference::PreferNoConsume)?, - event_sink, + command_sink, is_active: false, }; hotkey_system.activate()?; @@ -115,7 +131,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.event_sink.clone(); + let inner = self.command_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 ec89ea38..15a6d49d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,17 +35,17 @@ //! let mut timer = Timer::new(run).expect("Run with at least one segment provided"); //! //! // Start a new attempt. -//! timer.start(); +//! timer.start().unwrap(); //! assert_eq!(timer.current_phase(), TimerPhase::Running); //! //! // Create a split. -//! timer.split(); +//! timer.split().unwrap(); //! //! // The run should be finished now. //! assert_eq!(timer.current_phase(), TimerPhase::Ended); //! //! // Reset the attempt and confirm that we want to store the attempt. -//! timer.reset(true); +//! timer.reset(true).unwrap(); //! //! // The attempt is now over. //! assert_eq!(timer.current_phase(), TimerPhase::NotRunning); @@ -72,7 +72,6 @@ mod hotkey_config; #[cfg(feature = "std")] mod hotkey_system; pub mod layout; -#[cfg(feature = "networking")] pub mod networking; #[cfg(feature = "rendering")] pub mod rendering; diff --git a/src/networking/mod.rs b/src/networking/mod.rs index b0071005..290fbbd8 100644 --- a/src/networking/mod.rs +++ b/src/networking/mod.rs @@ -3,4 +3,7 @@ //! and Speedrun.com to query and submit to the leaderboards of most games. The //! module is optional and is not compiled in by default. +#[cfg(feature = "std")] +pub mod server_protocol; +#[cfg(feature = "networking")] pub mod splits_io; diff --git a/src/networking/server_protocol.rs b/src/networking/server_protocol.rs new file mode 100644 index 00000000..0aa6ec46 --- /dev/null +++ b/src/networking/server_protocol.rs @@ -0,0 +1,390 @@ +//! The server protocol is an experimental JSON based protocol that is used to +//! remotely control the timer. Every command that you send has a response in +//! the form of a JSON object indicating whether the command was successful or +//! not. +//! +//! A command looks like this: +//! ```json +//! { "command": "SplitOrStart" } +//! ``` +//! +//! Additional parameters can be added to the command, depending on what command +//! it is. For example: +//! ```json +//! { "command": "SetGameTime", "time": "1:23:45" } +//! ``` +//! +//! If the command was successful, the response looks like this: +//! ```json +//! { "success": null } +//! ``` +//! +//! If the command was to retrieve information, the value is there instead of +//! `null`. +//! +//! An error is indicated by the following JSON object: +//! ```json +//! { "error": { "code": "NoRunInProgress" } } +//! ``` +//! +//! An optional `message` field may be present to provide additional information +//! about the error. +//! +//! You are also sent events that indicate changes in the timer. The events look +//! like this: +//! ```json +//! { "event": "Splitted" } +//! ``` +//! +//! The events are currently also sent as responses to commands, but this may +//! change in the future. So for example, if you split the timer, you will +//! receive a response like this: +//! ```json +//! { "event": "Splitted" } +//! { "success": null } +//! ``` +//! +//! This does not mean that two splits happened. +//! +//! Keep in mind the experimental nature of the protocol. It will likely change +//! a lot in the future. + +use crate::{ + event::{self, Event}, + timing::formatter::{self, TimeFormatter, ASCII_MINUS}, + TimeSpan, Timer, TimerPhase, TimingMethod, +}; + +/// Handles an incoming command and returns the response to be sent. +pub async fn handle_command( + command: &str, + command_sink: &S, +) -> String { + let response = match serde_json::from_str::(command) { + Ok(command) => command.handle(command_sink).await.into(), + Err(e) => CommandResult::Error(Error::InvalidCommand { + message: e.to_string(), + }), + }; + + serde_json::to_string(&response).unwrap() +} + +/// Encodes an event that happened to be sent. +pub fn encode_event(event: Event) -> String { + serde_json::to_string(&IsEvent { event }).unwrap() +} + +#[derive(serde_derive::Serialize)] +#[serde(rename_all = "camelCase")] +enum CommandResult { + Success(T), + Error(E), +} + +impl From> for CommandResult { + fn from(result: Result) -> Self { + match result { + Ok(value) => CommandResult::Success(value), + Err(error) => CommandResult::Error(error), + } + } +} + +#[derive(serde_derive::Serialize)] +struct IsEvent { + event: Event, +} + +#[derive(serde_derive::Deserialize)] +#[serde(tag = "command", rename_all = "camelCase")] +enum Command { + SplitOrStart, + Split, + UndoSplit, + SkipSplit, + Pause, + Resume, + TogglePauseOrStart, + Reset, + Start, + InitializeGameTime, + SetGameTime { + time: TimeSpan, + }, + SetLoadingTimes { + time: TimeSpan, + }, + PauseGameTime, + ResumeGameTime, + SetCustomVariable { + key: String, + value: String, + }, + SetCurrentComparison { + comparison: String, + }, + #[serde(rename_all = "camelCase")] + SetCurrentTimingMethod { + timing_method: TimingMethod, + }, + #[serde(rename_all = "camelCase")] + GetCurrentTime { + timing_method: Option, + }, + GetSegmentName { + index: Option, + #[serde(default)] + relative: bool, + }, + #[serde(rename_all = "camelCase")] + GetComparisonTime { + index: Option, + #[serde(default)] + relative: bool, + comparison: Option, + timing_method: Option, + }, + #[serde(rename_all = "camelCase")] + GetCurrentRunSplitTime { + index: Option, + #[serde(default)] + relative: bool, + timing_method: Option, + }, + GetCurrentState, + Ping, +} + +#[derive(serde_derive::Serialize)] +#[serde(tag = "state", content = "index")] +enum State { + NotRunning, + Running(usize), + Paused(usize), + Ended, +} + +#[derive(serde_derive::Serialize)] +#[serde(untagged)] +enum Response { + None, + String(String), + State(State), +} + +#[derive(serde_derive::Serialize)] +#[serde(tag = "code")] +enum Error { + InvalidCommand { + message: String, + }, + InvalidIndex, + #[serde(untagged)] + Timer { + code: event::Error, + }, +} + +impl Error { + const fn timer(code: event::Error) -> Self { + Error::Timer { code } + } +} + +impl Command { + async fn handle( + &self, + command_sink: &E, + ) -> Result { + Ok(match self { + Command::SplitOrStart => { + command_sink.split_or_start().await.map_err(Error::timer)?; + Response::None + } + Command::Split => { + command_sink.split().await.map_err(Error::timer)?; + Response::None + } + Command::UndoSplit => { + command_sink.undo_split().await.map_err(Error::timer)?; + Response::None + } + Command::SkipSplit => { + command_sink.skip_split().await.map_err(Error::timer)?; + Response::None + } + Command::Pause => { + command_sink.pause().await.map_err(Error::timer)?; + Response::None + } + Command::Resume => { + command_sink.resume().await.map_err(Error::timer)?; + Response::None + } + Command::TogglePauseOrStart => { + command_sink + .toggle_pause_or_start() + .await + .map_err(Error::timer)?; + Response::None + } + Command::Reset => { + command_sink.reset(None).await.map_err(Error::timer)?; + Response::None + } + Command::Start => { + command_sink.start().await.map_err(Error::timer)?; + Response::None + } + Command::InitializeGameTime => { + command_sink + .initialize_game_time() + .await + .map_err(Error::timer)?; + Response::None + } + Command::SetGameTime { time } => { + command_sink + .set_game_time(*time) + .await + .map_err(Error::timer)?; + Response::None + } + Command::SetLoadingTimes { time } => { + command_sink + .set_loading_times(*time) + .await + .map_err(Error::timer)?; + Response::None + } + Command::PauseGameTime => { + command_sink.pause_game_time().await.map_err(Error::timer)?; + Response::None + } + Command::ResumeGameTime => { + command_sink + .resume_game_time() + .await + .map_err(Error::timer)?; + Response::None + } + Command::SetCustomVariable { key, value } => { + command_sink + .set_custom_variable(key, value) + .await + .map_err(Error::timer)?; + Response::None + } + Command::SetCurrentComparison { comparison } => { + command_sink + .set_current_comparison(comparison) + .await + .map_err(Error::timer)?; + Response::None + } + Command::SetCurrentTimingMethod { timing_method } => { + command_sink + .set_current_timing_method(*timing_method) + .await + .map_err(Error::timer)?; + Response::None + } + Command::GetCurrentTime { timing_method } => { + let guard = command_sink.get_timer(); + let timer = &*guard; + + let timing_method = timing_method.unwrap_or_else(|| timer.current_timing_method()); + let time = timer.snapshot().current_time()[timing_method]; + if let Some(time) = time { + Response::String(format_time(time)) + } else { + Response::None + } + } + Command::GetSegmentName { index, relative } => { + let guard = command_sink.get_timer(); + let timer = &*guard; + let index = resolve_index(timer, *index, *relative)?; + Response::String(timer.run().segment(index).name().into()) + } + Command::GetComparisonTime { + index, + relative, + comparison, + timing_method, + } => { + let guard = command_sink.get_timer(); + let timer = &*guard; + let index = resolve_index(timer, *index, *relative)?; + let timing_method = timing_method.unwrap_or_else(|| timer.current_timing_method()); + + let comparison = comparison.as_deref().unwrap_or(timer.current_comparison()); + + let time = timer.run().segment(index).comparison(comparison)[timing_method]; + if let Some(time) = time { + Response::String(format_time(time)) + } else { + Response::None + } + } + Command::GetCurrentRunSplitTime { + index, + relative, + timing_method, + } => { + let guard = command_sink.get_timer(); + let timer = &*guard; + let index = resolve_index(timer, *index, *relative)?; + let timing_method = timing_method.unwrap_or_else(|| timer.current_timing_method()); + + let time = timer.run().segment(index).split_time()[timing_method]; + if let Some(time) = time { + Response::String(format_time(time)) + } else { + Response::None + } + } + Command::GetCurrentState => { + let guard = command_sink.get_timer(); + let timer = &*guard; + let phase = timer.current_phase(); + Response::State(match phase { + TimerPhase::NotRunning => State::NotRunning, + TimerPhase::Running => State::Running(timer.current_split_index().unwrap()), + TimerPhase::Paused => State::Paused(timer.current_split_index().unwrap()), + TimerPhase::Ended => State::Ended, + }) + } + Command::Ping => Response::None, + }) + } +} + +fn resolve_index(timer: &Timer, index: Option, relative: bool) -> Result { + let index = if let Some(index) = index { + let base = if relative { + timer.current_split_index().ok_or(Error::InvalidIndex)? + } else if index < 0 { + timer.run().len() + } else { + 0 + }; + base.checked_add_signed(index) + } else { + timer.current_split_index() + } + .ok_or(Error::InvalidIndex)?; + + if index >= timer.run().len() { + Err(Error::InvalidIndex) + } else { + Ok(index) + } +} + +fn format_time(time: TimeSpan) -> String { + formatter::none_wrapper::NoneWrapper::new(formatter::Complete::new(), ASCII_MINUS) + .format(time) + .to_string() +} diff --git a/src/timing/mod.rs b/src/timing/mod.rs index 2bc23713..ec854313 100644 --- a/src/timing/mod.rs +++ b/src/timing/mod.rs @@ -10,12 +10,14 @@ mod timer; mod timer_phase; mod timing_method; -pub use self::atomic_date_time::AtomicDateTime; -pub use self::time::{GameTime, RealTime, Time}; -pub use self::time_span::{ParseError, TimeSpan}; -pub use self::time_stamp::TimeStamp; #[cfg(feature = "std")] pub use self::timer::SharedTimer; -pub use self::timer::{CreationError as TimerCreationError, Snapshot, Timer}; -pub use self::timer_phase::TimerPhase; -pub use self::timing_method::TimingMethod; +pub use self::{ + atomic_date_time::AtomicDateTime, + time::{GameTime, RealTime, Time}, + time_span::{ParseError, TimeSpan}, + time_stamp::TimeStamp, + timer::{CreationError as TimerCreationError, Snapshot, Timer}, + timer_phase::TimerPhase, + timing_method::TimingMethod, +}; diff --git a/src/timing/timer/active_attempt.rs b/src/timing/timer/active_attempt.rs index b2cfa87f..f88f9384 100644 --- a/src/timing/timer/active_attempt.rs +++ b/src/timing/timer/active_attempt.rs @@ -1,4 +1,7 @@ -use crate::{AtomicDateTime, Run, Time, TimeSpan, TimeStamp, TimingMethod}; +use crate::{ + event::{Error, Event, Result}, + AtomicDateTime, Run, Time, TimeSpan, TimeStamp, TimingMethod, +}; #[derive(Debug, Clone)] pub struct ActiveAttempt { @@ -89,19 +92,23 @@ impl ActiveAttempt { } } - pub fn prepare_split(&mut self, run: &Run) -> Option<(usize, Time)> { + pub fn prepare_split(&mut self, run: &Run) -> Result<(usize, Time, Event)> { let State::NotEnded { current_split_index, - time_paused_at: None, + time_paused_at, } = &mut self.state else { - return None; + return Err(Error::RunFinished); }; + if time_paused_at.is_some() { + return Err(Error::TimerPaused); + } + let real_time = TimeStamp::now() - self.adjusted_start_time; if real_time < TimeSpan::zero() { - return None; + return Err(Error::NegativeTime); } let game_time = self @@ -111,18 +118,22 @@ impl ActiveAttempt { let previous_split_index = *current_split_index; *current_split_index += 1; - if *current_split_index == run.len() { + let event = if *current_split_index == run.len() { self.state = State::Ended { attempt_ended: AtomicDateTime::now(), }; - } + Event::Finished + } else { + Event::Splitted + }; - Some(( + Ok(( previous_split_index, Time { real_time: Some(real_time), game_time, }, + event, )) } diff --git a/src/timing/timer/mod.rs b/src/timing/timer/mod.rs index 25877115..a05c0ecc 100644 --- a/src/timing/timer/mod.rs +++ b/src/timing/timer/mod.rs @@ -1,7 +1,12 @@ use crate::{ - analysis::check_best_segment, comparison::personal_best, platform::prelude::*, - util::PopulateString, AtomicDateTime, Run, Segment, Time, TimeSpan, TimeStamp, TimerPhase, - TimerPhase::*, TimingMethod, + analysis::check_best_segment, + comparison::personal_best, + event::{Error, Event}, + platform::prelude::*, + util::PopulateString, + AtomicDateTime, Run, Segment, Time, TimeSpan, TimeStamp, + TimerPhase::{self, *}, + TimingMethod, }; use core::{mem, ops::Deref}; @@ -28,17 +33,17 @@ use active_attempt::{ActiveAttempt, State}; /// let mut timer = Timer::new(run).expect("Run with at least one segment provided"); /// /// // Start a new attempt. -/// timer.start(); +/// timer.start().unwrap(); /// assert_eq!(timer.current_phase(), TimerPhase::Running); /// /// // Create a split. -/// timer.split(); +/// timer.split().unwrap(); /// /// // The run should be finished now. /// assert_eq!(timer.current_phase(), TimerPhase::Ended); /// /// // Reset the attempt and confirm that we want to store the attempt. -/// timer.reset(true); +/// timer.reset(true).unwrap(); /// /// // The attempt is now over. /// assert_eq!(timer.current_phase(), TimerPhase::NotRunning); @@ -85,6 +90,8 @@ pub enum CreationError { EmptyRun, } +pub type Result = core::result::Result; + impl Timer { /// Creates a new Timer based on a Run object storing all the information /// about the splits. The Run object needs to have at least one segment, so @@ -119,7 +126,7 @@ impl Timer { /// of the current attempt is stored in the Run's history. Otherwise the /// current attempt's information is discarded. pub fn into_run(mut self, update_splits: bool) -> Run { - self.reset(update_splits); + let _ = self.reset(update_splits); self.run } @@ -135,7 +142,7 @@ impl Timer { return Err(run); } - self.reset(update_splits); + let _ = self.reset(update_splits); if !run.comparisons().any(|c| c == self.current_comparison) { self.current_comparison = personal_best::NAME.to_string(); } @@ -205,13 +212,13 @@ impl Timer { Snapshot { timer: self, time } } - /// Returns the currently selected Timing Method. + /// Returns the currently selected timing method. #[inline] pub const fn current_timing_method(&self) -> TimingMethod { self.current_timing_method } - /// Sets the current Timing Method to the Timing Method provided. + /// Sets the current timing method to the timing method provided. #[inline] pub fn set_current_timing_method(&mut self, method: TimingMethod) { self.current_timing_method = method; @@ -236,13 +243,13 @@ impl Timer { /// Tries to set the current comparison to the comparison specified. If the /// comparison doesn't exist `Err` is returned. #[inline] - pub fn set_current_comparison(&mut self, comparison: S) -> Result<(), ()> { + pub fn set_current_comparison(&mut self, comparison: S) -> Result { let as_str = comparison.as_str(); if self.run.comparisons().any(|c| c == as_str) { comparison.populate(&mut self.current_comparison); - Ok(()) + Ok(Event::ComparisonChanged) } else { - Err(()) + Err(Error::ComparisonDoesntExist) } } @@ -271,7 +278,7 @@ impl Timer { /// Starts the Timer if there is no attempt in progress. If that's not the /// case, nothing happens. - pub fn start(&mut self) { + pub fn start(&mut self) -> Result { if self.active_attempt.is_none() { let attempt_started = AtomicDateTime::now(); let start_time = TimeStamp::now(); @@ -290,19 +297,19 @@ impl Timer { loading_times: None, }); self.run.start_next_run(); + + Ok(Event::Started) + } else { + Err(Error::RunAlreadyInProgress) } } /// 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. - pub fn split(&mut self) { - let Some(active_attempt) = &mut self.active_attempt else { - return; - }; + pub fn split(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; - let Some((split_index, current_time)) = active_attempt.prepare_split(&self.run) else { - return; - }; + let (split_index, current_time, event) = active_attempt.prepare_split(&self.run)?; // FIXME: We shouldn't need to collect here. let variables = self @@ -317,27 +324,27 @@ impl Timer { *segment.variables_mut() = variables; self.run.mark_as_modified(); + + Ok(event) } /// 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. - pub fn split_or_start(&mut self) { + pub fn split_or_start(&mut self) -> Result { if self.active_attempt.is_none() { - self.start(); + self.start() } else { - self.split(); + self.split() } } /// Skips the current split if an attempt is in progress and the /// current split is not the last split. - pub fn skip_split(&mut self) { - let Some(active_attempt) = &mut self.active_attempt else { - return; - }; + pub fn skip_split(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; let Some(current_split_index) = active_attempt.current_split_index_mut() else { - return; + return Err(Error::RunFinished); }; if *current_split_index + 1 < self.run.len() { @@ -348,16 +355,18 @@ impl Timer { *current_split_index += 1; self.run.mark_as_modified(); + + Ok(Event::SplitSkipped) + } else { + Err(Error::CantSkipLastSplit) } } /// 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`] if it previously was [`Ended`]. - pub fn undo_split(&mut self) { - let Some(active_attempt) = &mut self.active_attempt else { - return; - }; + pub fn undo_split(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; if let Some(previous_split_index) = active_attempt .current_split_index_overflowing(&self.run) @@ -378,6 +387,10 @@ impl Timer { .clear_split_info(); self.run.mark_as_modified(); + + Ok(Event::SplitUndone) + } else { + Err(Error::CantUndoFirstSplit) } } @@ -431,21 +444,27 @@ impl Timer { /// 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. - pub fn reset(&mut self, update_splits: bool) { + pub fn reset(&mut self, update_splits: bool) -> Result { if self.active_attempt.is_some() { self.reset_state(update_splits); self.reset_splits(); + Ok(Event::Reset) + } else { + Err(Error::NoRunInProgress) } } /// Resets the current attempt if there is one in progress. The splits are /// updated such that the current attempt's split times are being stored as /// the new Personal Best. - pub fn reset_and_set_attempt_as_pb(&mut self) { + pub fn reset_and_set_attempt_as_pb(&mut self) -> Result { if self.active_attempt.is_some() { self.reset_state(true); set_run_as_pb(&mut self.run); self.reset_splits(); + Ok(Event::Reset) + } else { + Err(Error::NoRunInProgress) } } @@ -470,51 +489,56 @@ impl Timer { } /// Pauses an active attempt that is not paused. - pub fn pause(&mut self) { - if let Some(ActiveAttempt { - state: State::NotEnded { time_paused_at, .. }, - adjusted_start_time, - .. - }) = &mut self.active_attempt - { - if time_paused_at.is_none() { - *time_paused_at = Some(TimeStamp::now() - *adjusted_start_time); - } + pub fn pause(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; + + let State::NotEnded { time_paused_at, .. } = &mut active_attempt.state else { + return Err(Error::RunFinished); + }; + + if time_paused_at.is_none() { + *time_paused_at = Some(TimeStamp::now() - active_attempt.adjusted_start_time); + Ok(Event::Paused) + } else { + Err(Error::AlreadyPaused) } } /// Resumes an attempt that is paused. - pub fn resume(&mut self) { - if let Some(ActiveAttempt { - state: State::NotEnded { time_paused_at, .. }, - adjusted_start_time, - .. - }) = &mut self.active_attempt - { - if let Some(pause_time) = *time_paused_at { - *adjusted_start_time = TimeStamp::now() - pause_time; - *time_paused_at = None; - } + pub fn resume(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; + + let State::NotEnded { time_paused_at, .. } = &mut active_attempt.state else { + return Err(Error::RunFinished); + }; + + if let Some(pause_time) = *time_paused_at { + active_attempt.adjusted_start_time = TimeStamp::now() - pause_time; + *time_paused_at = None; + Ok(Event::Resumed) + } else { + Err(Error::NotPaused) } } /// Toggles an active attempt between `Paused` and `Running`. - pub fn toggle_pause(&mut self) { + pub fn toggle_pause(&mut self) -> Result { match self.current_phase() { Running => self.pause(), Paused => self.resume(), - _ => {} + NotRunning => Err(Error::NoRunInProgress), + Ended => Err(Error::RunFinished), } } /// 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) { + pub fn toggle_pause_or_start(&mut self) -> Result { match self.current_phase() { Running => self.pause(), Paused => self.resume(), NotRunning => self.start(), - _ => {} + Ended => Err(Error::RunFinished), } } @@ -528,9 +552,12 @@ impl Timer { /// 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. - pub fn undo_all_pauses(&mut self) { - match self.current_phase() { - Paused => self.resume(), + pub fn undo_all_pauses(&mut self) -> Result { + let event = match self.current_phase() { + Paused => { + self.resume()?; + Event::PausesUndoneAndResumed + } Ended => { let pause_time = Some(self.get_pause_time().unwrap_or_default()); @@ -545,12 +572,17 @@ impl Timer { *split_time += Time::new() .with_real_time(pause_time) .with_game_time(pause_time); + + Event::PausesUndone } - _ => {} - } + _ => Event::PausesUndone, + }; if let Some(active_attempt) = &mut self.active_attempt { active_attempt.adjusted_start_time = active_attempt.start_time_with_offset; + Ok(event) + } else { + Err(Error::NoRunInProgress) } } @@ -619,14 +651,17 @@ impl Timer { } } - /// Initializes Game Time for the current attempt. Game Time automatically + /// Initializes game time for the current attempt. Game time automatically /// gets uninitialized for each new attempt. #[inline] - pub fn initialize_game_time(&mut self) { - if let Some(active_attempt) = &mut self.active_attempt { - if active_attempt.loading_times.is_none() { - active_attempt.loading_times = Some(TimeSpan::zero()); - } + pub fn initialize_game_time(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; + + if active_attempt.loading_times.is_none() { + active_attempt.loading_times = Some(TimeSpan::zero()); + Ok(Event::GameTimeInitialized) + } else { + Err(Error::GameTimeAlreadyInitialized) } } @@ -650,27 +685,35 @@ impl Timer { /// Pauses the Game Timer such that it doesn't automatically increment /// similar to Real Time. - pub fn pause_game_time(&mut self) { - if let Some(active_attempt) = &mut self.active_attempt { - if active_attempt.game_time_paused_at.is_none() { - let current_time = active_attempt.current_time(&self.run); + pub fn pause_game_time(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; - active_attempt.game_time_paused_at = - current_time.game_time.or(Some(current_time.real_time)); - } + if active_attempt.game_time_paused_at.is_none() { + let current_time = active_attempt.current_time(&self.run); + + active_attempt.game_time_paused_at = + current_time.game_time.or(Some(current_time.real_time)); + + Ok(Event::GameTimePaused) + } else { + Err(Error::GameTimeAlreadyPaused) } } /// Resumes the Game Timer such that it automatically increments similar to /// Real Time, starting from the Game Time it was paused at. - pub fn resume_game_time(&mut self) { - if let Some(active_attempt) = &mut self.active_attempt { - if active_attempt.game_time_paused_at.take().is_some() { - let current_time = active_attempt.current_time(&self.run); + pub fn resume_game_time(&mut self) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; - let diff = catch! { current_time.real_time - current_time.game_time? }; - active_attempt.set_loading_times(diff.unwrap_or_default(), &self.run); - } + if active_attempt.game_time_paused_at.take().is_some() { + let current_time = active_attempt.current_time(&self.run); + + let diff = catch! { current_time.real_time - current_time.game_time? }; + active_attempt.set_loading_times(diff.unwrap_or_default(), &self.run); + + Ok(Event::GameTimeResumed) + } else { + Err(Error::GameTimeNotPaused) } } @@ -679,14 +722,16 @@ impl Timer { /// periodically without it automatically moving forward. This ensures that /// the Game Timer never shows any time that is not coming from the game. #[inline] - pub fn set_game_time(&mut self, game_time: TimeSpan) { - if let Some(active_attempt) = &mut self.active_attempt { - if active_attempt.game_time_paused_at.is_some() { - active_attempt.game_time_paused_at = Some(game_time); - } - active_attempt.loading_times = - Some(active_attempt.current_time(&self.run).real_time - game_time); + pub fn set_game_time(&mut self, game_time: TimeSpan) -> Result { + let active_attempt = self.active_attempt.as_mut().ok_or(Error::NoRunInProgress)?; + + if active_attempt.game_time_paused_at.is_some() { + active_attempt.game_time_paused_at = Some(game_time); } + active_attempt.loading_times = + Some(active_attempt.current_time(&self.run).real_time - game_time); + + Ok(Event::GameTimeSet) } /// Accesses the loading times. Loading times are defined as Game Time - Real Time. @@ -698,13 +743,16 @@ impl Timer { .unwrap_or_default() } - /// Instead of setting the Game Time directly, this method can be used to - /// just specify the amount of time the game has been loading. The Game Time + /// Instead of setting the game time directly, this method can be used to + /// just specify the amount of time the game has been loading. The game time /// is then automatically determined by Real Time - Loading Times. #[inline] - pub fn set_loading_times(&mut self, time: TimeSpan) { + pub fn set_loading_times(&mut self, time: TimeSpan) -> Result { if let Some(active_attempt) = &mut self.active_attempt { active_attempt.set_loading_times(time, &self.run); + Ok(Event::LoadingTimesSet) + } else { + Err(Error::NoRunInProgress) } } diff --git a/src/timing/timer/tests/events.rs b/src/timing/timer/tests/events.rs new file mode 100644 index 00000000..b8fde391 --- /dev/null +++ b/src/timing/timer/tests/events.rs @@ -0,0 +1,886 @@ +use crate::{ + comparison, + event::{Error, Event}, + TimeSpan, Timer, +}; + +use super::{run, timer}; + +mod start { + use super::*; + + #[test] + fn new_run_works() { + let mut timer = timer(); + + let event = timer.start().unwrap(); + + assert_eq!(event, Event::Started); + } + + #[test] + fn two_runs_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + let error = timer.start().unwrap_err(); + + assert_eq!(error, Error::RunAlreadyInProgress); + } +} + +mod split { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.split().unwrap(); + + assert_eq!(event, Event::Splitted); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.split().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.split().unwrap_err(); + + assert_eq!(error, Error::RunFinished); + } + + #[test] + fn before_timer_is_at_0_fails() { + let mut run = run(); + run.set_offset(TimeSpan::from_seconds(-1000.0)); + let mut timer = Timer::new(run).unwrap(); + + timer.start().unwrap(); + let error = timer.split().unwrap_err(); + + assert_eq!(error, Error::NegativeTime); + } + + #[test] + fn final_split_finishes_the_run() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.split().unwrap(); + + assert_eq!(event, Event::Finished); + } + + #[test] + fn while_paused_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let error = timer.split().unwrap_err(); + + assert_eq!(error, Error::TimerPaused); + } +} + +mod split_or_start { + use super::*; + + #[test] + fn starting_new_run_works() { + let mut timer = timer(); + + let event = timer.split_or_start().unwrap(); + + assert_eq!(event, Event::Started); + } + + #[test] + fn splitting_works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.split_or_start().unwrap(); + + assert_eq!(event, Event::Splitted); + } + + #[test] + fn finished_run_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.split_or_start().unwrap_err(); + + assert_eq!(error, Error::RunFinished); + } + + #[test] + fn before_timer_is_at_0_fails() { + let mut run = run(); + run.set_offset(TimeSpan::from_seconds(-1000.0)); + let mut timer = Timer::new(run).unwrap(); + + timer.start().unwrap(); + let error = timer.split_or_start().unwrap_err(); + + assert_eq!(error, Error::NegativeTime); + } + + #[test] + fn final_split_finishes_the_run() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.split_or_start().unwrap(); + + assert_eq!(event, Event::Finished); + } + + #[test] + fn while_paused_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let error = timer.split_or_start().unwrap_err(); + + assert_eq!(error, Error::TimerPaused); + } +} + +mod reset { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.reset(true).unwrap(); + + assert_eq!(event, Event::Reset); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.reset(true).unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.reset(true).unwrap(); + + assert_eq!(event, Event::Reset); + } + + #[test] + fn while_paused_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let event = timer.reset(true).unwrap(); + + assert_eq!(event, Event::Reset); + } +} + +mod reset_and_set_attempt_as_pb { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.reset_and_set_attempt_as_pb().unwrap(); + + assert_eq!(event, Event::Reset); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.reset_and_set_attempt_as_pb().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.reset_and_set_attempt_as_pb().unwrap(); + + assert_eq!(event, Event::Reset); + } + + #[test] + fn while_paused_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let event = timer.reset_and_set_attempt_as_pb().unwrap(); + + assert_eq!(event, Event::Reset); + } +} + +mod undo_split { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + let event = timer.undo_split().unwrap(); + + assert_eq!(event, Event::SplitUndone); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.undo_split().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.undo_split().unwrap(); + + assert_eq!(event, Event::SplitUndone); + } + + #[test] + fn cant_undo_first_split() { + let mut timer = timer(); + + timer.start().unwrap(); + let error = timer.undo_split().unwrap_err(); + + assert_eq!(error, Error::CantUndoFirstSplit); + } + + #[test] + fn while_paused_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.pause().unwrap(); + let event = timer.undo_split().unwrap(); + + assert_eq!(event, Event::SplitUndone); + } +} + +mod skip_split { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.skip_split().unwrap(); + + assert_eq!(event, Event::SplitSkipped); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.skip_split().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.skip_split().unwrap_err(); + + assert_eq!(error, Error::RunFinished); + } + + #[test] + fn cant_skip_last_split() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.skip_split().unwrap_err(); + + assert_eq!(error, Error::CantSkipLastSplit); + } + + #[test] + fn while_paused_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let event = timer.skip_split().unwrap(); + + assert_eq!(event, Event::SplitSkipped); + } +} + +mod toggle_pause { + use super::*; + + #[test] + fn pause_works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.toggle_pause().unwrap(); + + assert_eq!(event, Event::Paused); + } + + #[test] + fn resume_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let event = timer.toggle_pause().unwrap(); + + assert_eq!(event, Event::Resumed); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.toggle_pause().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.toggle_pause().unwrap_err(); + + assert_eq!(error, Error::RunFinished); + } +} + +mod toggle_pause_or_start { + use super::*; + + #[test] + fn start_works() { + let mut timer = timer(); + + let event = timer.toggle_pause_or_start().unwrap(); + + assert_eq!(event, Event::Started); + } + + #[test] + fn pause_works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.toggle_pause_or_start().unwrap(); + + assert_eq!(event, Event::Paused); + } + + #[test] + fn resume_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let event = timer.toggle_pause_or_start().unwrap(); + + assert_eq!(event, Event::Resumed); + } + + #[test] + fn finished_run_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.toggle_pause_or_start().unwrap_err(); + + assert_eq!(error, Error::RunFinished); + } +} + +mod pause { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.pause().unwrap(); + + assert_eq!(event, Event::Paused); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.pause().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn already_paused_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let error = timer.pause().unwrap_err(); + + assert_eq!(error, Error::AlreadyPaused); + } + + #[test] + fn finished_run_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.pause().unwrap_err(); + + assert_eq!(error, Error::RunFinished); + } +} + +mod resume { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let event = timer.resume().unwrap(); + + assert_eq!(event, Event::Resumed); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.resume().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn not_paused_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + let error = timer.resume().unwrap_err(); + + assert_eq!(error, Error::NotPaused); + } + + #[test] + fn finished_run_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let error = timer.resume().unwrap_err(); + + assert_eq!(error, Error::RunFinished); + } +} + +mod undo_all_pauses { + use super::*; + + #[test] + fn works_when_never_paused() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.undo_all_pauses().unwrap(); + + assert_eq!(event, Event::PausesUndone); + } + + #[test] + fn works_when_paused() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + let event = timer.undo_all_pauses().unwrap(); + + assert_eq!(event, Event::PausesUndoneAndResumed); + } + + #[test] + fn works_when_resumed() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause().unwrap(); + timer.resume().unwrap(); + let event = timer.undo_all_pauses().unwrap(); + + assert_eq!(event, Event::PausesUndone); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.undo_all_pauses().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.undo_all_pauses().unwrap(); + + assert_eq!(event, Event::PausesUndone); + } +} + +mod set_current_comparison { + use super::*; + + #[test] + fn setting_existing_comparison_works() { + let mut timer = timer(); + + let event = timer + .set_current_comparison(comparison::personal_best::NAME) + .unwrap(); + + assert_eq!(event, Event::ComparisonChanged); + } + + #[test] + fn setting_non_existing_comparison_fails() { + let mut timer = timer(); + + let error = timer.set_current_comparison("non-existing").unwrap_err(); + + assert_eq!(error, Error::ComparisonDoesntExist); + } +} + +mod toggle_timing_method { + // Infallible +} + +mod set_current_timing_method { + // Infallible +} + +mod initialize_game_time { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.initialize_game_time().unwrap(); + + assert_eq!(event, Event::GameTimeInitialized); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.initialize_game_time().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn already_initialized_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.initialize_game_time().unwrap(); + let error = timer.initialize_game_time().unwrap_err(); + + assert_eq!(error, Error::GameTimeAlreadyInitialized); + } + + #[test] + fn finished_run_works() { + // FIXME: This behavior seems questionable. + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.initialize_game_time().unwrap(); + + assert_eq!(event, Event::GameTimeInitialized); + } +} + +mod set_game_time { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.set_game_time(TimeSpan::default()).unwrap(); + + assert_eq!(event, Event::GameTimeSet); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.set_game_time(TimeSpan::default()).unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_works() { + // FIXME: This behavior seems questionable. + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.set_game_time(TimeSpan::default()).unwrap(); + + assert_eq!(event, Event::GameTimeSet); + } +} + +mod pause_game_time { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.pause_game_time().unwrap(); + + assert_eq!(event, Event::GameTimePaused); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.pause_game_time().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn already_paused_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause_game_time().unwrap(); + let error = timer.pause_game_time().unwrap_err(); + + assert_eq!(error, Error::GameTimeAlreadyPaused); + } + + #[test] + fn finished_run_works() { + // FIXME: This behavior seems questionable. + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.pause_game_time().unwrap(); + + assert_eq!(event, Event::GameTimePaused); + } +} + +mod resume_game_time { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + timer.pause_game_time().unwrap(); + let event = timer.resume_game_time().unwrap(); + + assert_eq!(event, Event::GameTimeResumed); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.resume_game_time().unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn not_paused_fails() { + let mut timer = timer(); + + timer.start().unwrap(); + let error = timer.resume_game_time().unwrap_err(); + + assert_eq!(error, Error::GameTimeNotPaused); + } + + #[test] + fn finished_run_works() { + // FIXME: This behavior seems questionable. + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.pause_game_time().unwrap(); + let event = timer.resume_game_time().unwrap(); + + assert_eq!(event, Event::GameTimeResumed); + } +} + +mod set_loading_times { + use super::*; + + #[test] + fn works() { + let mut timer = timer(); + + timer.start().unwrap(); + let event = timer.set_loading_times(TimeSpan::default()).unwrap(); + + assert_eq!(event, Event::LoadingTimesSet); + } + + #[test] + fn without_a_run_fails() { + let mut timer = timer(); + + let error = timer.set_loading_times(TimeSpan::default()).unwrap_err(); + + assert_eq!(error, Error::NoRunInProgress); + } + + #[test] + fn finished_run_works() { + // FIXME: This behavior seems questionable. + let mut timer = timer(); + + timer.start().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + timer.split().unwrap(); + let event = timer.set_loading_times(TimeSpan::default()).unwrap(); + + assert_eq!(event, Event::LoadingTimesSet); + } +} + +mod set_custom_variable { + // Infallible +} diff --git a/src/timing/timer/tests/mark_as_modified.rs b/src/timing/timer/tests/mark_as_modified.rs index a2a146c7..3ad37e4f 100644 --- a/src/timing/timer/tests/mark_as_modified.rs +++ b/src/timing/timer/tests/mark_as_modified.rs @@ -1,5 +1,6 @@ use crate::{Run, Segment, TimeSpan, Timer, TimingMethod}; +#[track_caller] fn timer() -> Timer { use super::run; let mut run = run(); @@ -11,6 +12,15 @@ fn timer() -> Timer { timer } +#[track_caller] +fn started_but_unmodified_timer() -> Timer { + let mut timer = timer(); + timer.start().unwrap(); + timer.mark_as_unmodified(); + assert!(!timer.run().has_been_modified()); + timer +} + #[test] fn not_when_replacing_run() { let mut timer = timer(); @@ -47,93 +57,93 @@ fn not_when_setting_current_timing_method() { #[test] fn when_starting_the_timer() { let mut timer = timer(); - timer.start(); + timer.start().unwrap(); assert!(timer.run().has_been_modified()); } #[test] fn not_when_splitting_without_an_attempt() { let mut timer = timer(); - timer.split(); + timer.split().unwrap_err(); assert!(!timer.run().has_been_modified()); } #[test] fn when_splitting_or_starting_the_timer() { let mut timer = timer(); - timer.split_or_start(); + timer.split_or_start().unwrap(); assert!(timer.run().has_been_modified()); } #[test] fn not_when_skipping_a_split_without_an_attempt() { let mut timer = timer(); - timer.skip_split(); + timer.skip_split().unwrap_err(); assert!(!timer.run().has_been_modified()); } #[test] fn not_when_undoing_a_split_without_an_attempt() { let mut timer = timer(); - timer.undo_split(); + timer.undo_split().unwrap_err(); assert!(!timer.run().has_been_modified()); } #[test] fn when_starting_and_resetting_with_update_splits() { let mut timer = timer(); - timer.start(); - timer.reset(true); + timer.start().unwrap(); + timer.reset(true).unwrap(); assert!(timer.run().has_been_modified()); } #[test] fn when_starting_and_resetting_without_update_splits() { let mut timer = timer(); - timer.start(); - timer.reset(false); + timer.start().unwrap(); + timer.reset(false).unwrap(); assert!(timer.run().has_been_modified()); } #[test] fn not_when_resetting_without_an_attempt() { let mut timer = timer(); - timer.reset(true); + timer.reset(true).unwrap_err(); assert!(!timer.run().has_been_modified()); } #[test] fn not_when_pausing_without_an_attempt() { let mut timer = timer(); - timer.pause(); + timer.pause().unwrap_err(); assert!(!timer.run().has_been_modified()); } #[test] fn not_when_resuming_without_an_attempt() { let mut timer = timer(); - timer.resume(); + timer.resume().unwrap_err(); assert!(!timer.run().has_been_modified()); } #[test] fn not_when_toggling_pause_without_an_attempt() { let mut timer = timer(); - timer.toggle_pause(); + timer.toggle_pause().unwrap_err(); assert!(!timer.run().has_been_modified()); } #[test] fn when_toggling_pause_or_start() { let mut timer = timer(); - timer.toggle_pause_or_start(); + timer.toggle_pause_or_start().unwrap(); assert!(timer.run().has_been_modified()); } #[test] fn not_when_undoing_all_pauses_without_an_attempt() { let mut timer = timer(); - timer.undo_all_pauses(); + timer.undo_all_pauses().unwrap_err(); assert!(!timer.run().has_been_modified()); } @@ -148,8 +158,8 @@ fn not_when_switching_comparisons() { #[test] fn not_when_initializing_or_deinitializing_game_time() { - let mut timer = timer(); - timer.initialize_game_time(); + let mut timer = started_but_unmodified_timer(); + timer.initialize_game_time().unwrap(); assert!(!timer.run().has_been_modified()); timer.deinitialize_game_time(); assert!(!timer.run().has_been_modified()); @@ -158,13 +168,13 @@ fn not_when_initializing_or_deinitializing_game_time() { #[test] fn not_when_pausing_resuming_or_setting_game_time_without_an_attempt() { let mut timer = timer(); - timer.pause_game_time(); + timer.pause_game_time().unwrap_err(); assert!(!timer.run().has_been_modified()); - timer.resume_game_time(); + timer.resume_game_time().unwrap_err(); assert!(!timer.run().has_been_modified()); - timer.set_game_time(TimeSpan::default()); + timer.set_game_time(TimeSpan::default()).unwrap_err(); assert!(!timer.run().has_been_modified()); - timer.set_loading_times(TimeSpan::default()); + timer.set_loading_times(TimeSpan::default()).unwrap_err(); assert!(!timer.run().has_been_modified()); } diff --git a/src/timing/timer/tests/mod.rs b/src/timing/timer/tests/mod.rs index 7a58460c..35fe0a36 100644 --- a/src/timing/timer/tests/mod.rs +++ b/src/timing/timer/tests/mod.rs @@ -6,6 +6,7 @@ use crate::{ Run, Segment, TimeSpan, Timer, TimerPhase, TimingMethod, }; +mod events; mod mark_as_modified; mod variables; @@ -490,8 +491,6 @@ fn clears_run_id_when_pbing() { ], ); - timer.reset(true); - assert_eq!(timer.run().metadata().run_id(), ""); } @@ -501,7 +500,7 @@ fn reset_and_set_attempt_as_pb() { // Call it for the phase NotRunning assert_eq!(timer.current_phase(), TimerPhase::NotRunning); - timer.reset_and_set_attempt_as_pb(); + timer.reset_and_set_attempt_as_pb().unwrap_err(); for segment in timer.run().segments() { assert_eq!(segment.personal_best_split_time().game_time, None); } @@ -509,7 +508,7 @@ fn reset_and_set_attempt_as_pb() { // Call it for the phase Running, but don't do any splits yet start_run(&mut timer); assert_eq!(timer.current_phase(), TimerPhase::Running); - timer.reset_and_set_attempt_as_pb(); + timer.reset_and_set_attempt_as_pb().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::NotRunning); for segment in timer.run().segments() { assert_eq!(segment.personal_best_split_time().game_time, None); @@ -517,9 +516,9 @@ fn reset_and_set_attempt_as_pb() { // Call it for the phase Paused, but don't do any splits yet start_run(&mut timer); - timer.pause(); + timer.pause().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::Paused); - timer.reset_and_set_attempt_as_pb(); + timer.reset_and_set_attempt_as_pb().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::NotRunning); for segment in timer.run().segments() { assert_eq!(segment.personal_best_split_time().game_time, None); @@ -528,13 +527,13 @@ fn reset_and_set_attempt_as_pb() { // Call it for the phase Running, this time with splits start_run(&mut timer); let first = TimeSpan::from_seconds(1.0); - timer.set_game_time(first); - timer.split(); + timer.set_game_time(first).unwrap(); + timer.split().unwrap(); let second = TimeSpan::from_seconds(2.0); - timer.set_game_time(second); - timer.split(); + timer.set_game_time(second).unwrap(); + timer.split().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::Running); - timer.reset_and_set_attempt_as_pb(); + timer.reset_and_set_attempt_as_pb().unwrap(); assert_eq!( timer.run().segment(0).personal_best_split_time().game_time, Some(first) @@ -551,16 +550,16 @@ fn reset_and_set_attempt_as_pb() { // Call it for the phase Ended start_run(&mut timer); let first = TimeSpan::from_seconds(4.0); - timer.set_game_time(first); - timer.split(); + timer.set_game_time(first).unwrap(); + timer.split().unwrap(); let second = TimeSpan::from_seconds(5.0); - timer.set_game_time(second); - timer.split(); + timer.set_game_time(second).unwrap(); + timer.split().unwrap(); let third = TimeSpan::from_seconds(6.0); - timer.set_game_time(third); - timer.split(); + timer.set_game_time(third).unwrap(); + timer.split().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::Ended); - timer.reset_and_set_attempt_as_pb(); + timer.reset_and_set_attempt_as_pb().unwrap(); assert_eq!( timer.run().segment(0).personal_best_split_time().game_time, Some(first) @@ -579,16 +578,16 @@ fn reset_and_set_attempt_as_pb() { // Call it for the phase Ended, this time with slower splits start_run(&mut timer); let first = TimeSpan::from_seconds(14.0); - timer.set_game_time(first); - timer.split(); + timer.set_game_time(first).unwrap(); + timer.split().unwrap(); let second = TimeSpan::from_seconds(15.0); - timer.set_game_time(second); - timer.split(); + timer.set_game_time(second).unwrap(); + timer.split().unwrap(); let third = TimeSpan::from_seconds(16.0); - timer.set_game_time(third); - timer.split(); + timer.set_game_time(third).unwrap(); + timer.split().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::Ended); - timer.reset_and_set_attempt_as_pb(); + timer.reset_and_set_attempt_as_pb().unwrap(); assert_eq!( timer.run().segment(0).personal_best_split_time().game_time, Some(first) @@ -636,7 +635,7 @@ fn identifies_new_best_times_in_current_attempt() { make_progress_run_with_splits_opt(&mut timer, &[Some(5.0), Some(10.0), Some(13.0)]); assert!(!timer.current_attempt_has_new_personal_best(TimingMethod::GameTime)); assert!(timer.current_attempt_has_new_best_segments(TimingMethod::GameTime)); - timer.reset(true); + timer.reset(true).unwrap(); // Always false between attempts assert!(!timer.current_attempt_has_new_best_times()); @@ -647,7 +646,7 @@ fn identifies_new_best_times_in_current_attempt() { make_progress_run_with_splits_opt(&mut timer, &[Some(7.0), Some(9.0), Some(11.0)]); assert!(timer.current_attempt_has_new_personal_best(TimingMethod::GameTime)); assert!(!timer.current_attempt_has_new_best_segments(TimingMethod::GameTime)); - timer.reset(true); + timer.reset(true).unwrap(); // Always false between attempts assert!(!timer.current_attempt_has_new_best_times()); @@ -664,13 +663,13 @@ fn identifies_new_best_times_in_current_attempt() { fn skipping_keeps_timer_paused() { let mut timer = timer(); start_run(&mut timer); - timer.pause(); + timer.pause().unwrap(); - timer.skip_split(); + timer.skip_split().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::Paused); assert_eq!(timer.current_split_index(), Some(1)); - timer.undo_split(); + timer.undo_split().unwrap(); assert_eq!(timer.current_phase(), TimerPhase::Paused); assert_eq!(timer.current_split_index(), Some(0)); } diff --git a/src/timing/timer/tests/variables.rs b/src/timing/timer/tests/variables.rs index 5d6904f3..81ef2df0 100644 --- a/src/timing/timer/tests/variables.rs +++ b/src/timing/timer/tests/variables.rs @@ -16,7 +16,7 @@ fn can_set_variable() { "10" ); - timer.start(); + timer.start().unwrap(); assert_eq!( timer @@ -49,7 +49,7 @@ fn can_set_variable() { "30" ); - timer.split(); + timer.split().unwrap(); assert_eq!( timer @@ -82,7 +82,7 @@ fn can_set_variable() { "50" ); - timer.reset(true); + timer.reset(true).unwrap(); assert_eq!( timer diff --git a/src/timing/timer_phase.rs b/src/timing/timer_phase.rs index 612532a5..dced61b7 100644 --- a/src/timing/timer_phase.rs +++ b/src/timing/timer_phase.rs @@ -2,7 +2,7 @@ use crate::TimingMethod; /// Describes which phase the timer is currently in. This tells you if there's /// an active speedrun attempt and whether it is paused or it ended. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)] #[repr(u8)] pub enum TimerPhase { /// There's currently no active attempt. diff --git a/src/util/tests_helper.rs b/src/util/tests_helper.rs index ec92179e..074b30ad 100644 --- a/src/util/tests_helper.rs +++ b/src/util/tests_helper.rs @@ -2,6 +2,7 @@ use crate::{Run, Segment, TimeSpan, Timer, TimingMethod}; +#[track_caller] pub fn create_run(names: &[&str]) -> Run { let mut run = Run::new(); for &name in names { @@ -10,48 +11,54 @@ pub fn create_run(names: &[&str]) -> Run { run } +#[track_caller] pub fn create_timer(names: &[&str]) -> Timer { Timer::new(create_run(names)).unwrap() } +#[track_caller] pub fn start_run(timer: &mut Timer) { timer.set_current_timing_method(TimingMethod::GameTime); - timer.start(); - timer.initialize_game_time(); - timer.pause_game_time(); - timer.set_game_time(TimeSpan::zero()); + timer.start().unwrap(); + timer.initialize_game_time().unwrap(); + timer.pause_game_time().unwrap(); + timer.set_game_time(TimeSpan::zero()).unwrap(); } +#[track_caller] pub fn run_with_splits(timer: &mut Timer, splits: &[f64]) { start_run(timer); for &split in splits { - timer.set_game_time(TimeSpan::from_seconds(split)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(split)).unwrap(); + timer.split().unwrap(); } - timer.reset(true); + timer.reset(true).unwrap(); } /// Same as run_with_splits_opt, but progresses an already active attempt and /// doesn't reset it. Useful for checking intermediate states. +#[track_caller] pub fn make_progress_run_with_splits_opt(timer: &mut Timer, splits: &[Option]) { for &split in splits { if let Some(split) = split { - timer.set_game_time(TimeSpan::from_seconds(split)); - timer.split(); + timer.set_game_time(TimeSpan::from_seconds(split)).unwrap(); + timer.split().unwrap(); } else { - timer.skip_split(); + timer.skip_split().unwrap(); } } } +#[track_caller] pub fn run_with_splits_opt(timer: &mut Timer, splits: &[Option]) { start_run(timer); make_progress_run_with_splits_opt(timer, splits); - timer.reset(true); + timer.reset(true).unwrap(); } +#[track_caller] pub fn span(seconds: f64) -> TimeSpan { TimeSpan::from_seconds(seconds) } diff --git a/tests/rendering.rs b/tests/rendering.rs index 4400d82e..ad5f42cd 100644 --- a/tests/rendering.rs +++ b/tests/rendering.rs @@ -180,8 +180,8 @@ fn actual_split_file() { check( &layout.state(&mut image_cache, &timer.snapshot()), &image_cache, - "86ccc8595787d41b", - "ec4e4283ff1aaf7c", + "42b2292f5c884c17", + "1e4c66359bdc32e8", "actual_split_file", ); } @@ -217,19 +217,19 @@ fn timer_delta_background() { &layout.state(&mut image_cache, &timer.snapshot()), &image_cache, [250, 300], - "fc8e7890593f9da6", - "0140697763078566", + "4d9c5c580b0c435f", + "bf8e5c596684688d", "timer_delta_background_ahead", ); - timer.reset(true); + timer.reset(true).unwrap(); check_dims( &layout.state(&mut image_cache, &timer.snapshot()), &image_cache, [250, 300], - "c56d1f6715627391", - "75b3c2a49c0f0b93", + "e96525c84aa77f66", + "bd8139d0f625af38", "timer_delta_background_stopped", ); } diff --git a/tests/run_files/livesplit1.0.lss b/tests/run_files/livesplit1.0.lss index 24988299..be2f1172 100644 --- a/tests/run_files/livesplit1.0.lss +++ b/tests/run_files/livesplit1.0.lss @@ -3,7 +3,7 @@ Pac-Man World 3 Any% - -00:00:10 + 00:00:00 3