Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the auto splitters to add settings #606

Merged
merged 1 commit into from
Nov 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/livesplit-auto-splitting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
//! }
//! ```
//!
Expand All @@ -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};
65 changes: 58 additions & 7 deletions crates/livesplit-auto-splitting/src/runtime.rs
Original file line number Diff line number Diff line change
@@ -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},
};
Expand Down Expand Up @@ -83,6 +82,8 @@ slotmap::new_key_type! {
pub struct Context<T: Timer> {
tick_rate: Duration,
processes: SlotMap<ProcessKey, Process>,
user_settings: Vec<UserSetting>,
settings_store: SettingsStore,
timer: T,
memory: Option<Memory>,
process_list: ProcessList,
Expand Down Expand Up @@ -147,21 +148,28 @@ pub struct Runtime<T: Timer> {
impl<T: Timer> Runtime<T> {
/// 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<Self, CreationError> {
pub fn new(
module: &[u8],
timer: T,
settings_store: SettingsStore,
) -> Result<Self, CreationError> {
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(),
Expand Down Expand Up @@ -216,10 +224,21 @@ impl<T: Timer> Runtime<T> {
/// 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<Duration, RunError> {
pub fn update(&mut self) -> Result<Duration, RunError> {
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<T: Timer>(linker: &mut Linker<Context<T>>) -> Result<(), CreationError> {
Expand Down Expand Up @@ -419,6 +438,38 @@ fn bind_interface<T: Timer>(linker: &mut Linker<Context<T>>) -> Result<(), Creat
})
.context(LinkFunction {
name: "process_read",
})?
.func_wrap("env", "user_settings_add_bool", {
|mut caller: Caller<'_, Context<T>>,
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::<str>::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(())
}
Expand Down
57 changes: 57 additions & 0 deletions crates/livesplit-auto-splitting/src/settings.rs
Original file line number Diff line number Diff line change
@@ -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<str>,
/// The name of the setting that is shown to the user.
pub description: Box<str>,
/// 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<Box<str>, 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<str>, 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<Item = (&str, &SettingValue)> {
self.values.iter().map(|(k, v)| (k.as_ref(), v))
}
}
12 changes: 8 additions & 4 deletions crates/livesplit-auto-splitting/tests/sandboxing.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -61,12 +61,16 @@ fn compile(crate_name: &str) -> anyhow::Result<Runtime<DummyTimer>> {
})
.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(())
}

Expand Down Expand Up @@ -144,7 +148,7 @@ fn infinite_loop() {
interrupt.interrupt();
});

assert!(runtime.step().is_err());
assert!(runtime.update().is_err());
}

// FIXME: Test Network
Expand Down
37 changes: 29 additions & 8 deletions src/auto_splitting/mod.rs
Original file line number Diff line number Diff line change
@@ -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
//!
Expand Down Expand Up @@ -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},
Expand All @@ -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
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -243,7 +260,7 @@ impl Runtime {
}

enum Request {
LoadScript(PathBuf, oneshot::Sender<Result<(), Error>>),
LoadScript(Vec<u8>, oneshot::Sender<Result<(), Error>>),
UnloadScript(oneshot::Sender<()>),
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/run/parser/livesplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,15 +431,15 @@ 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<PathBuf>) -> Result<Run> {
let reader = &mut Reader::new(source);
let mut reader = Reader::new(source);

let mut image_buf = Vec::new();

let mut run = Run::new();

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)?;
Expand Down