From 3c3762bec27c1774e28384216281c7ed2d760250 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Mon, 15 Jul 2024 19:52:02 +0200 Subject: [PATCH] Expose more of the Server Protocol (#827) This exposes more of the Server Protocol publicly, so it can also be used as a client. --- capi/src/command_sink.rs | 19 ++- capi/src/web_command_sink.rs | 17 +- src/auto_splitting/mod.rs | 2 +- src/component/graph.rs | 2 +- src/event.rs | 44 +++-- src/networking/server_protocol.rs | 270 ++++++++++++++++++++++++------ src/timing/timer/mod.rs | 2 +- 7 files changed, 269 insertions(+), 87 deletions(-) diff --git a/capi/src/command_sink.rs b/capi/src/command_sink.rs index 225240bc..facb4361 100644 --- a/capi/src/command_sink.rs +++ b/capi/src/command_sink.rs @@ -8,7 +8,7 @@ //! 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 std::{borrow::Cow, future::Future, ops::Deref, pin::Pin, sync::Arc}; use livesplit_core::{ event::{self, Result}, @@ -50,7 +50,7 @@ pub(crate) trait CommandSinkAndQuery: Send + Sync + 'static { 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_set_current_comparison(&self, comparison: Cow<'_, 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; @@ -58,7 +58,7 @@ pub(crate) trait CommandSinkAndQuery: Send + Sync + 'static { 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; + fn dyn_set_custom_variable(&self, name: Cow<'_, str>, value: Cow<'_, str>) -> Fut; } type Fut = Pin + 'static>>; @@ -107,7 +107,7 @@ where fn dyn_switch_to_next_comparison(&self) -> Fut { Box::pin(self.switch_to_next_comparison()) } - fn dyn_set_current_comparison(&self, comparison: &str) -> Fut { + fn dyn_set_current_comparison(&self, comparison: Cow<'_, str>) -> Fut { Box::pin(self.set_current_comparison(comparison)) } fn dyn_toggle_timing_method(&self) -> Fut { @@ -131,7 +131,7 @@ where 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 { + fn dyn_set_custom_variable(&self, name: Cow<'_, str>, value: Cow<'_, str>) -> Fut { Box::pin(self.set_custom_variable(name, value)) } } @@ -185,7 +185,10 @@ impl event::CommandSink for CommandSink { self.0.dyn_switch_to_next_comparison() } - fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + fn set_current_comparison( + &self, + comparison: Cow<'_, str>, + ) -> impl Future + 'static { self.0.dyn_set_current_comparison(comparison) } @@ -222,8 +225,8 @@ impl event::CommandSink for CommandSink { fn set_custom_variable( &self, - name: &str, - value: &str, + name: Cow<'_, str>, + value: Cow<'_, str>, ) -> impl Future + 'static { self.0.dyn_set_custom_variable(name, value) } diff --git a/capi/src/web_command_sink.rs b/capi/src/web_command_sink.rs index 96b85731..2dc3a28e 100644 --- a/capi/src/web_command_sink.rs +++ b/capi/src/web_command_sink.rs @@ -3,7 +3,7 @@ //! timer commands. All of them are optional except for `getTimer`. use core::ptr; -use std::{cell::Cell, convert::TryFrom, future::Future, sync::Arc}; +use std::{borrow::Cow, cell::Cell, convert::TryFrom, future::Future, sync::Arc}; use livesplit_core::{ event::{CommandSink, Error, Event, Result, TimerQuery}, @@ -221,12 +221,15 @@ impl CommandSink for WebCommandSink { ) } - fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + fn set_current_comparison( + &self, + comparison: Cow<'_, 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()), + .and_then(|f| f.call1(&self.obj, &JsValue::from_str(&comparison)).ok()), ) } @@ -301,15 +304,15 @@ impl CommandSink for WebCommandSink { fn set_custom_variable( &self, - name: &str, - value: &str, + name: Cow<'_, str>, + value: Cow<'_, 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), + &JsValue::from_str(&name), + &JsValue::from_str(&value), ) .ok() })) diff --git a/src/auto_splitting/mod.rs b/src/auto_splitting/mod.rs index 32b55701..e73770ce 100644 --- a/src/auto_splitting/mod.rs +++ b/src/auto_splitting/mod.rs @@ -761,7 +761,7 @@ impl AutoSplitTimer for Timer { } fn set_variable(&mut self, name: &str, value: &str) { - drop(self.0.set_custom_variable(name, value)); + drop(self.0.set_custom_variable(name.into(), value.into())); } fn log(&mut self, message: fmt::Arguments<'_>) { diff --git a/src/component/graph.rs b/src/component/graph.rs index e1426b45..022555ab 100644 --- a/src/component/graph.rs +++ b/src/component/graph.rs @@ -3,7 +3,7 @@ //! the chosen comparison throughout the whole attempt. Every point of the graph //! represents a split. Its x-coordinate is proportional to the split time and //! its y-coordinate is proportional to the split delta. The entire diagram is -//! refered to as the chart and it contains the graph. The x-axis is the +//! referred to as the chart and it contains the graph. The x-axis is the //! horizontal line that separates positive deltas from negative ones. // The words "padding" and "content" are from the CSS box model. "Padding" is an diff --git a/src/event.rs b/src/event.rs index 4e2db1e2..7c434912 100644 --- a/src/event.rs +++ b/src/event.rs @@ -11,7 +11,7 @@ use core::{future::Future, ops::Deref}; -use alloc::sync::Arc; +use alloc::{borrow::Cow, sync::Arc}; use crate::{TimeSpan, Timer, TimingMethod}; @@ -59,13 +59,14 @@ pub enum Event { LoadingTimesSet = 16, /// A custom variable has been set. CustomVariableSet = 17, + /// An unknown event occurred. + #[serde(other)] + Unknown, } -impl TryFrom for Event { - type Error = (); - - fn try_from(value: u32) -> Result { - Ok(match value { +impl From for Event { + fn from(value: u32) -> Self { + match value { 0 => Event::Started, 1 => Event::Splitted, 2 => Event::Finished, @@ -84,8 +85,8 @@ impl TryFrom for Event { 15 => Event::GameTimeResumed, 16 => Event::LoadingTimesSet, 17 => Event::CustomVariableSet, - _ => return Err(()), - }) + _ => Event::Unknown, + } } } @@ -238,7 +239,10 @@ pub trait CommandSink { 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; + fn set_current_comparison( + &self, + comparison: Cow<'_, str>, + ) -> impl Future + 'static; /// Toggles between the `Real Time` and `Game Time` timing methods. fn toggle_timing_method(&self) -> impl Future + 'static; /// Sets the current timing method to the timing method provided. @@ -269,8 +273,8 @@ pub trait CommandSink { /// be stored in the splits file. fn set_custom_variable( &self, - name: &str, - value: &str, + name: Cow<'_, str>, + value: Cow<'_, str>, ) -> impl Future + 'static; } @@ -347,7 +351,10 @@ impl CommandSink for crate::SharedTimer { async { Ok(Event::ComparisonChanged) } } - fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + fn set_current_comparison( + &self, + comparison: Cow<'_, str>, + ) -> impl Future + 'static { let result = self.write().unwrap().set_current_comparison(comparison); async move { result } } @@ -392,8 +399,8 @@ impl CommandSink for crate::SharedTimer { fn set_custom_variable( &self, - name: &str, - value: &str, + name: Cow<'_, str>, + value: Cow<'_, str>, ) -> impl Future + 'static { self.write().unwrap().set_custom_variable(name, value); async { Ok(Event::CustomVariableSet) } @@ -457,7 +464,10 @@ impl CommandSink for Arc { CommandSink::switch_to_next_comparison(&**self) } - fn set_current_comparison(&self, comparison: &str) -> impl Future + 'static { + fn set_current_comparison( + &self, + comparison: Cow<'_, str>, + ) -> impl Future + 'static { CommandSink::set_current_comparison(&**self, comparison) } @@ -494,8 +504,8 @@ impl CommandSink for Arc { fn set_custom_variable( &self, - name: &str, - value: &str, + name: Cow<'_, str>, + value: Cow<'_, str>, ) -> impl Future + 'static { CommandSink::set_custom_variable(&**self, name, value) } diff --git a/src/networking/server_protocol.rs b/src/networking/server_protocol.rs index 0aa6ec46..a34fd48b 100644 --- a/src/networking/server_protocol.rs +++ b/src/networking/server_protocol.rs @@ -49,6 +49,9 @@ //! Keep in mind the experimental nature of the protocol. It will likely change //! a lot in the future. +use alloc::borrow::Cow; +use serde::Serializer; + use crate::{ event::{self, Event}, timing::formatter::{self, TimeFormatter, ASCII_MINUS}, @@ -60,7 +63,7 @@ pub async fn handle_command( command: &str, command_sink: &S, ) -> String { - let response = match serde_json::from_str::(command) { + 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(), @@ -96,63 +99,195 @@ struct IsEvent { event: Event, } -#[derive(serde_derive::Deserialize)] +fn serialize_time_span( + time_span: &TimeSpan, + serializer: S, +) -> Result { + let (secs, nanos) = time_span.to_seconds_and_subsec_nanoseconds(); + serializer.collect_str(&format_args!("{secs}.{:09}", nanos.abs())) +} + +const fn is_false(v: &bool) -> bool { + !*v +} + +/// A command that can be sent to the timer. +#[derive(Clone, serde_derive::Serialize, serde_derive::Deserialize)] #[serde(tag = "command", rename_all = "camelCase")] -enum Command { - SplitOrStart, +pub enum Command<'a> { + /// Starts the timer if there is no attempt in progress. If that's not the + /// case, nothing happens. + 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. 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. + SplitOrStart, + /// 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. + #[serde(rename_all = "camelCase")] + Reset { + /// Whether to save the current attempt in the run's history. + #[serde(skip_serializing_if = "Option::is_none")] + save_attempt: Option, + }, + /// Removes the split time from the last split if an attempt is in progress + /// and there is a previous split. The Timer Phase also switches to + /// [`Running`](TimerPhase::Running) if it previously was + /// [`Ended`](TimerPhase::Ended). UndoSplit, + /// Skips the current split if an attempt is in progress and the current + /// split is not the last split. SkipSplit, + /// Toggles an active attempt between [`Paused`](TimerPhase::Paused) and + /// [`Running`](TimerPhase::Paused) or starts an attempt if there's none in + /// progress. + TogglePauseOrStart, + /// Pauses an active attempt that is not paused. Pause, + /// Resumes an attempt that is paused. Resume, - TogglePauseOrStart, - Reset, - Start, + /// Removes all the pause times from the current time. If the current + /// attempt is paused, it also resumes that attempt. Additionally, if the + /// attempt is finished, the final split time is adjusted to not include the + /// pause times as well. + /// + /// # Warning + /// + /// This behavior is not entirely optimal, as generally only the final split + /// time is modified, while all other split times are left unmodified, which + /// may not be what actually happened during the run. + UndoAllPauses, + /// Switches the current comparison to the previous comparison in the list. + SwitchToPreviousComparison, + /// Switches the current comparison to the next comparison in the list. + SwitchToNextComparison, + /// Tries to set the current comparison to the comparison specified. If the + /// comparison doesn't exist an error is returned. + SetCurrentComparison { + /// The name of the comparison. + #[serde(borrow)] + comparison: Cow<'a, str>, + }, + /// Toggles between the `Real Time` and `Game Time` timing methods. + ToggleTimingMethod, + /// Sets the current timing method to the timing method provided. + #[serde(rename_all = "camelCase")] + SetCurrentTimingMethod { + /// The timing method to use. + timing_method: TimingMethod, + }, + /// Initializes game time for the current attempt. Game time automatically + /// gets uninitialized for each new attempt. InitializeGameTime, + /// 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. SetGameTime { + /// The time to set the game time to. + #[serde(serialize_with = "serialize_time_span")] time: TimeSpan, }, + /// Pauses the game timer such that it doesn't automatically increment + /// similar to real time. + PauseGameTime, + /// Resumes the game timer such that it automatically increments similar to + /// real time, starting from the game time it was paused at. + ResumeGameTime, + /// 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. SetLoadingTimes { + /// The loading times to set the game time to. + #[serde(serialize_with = "serialize_time_span")] time: TimeSpan, }, - PauseGameTime, - ResumeGameTime, + /// 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. SetCustomVariable { - key: String, - value: String, - }, - SetCurrentComparison { - comparison: String, - }, - #[serde(rename_all = "camelCase")] - SetCurrentTimingMethod { - timing_method: TimingMethod, + /// The name of the custom variable. + #[serde(borrow)] + key: Cow<'a, str>, + /// The value of the custom variable. + #[serde(borrow)] + value: Cow<'a, str>, }, + + /// Returns the timer's current time. The Game Time is [`None`] if the Game + /// Time has not been initialized. #[serde(rename_all = "camelCase")] GetCurrentTime { + /// The timing method to retrieve the time for. + #[serde(skip_serializing_if = "Option::is_none")] timing_method: Option, }, + /// Returns the name of the segment with the specified index. If no index is + /// specified, the name of the current segment is returned. If the index is + /// out of bounds, an error is returned. If the index is negative, it is + /// treated as relative to the end of the segment list. If the `relative` + /// field is set to `true`, the index is treated as relative to the current + /// segment index. GetSegmentName { + /// The index of the segment. + #[serde(skip_serializing_if = "Option::is_none")] index: Option, - #[serde(default)] + /// Specifies whether the index is relative to the current segment + /// index. + #[serde(default, skip_serializing_if = "is_false")] relative: bool, }, + /// Returns the time of the comparison with the specified name for the + /// segment with the specified index. If the segment index is out of bounds, + /// an error is returned. If the segment index is negative, it is treated as + /// relative to the end of the segment list. If the `relative` field is set + /// to `true`, the index is treated as relative to the current segment + /// index. The current comparison is used if the comparison name is not + /// specified. The current timing method is used if the timing method is not + /// specified. #[serde(rename_all = "camelCase")] GetComparisonTime { + /// The index of the segment. + #[serde(skip_serializing_if = "Option::is_none")] index: Option, - #[serde(default)] + /// Specifies whether the index is relative to the current segment + /// index. + #[serde(default, skip_serializing_if = "is_false")] relative: bool, - comparison: Option, + /// The name of the comparison. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(borrow)] + comparison: Option>, + /// The timing method to retrieve the time for. + #[serde(skip_serializing_if = "Option::is_none")] timing_method: Option, }, + /// Returns the current split time of the segment with the specified index + /// and timing method. If the segment index is out of bounds, an error is + /// returned. If the segment index is negative, it is treated as relative to + /// the end of the segment list. If the `relative` field is set to `true`, + /// the index is treated as relative to the current segment index. The + /// current timing method is used if the timing method is not specified. #[serde(rename_all = "camelCase")] GetCurrentRunSplitTime { + /// The index of the segment. + #[serde(skip_serializing_if = "Option::is_none")] index: Option, - #[serde(default)] + /// Specifies whether the index is relative to the current segment + /// index. + #[serde(default, skip_serializing_if = "is_false")] relative: bool, + /// The timing method to retrieve the time for. + #[serde(skip_serializing_if = "Option::is_none")] timing_method: Option, }, + /// Returns the current timer phase and split index. GetCurrentState, + /// Pings the application to check whether it is still running. Ping, } @@ -192,20 +327,31 @@ impl Error { } } -impl Command { +impl Command<'_> { async fn handle( - &self, + self, command_sink: &E, ) -> Result { Ok(match self { - Command::SplitOrStart => { - command_sink.split_or_start().await.map_err(Error::timer)?; + Command::Start => { + command_sink.start().await.map_err(Error::timer)?; Response::None } Command::Split => { command_sink.split().await.map_err(Error::timer)?; Response::None } + Command::SplitOrStart => { + command_sink.split_or_start().await.map_err(Error::timer)?; + Response::None + } + Command::Reset { save_attempt } => { + command_sink + .reset(save_attempt) + .await + .map_err(Error::timer)?; + Response::None + } Command::UndoSplit => { command_sink.undo_split().await.map_err(Error::timer)?; Response::None @@ -214,6 +360,13 @@ impl Command { command_sink.skip_split().await.map_err(Error::timer)?; Response::None } + Command::TogglePauseOrStart => { + command_sink + .toggle_pause_or_start() + .await + .map_err(Error::timer)?; + Response::None + } Command::Pause => { command_sink.pause().await.map_err(Error::timer)?; Response::None @@ -222,38 +375,55 @@ impl Command { command_sink.resume().await.map_err(Error::timer)?; Response::None } - Command::TogglePauseOrStart => { + Command::UndoAllPauses => { + command_sink.undo_all_pauses().await.map_err(Error::timer)?; + Response::None + } + Command::SwitchToPreviousComparison => { command_sink - .toggle_pause_or_start() + .switch_to_previous_comparison() .await .map_err(Error::timer)?; Response::None } - Command::Reset => { - command_sink.reset(None).await.map_err(Error::timer)?; + Command::SwitchToNextComparison => { + command_sink + .switch_to_next_comparison() + .await + .map_err(Error::timer)?; Response::None } - Command::Start => { - command_sink.start().await.map_err(Error::timer)?; + Command::SetCurrentComparison { comparison } => { + command_sink + .set_current_comparison(comparison) + .await + .map_err(Error::timer)?; Response::None } - Command::InitializeGameTime => { + Command::ToggleTimingMethod => { command_sink - .initialize_game_time() + .toggle_timing_method() .await .map_err(Error::timer)?; Response::None } - Command::SetGameTime { time } => { + Command::SetCurrentTimingMethod { timing_method } => { command_sink - .set_game_time(*time) + .set_current_timing_method(timing_method) .await .map_err(Error::timer)?; Response::None } - Command::SetLoadingTimes { time } => { + Command::InitializeGameTime => { command_sink - .set_loading_times(*time) + .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 @@ -269,27 +439,21 @@ impl Command { .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::SetLoadingTimes { time } => { command_sink - .set_current_comparison(comparison) + .set_loading_times(time) .await .map_err(Error::timer)?; Response::None } - Command::SetCurrentTimingMethod { timing_method } => { + Command::SetCustomVariable { key, value } => { command_sink - .set_current_timing_method(*timing_method) + .set_custom_variable(key, value) .await .map_err(Error::timer)?; Response::None } + Command::GetCurrentTime { timing_method } => { let guard = command_sink.get_timer(); let timer = &*guard; @@ -305,7 +469,7 @@ impl Command { Command::GetSegmentName { index, relative } => { let guard = command_sink.get_timer(); let timer = &*guard; - let index = resolve_index(timer, *index, *relative)?; + let index = resolve_index(timer, index, relative)?; Response::String(timer.run().segment(index).name().into()) } Command::GetComparisonTime { @@ -316,7 +480,7 @@ impl Command { } => { let guard = command_sink.get_timer(); let timer = &*guard; - let index = resolve_index(timer, *index, *relative)?; + 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()); @@ -335,7 +499,7 @@ impl Command { } => { let guard = command_sink.get_timer(); let timer = &*guard; - let index = resolve_index(timer, *index, *relative)?; + 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]; @@ -384,6 +548,8 @@ fn resolve_index(timer: &Timer, index: Option, relative: bool) -> Result< } fn format_time(time: TimeSpan) -> String { + // FIXME: I don't think we can parse it again if days are included. Let's not + // use this formatter. formatter::none_wrapper::NoneWrapper::new(formatter::Complete::new(), ASCII_MINUS) .format(time) .to_string() diff --git a/src/timing/timer/mod.rs b/src/timing/timer/mod.rs index d2018bed..3ae72cf8 100644 --- a/src/timing/timer/mod.rs +++ b/src/timing/timer/mod.rs @@ -66,7 +66,7 @@ pub struct Snapshot<'timer> { impl Snapshot<'_> { /// Returns the time the timer was at when the snapshot was taken. The Game - /// Time is None if the Game Time has not been initialized. + /// Time is [`None`] if the Game Time has not been initialized. pub const fn current_time(&self) -> Time { self.time }