From 468e64bfbae7088ecc14477ecfa39994ff5b21a5 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Sun, 27 Nov 2022 22:29:51 +0100 Subject: [PATCH] Allow the auto splitters to add settings The auto splitters can now specify settings that are meant to be shown to and modified by the user. For now there are only boolean values. Also to keep things simple, the auto splitters are reloaded when the settings are changed. When registering the settings their values are returned, so there doesn't need to be a separate "reload" API for now. This however means that whenever there is such a reloading API, the auto splitters probably would have to get updated to properly get notified. This is acceptable breakage for such an early API though. --- crates/livesplit-auto-splitting/src/lib.rs | 12 ++++ .../livesplit-auto-splitting/src/runtime.rs | 65 +++++++++++++++++-- .../livesplit-auto-splitting/src/settings.rs | 57 ++++++++++++++++ .../tests/sandboxing.rs | 12 ++-- src/auto_splitting/mod.rs | 37 ++++++++--- src/run/parser/livesplit.rs | 4 +- 6 files changed, 166 insertions(+), 21 deletions(-) create mode 100644 crates/livesplit-auto-splitting/src/settings.rs diff --git a/crates/livesplit-auto-splitting/src/lib.rs b/crates/livesplit-auto-splitting/src/lib.rs index f8386065..133f3ad4 100644 --- a/crates/livesplit-auto-splitting/src/lib.rs +++ b/crates/livesplit-auto-splitting/src/lib.rs @@ -112,6 +112,16 @@ //! pub fn runtime_set_tick_rate(ticks_per_second: f64); //! /// Prints a log message for debugging purposes. //! pub fn runtime_print_message(text_ptr: *const u8, text_len: usize); +//! +//! /// Adds a new setting that the user can modify. This will return either +//! /// the specified default value or the value that the user has set. +//! pub fn user_settings_add_bool( +//! key_ptr: *const u8, +//! key_len: usize, +//! description_ptr: *const u8, +//! description_len: usize, +//! default_value: bool, +//! ) -> bool; //! } //! ``` //! @@ -130,8 +140,10 @@ mod process; mod runtime; +mod settings; mod timer; pub use runtime::{CreationError, InterruptHandle, RunError, Runtime}; +pub use settings::{SettingValue, SettingsStore, UserSetting}; pub use time; pub use timer::{Timer, TimerState}; diff --git a/crates/livesplit-auto-splitting/src/runtime.rs b/crates/livesplit-auto-splitting/src/runtime.rs index ce58e5b2..cedc2285 100644 --- a/crates/livesplit-auto-splitting/src/runtime.rs +++ b/crates/livesplit-auto-splitting/src/runtime.rs @@ -1,9 +1,8 @@ -use crate::{process::Process, timer::Timer}; +use crate::{process::Process, settings::UserSetting, timer::Timer, SettingValue, SettingsStore}; use slotmap::{Key, KeyData, SlotMap}; use snafu::{ResultExt, Snafu}; use std::{ - path::Path, result, str, time::{Duration, Instant}, }; @@ -83,6 +82,8 @@ slotmap::new_key_type! { pub struct Context { tick_rate: Duration, processes: SlotMap, + user_settings: Vec, + settings_store: SettingsStore, timer: T, memory: Option, process_list: ProcessList, @@ -147,21 +148,28 @@ pub struct Runtime { impl Runtime { /// Creates a new runtime with the given path to the WebAssembly module and /// the timer that the module then controls. - pub fn new(path: &Path, timer: T) -> Result { + pub fn new( + module: &[u8], + timer: T, + settings_store: SettingsStore, + ) -> Result { let engine = Engine::new( Config::new() .cranelift_opt_level(OptLevel::Speed) - .epoch_interruption(true), + .epoch_interruption(true) + .debug_info(true), ) .context(EngineCreation)?; - let module = Module::from_file(&engine, path).context(ModuleLoading)?; + let module = Module::from_binary(&engine, module).context(ModuleLoading)?; let mut store = Store::new( &engine, Context { processes: SlotMap::with_key(), - tick_rate: Duration::from_secs(1) / 120, + user_settings: Vec::new(), + settings_store, + tick_rate: Duration::new(0, 1_000_000_000 / 120), timer, memory: None, process_list: ProcessList::new(), @@ -216,10 +224,21 @@ impl Runtime { /// Runs the exported `update` function of the WebAssembly module a single /// time and returns the duration to wait until the next execution. The auto /// splitter can change this tick rate. It is 120Hz by default. - pub fn step(&mut self) -> Result { + pub fn update(&mut self) -> Result { self.update.call(&mut self.store, ()).context(RunUpdate)?; Ok(self.store.data().tick_rate) } + + /// Accesses the currently stored settings. + pub fn settings_store(&self) -> &SettingsStore { + &self.store.data().settings_store + } + + /// Accesses all the settings that are meant to be shown to and modified by + /// the user. + pub fn user_settings(&self) -> &[UserSetting] { + &self.store.data().user_settings + } } fn bind_interface(linker: &mut Linker>) -> Result<(), CreationError> { @@ -419,6 +438,38 @@ fn bind_interface(linker: &mut Linker>) -> Result<(), Creat }) .context(LinkFunction { name: "process_read", + })? + .func_wrap("env", "user_settings_add_bool", { + |mut caller: Caller<'_, Context>, + key_ptr: u32, + key_len: u32, + description_ptr: u32, + description_len: u32, + default_value: u32| { + let (memory, context) = memory_and_context(&mut caller); + let key = Box::::from(read_str(memory, key_ptr, key_len)?); + let description = read_str(memory, description_ptr, description_len)?.into(); + let default_value = default_value != 0; + let value_in_store = match context.settings_store.get(&key) { + Some(SettingValue::Bool(v)) => *v, + None => { + // TODO: Should this auto insert into the store? + context + .settings_store + .set(key.clone(), SettingValue::Bool(default_value)); + default_value + } + }; + context.user_settings.push(UserSetting { + key, + description, + default_value: SettingValue::Bool(default_value), + }); + Ok(value_in_store as u32) + } + }) + .context(LinkFunction { + name: "user_settings_add_bool", })?; Ok(()) } diff --git a/crates/livesplit-auto-splitting/src/settings.rs b/crates/livesplit-auto-splitting/src/settings.rs new file mode 100644 index 00000000..417cd4f8 --- /dev/null +++ b/crates/livesplit-auto-splitting/src/settings.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; + +/// A setting that is meant to be shown to and modified by the user. +#[non_exhaustive] +pub struct UserSetting { + /// A unique identifier for this setting. This is not meant to be shown to + /// the user and is only used to keep track of the setting. + pub key: Box, + /// The name of the setting that is shown to the user. + pub description: Box, + /// The default value of the setting. This also specifies the type of the setting. + pub default_value: SettingValue, +} + +/// A value that a setting can have. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum SettingValue { + /// A boolean value. + Bool(bool), +} + +/// Stores all the settings of an auto splitter. Currently this only stores +/// values that are modified. So there may be settings that are registered as +/// user settings, but because the user didn't modify them, they are not stored +/// here yet. +#[derive(Clone, Default)] +pub struct SettingsStore { + values: HashMap, SettingValue>, +} + +impl SettingsStore { + /// Creates a new empty settings store. + pub fn new() -> Self { + Self::default() + } + + /// Sets a setting to the new value. If the key of the setting doesn't exist + /// yet it will be stored as a new value. Otherwise the value will be + /// updated. + pub fn set(&mut self, key: Box, value: SettingValue) { + self.values.insert(key, value); + } + + /// Accesses the value of a setting by its key. While the setting may exist + /// as part of the user settings, it may not have been stored into the + /// settings store yet, so it may not exist, despite being registered. + pub fn get(&self, key: &str) -> Option<&SettingValue> { + self.values.get(key) + } + + /// Iterates over all the setting keys and their values in the settings + /// store. + pub fn iter(&self) -> impl Iterator { + self.values.iter().map(|(k, v)| (k.as_ref(), v)) + } +} diff --git a/crates/livesplit-auto-splitting/tests/sandboxing.rs b/crates/livesplit-auto-splitting/tests/sandboxing.rs index e0f6be1c..210b3dcb 100644 --- a/crates/livesplit-auto-splitting/tests/sandboxing.rs +++ b/crates/livesplit-auto-splitting/tests/sandboxing.rs @@ -1,6 +1,6 @@ #![cfg(feature = "unstable")] -use livesplit_auto_splitting::{Runtime, Timer, TimerState}; +use livesplit_auto_splitting::{Runtime, SettingsStore, Timer, TimerState}; use std::{ ffi::OsStr, fmt, fs, @@ -61,12 +61,16 @@ fn compile(crate_name: &str) -> anyhow::Result> { }) .unwrap(); - Ok(Runtime::new(&wasm_path, DummyTimer)?) + Ok(Runtime::new( + &std::fs::read(wasm_path).unwrap(), + DummyTimer, + SettingsStore::new(), + )?) } fn run(crate_name: &str) -> anyhow::Result<()> { let mut runtime = compile(crate_name)?; - runtime.step()?; + runtime.update()?; Ok(()) } @@ -144,7 +148,7 @@ fn infinite_loop() { interrupt.interrupt(); }); - assert!(runtime.step().is_err()); + assert!(runtime.update().is_err()); } // FIXME: Test Network diff --git a/src/auto_splitting/mod.rs b/src/auto_splitting/mod.rs index 0ad8af42..13416b08 100644 --- a/src/auto_splitting/mod.rs +++ b/src/auto_splitting/mod.rs @@ -1,6 +1,6 @@ //! The auto splitting module provides a runtime for running auto splitters that -//! can control the [`Timer`](crate::timing::Timer). These auto splitters are provided as WebAssembly -//! modules. +//! can control the [`Timer`](crate::timing::Timer). These auto splitters are +//! provided as WebAssembly modules. //! //! # Requirements for the Auto Splitters //! @@ -112,15 +112,26 @@ //! pub fn runtime_set_tick_rate(ticks_per_second: f64); //! /// Prints a log message for debugging purposes. //! pub fn runtime_print_message(text_ptr: *const u8, text_len: usize); +//! +//! /// Adds a new setting that the user can modify. This will return either +//! /// the specified default value or the value that the user has set. +//! pub fn user_settings_add_bool( +//! key_ptr: *const u8, +//! key_len: usize, +//! description_ptr: *const u8, +//! description_len: usize, +//! default_value: bool, +//! ) -> bool; //! } //! ``` use crate::timing::{SharedTimer, TimerPhase}; use livesplit_auto_splitting::{ - CreationError, InterruptHandle, Runtime as ScriptRuntime, Timer as AutoSplitTimer, TimerState, + CreationError, InterruptHandle, Runtime as ScriptRuntime, SettingsStore, + Timer as AutoSplitTimer, TimerState, }; use snafu::{ErrorCompat, Snafu}; -use std::{fmt, path::PathBuf, thread, time::Duration}; +use std::{fmt, fs, io, path::PathBuf, thread, time::Duration}; use tokio::{ runtime, sync::{mpsc, oneshot, watch}, @@ -137,6 +148,11 @@ pub enum Error { /// The underlying error. source: CreationError, }, + /// Failed reading the auto splitter file. + ReadFileFailed { + /// The underlying error. + source: io::Error, + }, } /// An auto splitter runtime that allows using an auto splitter provided as a @@ -198,6 +214,7 @@ impl Runtime { /// or failed. pub async fn load_script(&self, script: PathBuf) -> Result<(), Error> { let (sender, receiver) = oneshot::channel(); + let script = fs::read(script).map_err(|e| Error::ReadFileFailed { source: e })?; self.sender .send(Request::LoadScript(script, sender)) .map_err(|_| Error::ThreadStopped)?; @@ -243,7 +260,7 @@ impl Runtime { } enum Request { - LoadScript(PathBuf, oneshot::Sender>), + LoadScript(Vec, oneshot::Sender>), UnloadScript(oneshot::Sender<()>), } @@ -307,7 +324,7 @@ async fn run( let mut runtime = loop { match receiver.recv().await { Some(Request::LoadScript(script, ret)) => { - match ScriptRuntime::new(&script, Timer(timer.clone())) { + match ScriptRuntime::new(&script, Timer(timer.clone()), SettingsStore::new()) { Ok(r) => { ret.send(Ok(())).ok(); break r; @@ -336,7 +353,11 @@ async fn run( match timeout_at(next_step, receiver.recv()).await { Ok(Some(request)) => match request { Request::LoadScript(script, ret) => { - match ScriptRuntime::new(&script, Timer(timer.clone())) { + match ScriptRuntime::new( + &script, + Timer(timer.clone()), + SettingsStore::new(), + ) { Ok(r) => { ret.send(Ok(())).ok(); runtime = r; @@ -355,7 +376,7 @@ async fn run( } }, Ok(None) => return, - Err(_) => match runtime.step() { + Err(_) => match runtime.update() { Ok(tick_rate) => { next_step += tick_rate; timeout_sender.send(Some(next_step)).ok(); diff --git a/src/run/parser/livesplit.rs b/src/run/parser/livesplit.rs index c6b3bbe9..9e05e87d 100644 --- a/src/run/parser/livesplit.rs +++ b/src/run/parser/livesplit.rs @@ -431,7 +431,7 @@ fn parse_attempt_history(version: Version, reader: &mut Reader<'_>, run: &mut Ru /// parse, you can provide a path to the splits file, which helps saving the /// splits file again later. pub fn parse(source: &str, path: Option) -> Result { - let reader = &mut Reader::new(source); + let mut reader = Reader::new(source); let mut image_buf = Vec::new(); @@ -439,7 +439,7 @@ pub fn parse(source: &str, path: Option) -> Result { let mut required_flags = 0u8; - parse_base(reader, "Run", |reader, attributes| { + parse_base(&mut reader, "Run", |reader, attributes| { let mut version = Version(1, 0, 0, 0); type_hint(optional_attribute_escaped_err(attributes, "version", |t| { version = parse_version(t)?;