diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67caf883..e7e9ab3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -290,12 +290,14 @@ jobs: target: i586-pc-windows-msvc os: windows-latest cross: skip + auto_splitting: skip install_target: true - label: Windows i686 target: i686-pc-windows-msvc os: windows-latest cross: skip + auto_splitting: skip install_target: true - label: Windows x86_64 @@ -308,6 +310,7 @@ jobs: toolchain: stable-i686-pc-windows-gnu os: windows-latest cross: skip + auto_splitting: skip install_target: true - label: Windows x86_64 gnu @@ -639,6 +642,7 @@ jobs: env: TARGET: ${{ matrix.target }} SKIP_CROSS: ${{ matrix.cross }} + SKIP_AUTO_SPLITTING: ${{ matrix.auto_splitting }} - name: Prepare Release if: startsWith(github.ref, 'refs/tags/') && matrix.release == '' diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index ec074be8..96d6a884 100644 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -3,12 +3,15 @@ set -ex main() { local cargo=cross - # all features except those that don't easily work with cross such as font-loading + # all features except those that don't easily work with cross such as + # font-loading or auto-splitting. local all_features="--features std,more-image-formats,image-shrinking,rendering,software-rendering,wasm-web,networking" if [ "$SKIP_CROSS" = "skip" ]; then cargo=cargo - all_features="--all-features" + if [ "$SKIP_AUTO_SPLITTING" -ne "skip" ]; then + all_features="--all-features" + fi fi if [ "$TARGET" = "wasm32-wasi" ]; then diff --git a/Cargo.toml b/Cargo.toml index e2bca12e..677a392e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,11 @@ tiny-skia = { version = "0.6.0", default-features = false, features = [ # Networking splits-io-api = { version = "0.2.0", optional = true } +# Auto Splitting +livesplit-auto-splitting = { path = "crates/livesplit-auto-splitting", version = "0.1.0", optional = true } +crossbeam-channel = { version = "0.5.1", default-features = false, optional = true } +log = { version = "0.4.14", default-features = false, optional = true } + [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] # WebAssembly in the Web js-sys = { version = "0.3.55", optional = true } @@ -151,6 +156,7 @@ wasm-web = [ "web-sys", ] networking = ["std", "splits-io-api"] +auto-splitting = ["std", "livesplit-auto-splitting", "crossbeam-channel", "log"] # FIXME: Some targets don't have atomics, but we can't test for this properly # yet. So there's a feature you explicitly have to opt into to deactivate the diff --git a/crates/livesplit-auto-splitting/Cargo.toml b/crates/livesplit-auto-splitting/Cargo.toml new file mode 100644 index 00000000..61e8c75a --- /dev/null +++ b/crates/livesplit-auto-splitting/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "livesplit-auto-splitting" +version = "0.1.0" +authors = ["Christopher Serr "] +edition = "2021" + +[dependencies] +anyhow = { version = "1.0.45", default-features = false } +log = { version = "0.4.14", default-features = false } +proc-maps = { version = "0.2.0", default-features = false } +read-process-memory = { version = "0.1.4", default-features = false } +slotmap = { version = "1.0.2", default-features = false } +snafu = { version = "0.6.10", default-features = false, features = ["std"] } +sysinfo = { version = "0.22.4", default-features = false, features = ["multithread"] } +time = { version = "0.3.3", default-features = false } +wasmtime = { version = "0.33.0", default-features = false, features = [ + "cranelift", + "parallel-compilation", +] } diff --git a/crates/livesplit-auto-splitting/README.md b/crates/livesplit-auto-splitting/README.md new file mode 100644 index 00000000..cd6254bb --- /dev/null +++ b/crates/livesplit-auto-splitting/README.md @@ -0,0 +1,110 @@ +# LiveSplit livesplit-auto-splitting + +livesplit-auto-splitting is a library that provides a runtime for running +auto splitters that can control a speedrun timer. These auto splitters are +provided as WebAssembly modules. + +## Requirements for the Auto Splitters + +The auto splitters must provide an `update` function with the following +signature: + +```rust +#[no_mangle] +pub extern "C" fn update() {} +``` + +This function is called periodically by the runtime at the configured tick +rate. The tick rate is 120 Hz by default, but can be changed by the auto +splitter. + +In addition the WebAssembly module is expected to export a memory called +`memory`. + +## API exposed to the Auto Splitters + +The following functions are provided to the auto splitters in the module +`env`: + +```rust +#[repr(transparent)] +pub struct Address(pub u64); + +#[repr(transparent)] +pub struct NonZeroAddress(pub NonZeroU64); + +#[repr(transparent)] +pub struct ProcessId(NonZeroU64); + +#[repr(transparent)] +pub struct TimerState(u32); + +impl TimerState { + /// The timer is not running. + pub const NOT_RUNNING: Self = Self(0); + /// The timer is running. + pub const RUNNING: Self = Self(1); + /// The timer started but got paused. This is separate from the game + /// time being paused. Game time may even always be paused. + pub const PAUSED: Self = Self(2); + /// The timer has ended, but didn't get reset yet. + pub const ENDED: Self = Self(3); +} + +extern "C" { + /// Gets the state that the timer currently is in. + pub fn timer_get_state() -> TimerState; + + /// Starts the timer. + pub fn timer_start(); + /// Splits the current segment. + pub fn timer_split(); + /// Resets the timer. + pub fn timer_reset(); + /// Sets a custom key value pair. This may be arbitrary information that + /// the auto splitter wants to provide for visualization. + pub fn timer_set_variable( + key_ptr: *const u8, + key_len: usize, + value_ptr: *const u8, + value_len: usize, + ); + + /// Sets the game time. + pub fn timer_set_game_time(secs: i64, nanos: i32); + /// Pauses the game time. This does not pause the timer, only the + /// automatic flow of time for the game time. + pub fn timer_pause_game_time(); + /// Resumes the game time. This does not resume the timer, only the + /// automatic flow of time for the game time. + pub fn timer_resume_game_time(); + + /// Attaches to a process based on its name. + pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; + /// Detaches from a process. + pub fn process_detach(process: ProcessId); + /// Checks whether is a process is still open. You should detach from a + /// process and stop using it if this returns `false`. + pub fn process_is_open(process: ProcessId) -> bool; + /// Reads memory from a process at the address given. This will write + /// the memory to the buffer given. Returns `false` if this fails. + pub fn process_read( + process: ProcessId, + address: Address, + buf_ptr: *mut u8, + buf_len: usize, + ) -> bool; + /// Gets the address of a module in a process. + pub fn process_get_module_address( + process: ProcessId, + name_ptr: *const u8, + name_len: usize, + ) -> Option; + + /// Sets the tick rate of the runtime. This influences the amount of + /// times the `update` function is called per second. + 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); +} +``` diff --git a/crates/livesplit-auto-splitting/src/lib.rs b/crates/livesplit-auto-splitting/src/lib.rs new file mode 100644 index 00000000..8a971585 --- /dev/null +++ b/crates/livesplit-auto-splitting/src/lib.rs @@ -0,0 +1,126 @@ +//! livesplit-auto-splitting is a library that provides a runtime for running +//! auto splitters that can control a speedrun timer. These auto splitters are +//! provided as WebAssembly modules. +//! +//! # Requirements for the Auto Splitters +//! +//! The auto splitters must provide an `update` function with the following +//! signature: +//! +//! ```rust +//! #[no_mangle] +//! pub extern "C" fn update() {} +//! ``` +//! +//! This function is called periodically by the runtime at the configured tick +//! rate. The tick rate is 120 Hz by default, but can be changed by the auto +//! splitter. +//! +//! In addition the WebAssembly module is expected to export a memory called +//! `memory`. +//! +//! # API exposed to the Auto Splitters +//! +//! The following functions are provided to the auto splitters in the module +//! `env`: +//! +//! ```rust +//! #[repr(transparent)] +//! pub struct Address(pub u64); +//! +//! #[repr(transparent)] +//! pub struct NonZeroAddress(pub NonZeroU64); +//! +//! #[repr(transparent)] +//! pub struct ProcessId(NonZeroU64); +//! +//! #[repr(transparent)] +//! pub struct TimerState(u32); +//! +//! impl TimerState { +//! /// The timer is not running. +//! pub const NOT_RUNNING: Self = Self(0); +//! /// The timer is running. +//! pub const RUNNING: Self = Self(1); +//! /// The timer started but got paused. This is separate from the game +//! /// time being paused. Game time may even always be paused. +//! pub const PAUSED: Self = Self(2); +//! /// The timer has ended, but didn't get reset yet. +//! pub const ENDED: Self = Self(3); +//! } +//! +//! extern "C" { +//! /// Gets the state that the timer currently is in. +//! pub fn timer_get_state() -> TimerState; +//! +//! /// Starts the timer. +//! pub fn timer_start(); +//! /// Splits the current segment. +//! pub fn timer_split(); +//! /// Resets the timer. +//! pub fn timer_reset(); +//! /// Sets a custom key value pair. This may be arbitrary information that +//! /// the auto splitter wants to provide for visualization. +//! pub fn timer_set_variable( +//! key_ptr: *const u8, +//! key_len: usize, +//! value_ptr: *const u8, +//! value_len: usize, +//! ); +//! +//! /// Sets the game time. +//! pub fn timer_set_game_time(secs: i64, nanos: i32); +//! /// Pauses the game time. This does not pause the timer, only the +//! /// automatic flow of time for the game time. +//! pub fn timer_pause_game_time(); +//! /// Resumes the game time. This does not resume the timer, only the +//! /// automatic flow of time for the game time. +//! pub fn timer_resume_game_time(); +//! +//! /// Attaches to a process based on its name. +//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; +//! /// Detaches from a process. +//! pub fn process_detach(process: ProcessId); +//! /// Checks whether is a process is still open. You should detach from a +//! /// process and stop using it if this returns `false`. +//! pub fn process_is_open(process: ProcessId) -> bool; +//! /// Reads memory from a process at the address given. This will write +//! /// the memory to the buffer given. Returns `false` if this fails. +//! pub fn process_read( +//! process: ProcessId, +//! address: Address, +//! buf_ptr: *mut u8, +//! buf_len: usize, +//! ) -> bool; +//! /// Gets the address of a module in a process. +//! pub fn process_get_module_address( +//! process: ProcessId, +//! name_ptr: *const u8, +//! name_len: usize, +//! ) -> Option; +//! +//! /// Sets the tick rate of the runtime. This influences the amount of +//! /// times the `update` function is called per second. +//! 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); +//! } +//! ``` + +#![warn( + clippy::complexity, + clippy::correctness, + clippy::perf, + clippy::style, + clippy::missing_const_for_fn, + missing_docs, + rust_2018_idioms +)] + +mod process; +mod runtime; +mod timer; + +pub use runtime::{CreationError, RunError, Runtime}; +pub use timer::{Timer, TimerState}; +pub use wasmtime::InterruptHandle; diff --git a/crates/livesplit-auto-splitting/src/process.rs b/crates/livesplit-auto-splitting/src/process.rs new file mode 100644 index 00000000..5666f636 --- /dev/null +++ b/crates/livesplit-auto-splitting/src/process.rs @@ -0,0 +1,85 @@ +use std::{ + io, + time::{Duration, Instant}, +}; + +use proc_maps::{MapRange, Pid}; +use read_process_memory::{CopyAddress, ProcessHandle}; +use snafu::{OptionExt, ResultExt, Snafu}; +use sysinfo::{self, ProcessExt}; + +use crate::runtime::ProcessList; + +#[derive(Debug, Snafu)] +pub enum OpenError { + ProcessDoesntExist, + InvalidHandle { source: io::Error }, +} + +#[derive(Debug, Snafu)] +pub enum ModuleError { + ModuleDoesntExist, + ListModules { source: io::Error }, +} + +pub type Address = u64; + +pub struct Process { + handle: ProcessHandle, + pid: Pid, + modules: Vec, + last_check: Instant, +} + +impl std::fmt::Debug for Process { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Process").field("pid", &self.pid).finish() + } +} + +impl Process { + pub fn with_name(name: &str, process_list: &mut ProcessList) -> Result { + process_list.refresh(); + let processes = process_list.process_by_name(name); + + let pid = processes.first().context(ProcessDoesntExist)?.pid() as Pid; + + let handle = pid.try_into().context(InvalidHandle)?; + + Ok(Process { + handle, + pid, + modules: Vec::new(), + last_check: Instant::now() - Duration::from_secs(1), + }) + } + + pub fn is_open(&self, process_list: &mut ProcessList) -> bool { + // FIXME: We can actually ask the list to only refresh the individual process. + process_list.refresh(); + process_list.is_open(self.pid as _) + } + + pub fn module_address(&mut self, module: &str) -> Result { + let now = Instant::now(); + if now - self.last_check >= Duration::from_secs(1) { + self.modules = match proc_maps::get_process_maps(self.pid) { + Ok(m) => m, + Err(source) => { + self.modules.clear(); + return Err(ModuleError::ListModules { source }); + } + }; + self.last_check = now; + } + self.modules + .iter() + .find(|m| m.filename().map_or(false, |f| f.ends_with(module))) + .context(ModuleDoesntExist) + .map(|m| m.start() as u64) + } + + pub fn read_mem(&self, address: Address, buf: &mut [u8]) -> io::Result<()> { + self.handle.copy_address(address as usize, buf) + } +} diff --git a/crates/livesplit-auto-splitting/src/runtime.rs b/crates/livesplit-auto-splitting/src/runtime.rs new file mode 100644 index 00000000..a4e0cea3 --- /dev/null +++ b/crates/livesplit-auto-splitting/src/runtime.rs @@ -0,0 +1,331 @@ +use crate::{process::Process, timer::Timer, InterruptHandle}; + +use log::info; +use slotmap::{Key, KeyData, SlotMap}; +use snafu::{ResultExt, Snafu}; +use std::{ + path::Path, + result, str, thread, + time::{Duration, Instant}, +}; +use sysinfo::{ProcessRefreshKind, RefreshKind, System, SystemExt}; +use wasmtime::{ + Caller, Config, Engine, Extern, Linker, Memory, Module, OptLevel, Store, Trap, TypedFunc, +}; + +/// An error that is returned when the creation of a new runtime fails. +#[derive(Debug, Snafu)] +pub enum CreationError { + /// Failed creating the WebAssembly engine. + EngineCreation { + /// The underlying error. + source: anyhow::Error, + }, + /// Failed loading the WebAssembly module. + ModuleLoading { + /// The underlying error. + source: anyhow::Error, + }, + /// Failed linking the WebAssembly module. + #[snafu(display("Failed linking the function `{}` to the WebAssembly module.", name))] + LinkFunction { + /// The name of the function that failed to link. + name: &'static str, + /// The underlying error. + source: anyhow::Error, + }, + /// Failed instantiating the WebAssembly module. + ModuleInstantiation { + /// The underlying error. + source: anyhow::Error, + }, + /// The WebAssembly module has no exported function named `update`, which is + /// a required function. + MissingUpdateFunction { + /// The underlying error. + source: anyhow::Error, + }, + /// The WebAssembly module has no exported function memory called `memory`, + /// which is a requirement. + MissingMemory, +} + +/// An error that is returned when executing the WebAssembly module fails. +#[derive(Debug, Snafu)] +pub enum RunError { + /// Failed running the `update` function. + RunUpdate { + /// The underlying error. + source: Trap, + }, +} + +slotmap::new_key_type! { + struct ProcessKey; +} + +pub struct Context { + tick_rate: Duration, + processes: SlotMap, + timer: T, + memory: Option, + process_list: ProcessList, +} + +pub struct ProcessList { + system: System, + last_check: Instant, +} + +impl ProcessList { + fn new() -> Self { + Self { + system: System::new_with_specifics( + RefreshKind::new().with_processes(ProcessRefreshKind::new()), + ), + last_check: Instant::now() - Duration::from_secs(1), + } + } + + pub fn refresh(&mut self) { + let now = Instant::now(); + if now - self.last_check >= Duration::from_secs(1) { + self.system + .refresh_processes_specifics(ProcessRefreshKind::new()); + self.last_check = now; + } + } + + pub fn process_by_name(&self, name: &str) -> Vec<&sysinfo::Process> { + self.system.process_by_name(name) + } + + pub fn is_open(&self, pid: sysinfo::Pid) -> bool { + self.system.process(pid).is_some() + } +} + +/// An auto splitter runtime that allows using an auto splitter provided as a +/// WebAssembly module to control a timer. +pub struct Runtime { + store: Store>, + update: TypedFunc<(), ()>, + prev_time: Instant, +} + +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: P, timer: T) -> Result { + let engine = Engine::new( + Config::new() + .cranelift_opt_level(OptLevel::Speed) + .interruptable(true), + ) + .context(EngineCreation)?; + + let mut store = Store::new( + &engine, + Context { + processes: SlotMap::with_key(), + tick_rate: Duration::from_secs(1) / 120, + timer, + memory: None, + process_list: ProcessList::new(), + }, + ); + + let module = Module::from_file(&engine, path).context(ModuleLoading)?; + let mut linker = Linker::new(&engine); + bind_interface(&mut linker)?; + let instance = linker + .instantiate(&mut store, &module) + .context(ModuleInstantiation)?; + + let update = instance + .get_typed_func(&mut store, "update") + .context(MissingUpdateFunction)?; + + if let Some(Extern::Memory(mem)) = instance.get_export(&mut store, "memory") { + store.data_mut().memory = Some(mem); + } else { + return Err(CreationError::MissingMemory); + } + + Ok(Self { + store, + update, + prev_time: Instant::now(), + }) + } + + /// Accesses an interrupt handle that allows you to interrupt the ongoing + /// execution of the WebAssembly module. A WebAssembly module may + /// accidentally or maliciously loop forever, which is why this is needed. + pub fn interrupt_handle(&self) -> InterruptHandle { + self.store + .interrupt_handle() + .expect("We configured the runtime to produce an interrupt handle.") + } + + /// Runs the exported `update` function of the WebAssembly module a single + /// time. If the module has not been configured yet, this will also call the + /// optional `configure` function beforehand. + pub fn step(&mut self) -> Result<(), RunError> { + self.update.call(&mut self.store, ()).context(RunUpdate) + } + + /// Sleeps until the next tick based on the current tick rate. The auto + /// splitter can change this tick rate. It is 120Hz by default. + pub fn sleep(&mut self) { + let target = self.store.data().tick_rate; + let delta = self.prev_time.elapsed(); + if delta < target { + thread::sleep(target - delta); + } + self.prev_time = Instant::now(); + } +} + +fn bind_interface(linker: &mut Linker>) -> Result<(), CreationError> { + linker + .func_wrap("env", "timer_start", |mut caller: Caller<'_, Context>| { + caller.data_mut().timer.start(); + }).context(LinkFunction { name: "timer_start" })? + .func_wrap("env", "timer_split", |mut caller: Caller<'_, Context>| { + caller.data_mut().timer.split(); + }).context(LinkFunction { name: "timer_split" })? + .func_wrap("env", "timer_reset", |mut caller: Caller<'_, Context>| { + caller.data_mut().timer.reset(); + }).context(LinkFunction { name: "timer_reset" })? + .func_wrap("env", "timer_pause_game_time", { + |mut caller: Caller<'_, Context>| caller.data_mut().timer.pause_game_time() + }).context(LinkFunction { name: "timer_pause_game_time" })? + .func_wrap("env", "timer_resume_game_time", { + |mut caller: Caller<'_, Context>| caller.data_mut().timer.resume_game_time() + }).context(LinkFunction { name: "timer_resume_game_time" })? + .func_wrap("env", "timer_set_game_time", { + |mut caller: Caller<'_, Context>, secs: i64, nanos: i32| { + caller + .data_mut() + .timer + .set_game_time(time::Duration::new(secs, nanos)); + } + }).context(LinkFunction { name: "timer_set_game_time" })? + .func_wrap("env", "runtime_set_tick_rate", { + |mut caller: Caller<'_, Context>, ticks_per_sec: f64| { + info!(target: "Auto Splitter", "New Tick Rate: {}", ticks_per_sec); + caller.data_mut().tick_rate = Duration::from_secs_f64(ticks_per_sec.recip()); + } + }).context(LinkFunction { name: "runtime_set_tick_rate" })? + .func_wrap("env", "timer_get_state", { + |caller: Caller<'_, Context>| caller.data().timer.state() as u32 + }).context(LinkFunction { name: "timer_get_state" })? + .func_wrap("env", "runtime_print_message", { + |mut caller: Caller<'_, Context>, ptr: u32, len: u32| { + let message = read_str(&mut caller, ptr, len)?; + info!(target: "Auto Splitter", "{}", message); + Ok(()) + } + }).context(LinkFunction { name: "runtime_print_message" })? + .func_wrap("env", "timer_set_variable", { + |mut caller: Caller<'_, Context>, + name_ptr: u32, + name_len: u32, + value_ptr: u32, + value_len: u32| + -> result::Result<(), Trap> { + let name = read_str(&mut caller, name_ptr, name_len)?; + let value = read_str(&mut caller, value_ptr, value_len)?; + caller.data_mut().timer.set_variable(&name, &value); + Ok(()) + } + }).context(LinkFunction { name: "timer_set_variable" })? + .func_wrap("env", "process_attach", { + |mut caller: Caller<'_, Context>, ptr: u32, len: u32| { + let process_name = read_str(&mut caller, ptr, len)?; + Ok( + if let Ok(p) = Process::with_name(&process_name, &mut caller.data_mut().process_list) { + info!(target: "Auto Splitter", "Attached to a new process: {}", process_name); + caller.data_mut().processes.insert(p).data().as_ffi() + } else { + 0 + }, + ) + } + }).context(LinkFunction { name: "process_attach" })? + .func_wrap("env", "process_detach", { + |mut caller: Caller<'_, Context>, process: u64| { + caller + .data_mut() + .processes + .remove(ProcessKey::from(KeyData::from_ffi(process as u64))) + .ok_or_else(|| Trap::new(format!("Invalid process handle {}", process)))?; + info!(target: "Auto Splitter", "Detached from a process."); + Ok(()) + } + }).context(LinkFunction { name: "process_detach" })? + .func_wrap("env", "process_is_open", { + |mut caller: Caller<'_, Context>, process: u64| { + let ctx = caller.data_mut(); + let proc = ctx + .processes + .get(ProcessKey::from(KeyData::from_ffi(process as u64))) + .ok_or_else(|| Trap::new(format!("Invalid process handle: {}", process)))?; + Ok(proc.is_open(&mut ctx.process_list) as u32) + } + }).context(LinkFunction { name: "process_is_open" })? + .func_wrap("env", "process_get_module_address", { + |mut caller: Caller<'_, Context>, process: u64, ptr: u32, len: u32| { + let module_name = read_str(&mut caller, ptr, len)?; + Ok(caller + .data_mut() + .processes + .get_mut(ProcessKey::from(KeyData::from_ffi(process as u64))) + .ok_or_else(|| Trap::new(format!("Invalid process handle: {}", process)))? + .module_address(&module_name) + .unwrap_or_default()) + } + }).context(LinkFunction { name: "process_get_module_address" })? + .func_wrap("env", "process_read", { + |mut caller: Caller<'_, Context>, + process: u64, + address: u64, + buf_ptr: u32, + buf_len: u32| { + let (slice, context) = caller + .data() + .memory + .unwrap() + .data_and_store_mut(&mut caller); + Ok(context + .processes + .get(ProcessKey::from(KeyData::from_ffi(process as u64))) + .ok_or_else(|| Trap::new(format!("Invalid process handle: {}", process)))? + .read_mem( + address, + slice + .get_mut(buf_ptr as usize..(buf_ptr + buf_len) as usize) + .ok_or_else(|| Trap::new("Out of bounds"))?, + ) + .is_ok() as u32) + } + }).context(LinkFunction { name: "process_read" })?; + Ok(()) +} + +fn read_str( + caller: &mut Caller<'_, Context>, + ptr: u32, + len: u32, +) -> result::Result { + let data = caller + .data() + .memory + .unwrap() + .data(&caller) + .get(ptr as usize..(ptr + len) as usize) + .ok_or_else(|| Trap::new("Pointer out of bounds"))?; + let s = str::from_utf8(data).map_err(|_| Trap::new("Invalid utf-8"))?; + Ok(s.to_string()) +} diff --git a/crates/livesplit-auto-splitting/src/timer.rs b/crates/livesplit-auto-splitting/src/timer.rs new file mode 100644 index 00000000..349d9bc3 --- /dev/null +++ b/crates/livesplit-auto-splitting/src/timer.rs @@ -0,0 +1,43 @@ +/// Represents the state that a timer is in. +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum TimerState { + /// The timer is not running. + NotRunning = 0, + /// The timer is running. + Running = 1, + /// The timer started but got paused. This is separate from the game time + /// being paused. Game time may even always be paused. + Paused = 2, + /// The timer has ended, but didn't get reset yet. + Ended = 3, +} + +impl Default for TimerState { + fn default() -> Self { + Self::NotRunning + } +} + +/// A timer that can be controlled by an auto splitter. +pub trait Timer { + /// Returns the current state of the timer. + fn state(&self) -> TimerState; + /// Starts the timer. + fn start(&mut self); + /// Splits the current segment. + fn split(&mut self); + /// Resets the timer. + fn reset(&mut self); + /// Sets the game time. + fn set_game_time(&mut self, time: time::Duration); + /// Pauses the game time. This does not pause the timer, only the automatic + /// flow of time for the game time. + fn pause_game_time(&mut self); + /// Resumes the game time. This does not resume the timer, only the + /// automatic flow of time for the game time. + fn resume_game_time(&mut self); + /// Sets a custom key value pair. This may be arbitrary information that the + /// auto splitter wants to provide for visualization. + fn set_variable(&mut self, key: &str, value: &str); +} diff --git a/src/auto_splitting/mod.rs b/src/auto_splitting/mod.rs new file mode 100644 index 00000000..25489412 --- /dev/null +++ b/src/auto_splitting/mod.rs @@ -0,0 +1,286 @@ +//! The auto splitting module provides a runtime for running auto splitters that +//! can control the timer. These auto splitters are provided as WebAssembly +//! modules. +//! +//! # Requirements for the Auto Splitters +//! +//! The auto splitters must provide an `update` function with the following +//! signature: +//! +//! ```rust +//! #[no_mangle] +//! pub extern "C" fn update() {} +//! ``` +//! +//! This function is called periodically by the runtime at the configured tick +//! rate. The tick rate is 120 Hz by default, but can be changed by the auto +//! splitter. +//! +//! In addition the WebAssembly module is expected to export a memory called +//! `memory`. +//! +//! # API exposed to the Auto Splitters +//! +//! The following functions are provided to the auto splitters in the module +//! `env`: +//! +//! ```rust +//! #[repr(transparent)] +//! pub struct Address(pub u64); +//! +//! #[repr(transparent)] +//! pub struct NonZeroAddress(pub NonZeroU64); +//! +//! #[repr(transparent)] +//! pub struct ProcessId(NonZeroU64); +//! +//! #[repr(transparent)] +//! pub struct TimerState(u32); +//! +//! impl TimerState { +//! /// The timer is not running. +//! pub const NOT_RUNNING: Self = Self(0); +//! /// The timer is running. +//! pub const RUNNING: Self = Self(1); +//! /// The timer started but got paused. This is separate from the game +//! /// time being paused. Game time may even always be paused. +//! pub const PAUSED: Self = Self(2); +//! /// The timer has ended, but didn't get reset yet. +//! pub const ENDED: Self = Self(3); +//! } +//! +//! extern "C" { +//! /// Gets the state that the timer currently is in. +//! pub fn timer_get_state() -> TimerState; +//! +//! /// Starts the timer. +//! pub fn timer_start(); +//! /// Splits the current segment. +//! pub fn timer_split(); +//! /// Resets the timer. +//! pub fn timer_reset(); +//! /// Sets a custom key value pair. This may be arbitrary information that +//! /// the auto splitter wants to provide for visualization. +//! pub fn timer_set_variable( +//! key_ptr: *const u8, +//! key_len: usize, +//! value_ptr: *const u8, +//! value_len: usize, +//! ); +//! +//! /// Sets the game time. +//! pub fn timer_set_game_time(secs: i64, nanos: i32); +//! /// Pauses the game time. This does not pause the timer, only the +//! /// automatic flow of time for the game time. +//! pub fn timer_pause_game_time(); +//! /// Resumes the game time. This does not resume the timer, only the +//! /// automatic flow of time for the game time. +//! pub fn timer_resume_game_time(); +//! +//! /// Attaches to a process based on its name. +//! pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option; +//! /// Detaches from a process. +//! pub fn process_detach(process: ProcessId); +//! /// Checks whether is a process is still open. You should detach from a +//! /// process and stop using it if this returns `false`. +//! pub fn process_is_open(process: ProcessId) -> bool; +//! /// Reads memory from a process at the address given. This will write +//! /// the memory to the buffer given. Returns `false` if this fails. +//! pub fn process_read( +//! process: ProcessId, +//! address: Address, +//! buf_ptr: *mut u8, +//! buf_len: usize, +//! ) -> bool; +//! /// Gets the address of a module in a process. +//! pub fn process_get_module_address( +//! process: ProcessId, +//! name_ptr: *const u8, +//! name_len: usize, +//! ) -> Option; +//! +//! /// Sets the tick rate of the runtime. This influences the amount of +//! /// times the `update` function is called per second. +//! 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); +//! } +//! ``` + +use crate::timing::{SharedTimer, TimerPhase}; +use crossbeam_channel::{bounded, unbounded, Sender}; +use livesplit_auto_splitting::{ + CreationError, Runtime as ScriptRuntime, Timer as AutoSplitTimer, TimerState, +}; +use snafu::Snafu; +use std::{ + path::PathBuf, + thread::{self, JoinHandle}, +}; +use time::Duration; + +/// An error that the [`Runtime`] can return. +#[derive(Debug, Snafu)] +pub enum Error { + /// The runtime thread unexpectedly stopped. + ThreadStopped, + /// Failed loading the auto splitter. + LoadFailed { + /// The underlying error. + source: CreationError, + }, +} + +/// An auto splitter runtime that allows using an auto splitter provided as a +/// WebAssembly module to control a timer. +pub struct Runtime { + sender: Sender, + join_handle: Option>>, +} + +impl Drop for Runtime { + fn drop(&mut self) { + self.sender.send(Request::End).ok(); + self.join_handle.take().unwrap().join().ok(); + } +} + +impl Runtime { + /// Starts the runtime. Doesn't actually load an auto splitter until + /// [`load_script`][Runtime::load_script] is called. + pub fn new(timer: SharedTimer) -> Self { + let (sender, receiver) = unbounded(); + let join_handle = thread::spawn(move || -> Result<(), Error> { + 'back_to_not_having_a_runtime: loop { + let mut runtime = loop { + match receiver.recv().map_err(|_| Error::ThreadStopped)? { + Request::LoadScript(script, ret) => { + match ScriptRuntime::new(&script, Timer(timer.clone())) { + Ok(r) => { + ret.send(Ok(())).ok(); + break r; + } + Err(source) => ret.send(Err(Error::LoadFailed { source })).unwrap(), + }; + } + Request::UnloadScript(ret) => { + log::warn!(target: "Auto Splitter", "Attempted to unload already unloaded script"); + ret.send(()).ok(); + } + Request::End => return Ok(()), + }; + }; + log::info!(target: "Auto Splitter", "Loaded script"); + loop { + if let Ok(request) = receiver.try_recv() { + match request { + Request::LoadScript(script, ret) => { + match ScriptRuntime::new(&script, Timer(timer.clone())) { + Ok(r) => { + ret.send(Ok(())).ok(); + runtime = r; + log::info!(target: "Auto Splitter", "Reloaded script"); + } + Err(source) => { + ret.send(Err(Error::LoadFailed { source })).ok(); + log::info!(target: "Auto Splitter", "Failed to load"); + } + } + } + Request::UnloadScript(ret) => { + ret.send(()).ok(); + log::info!(target: "Auto Splitter", "Unloaded script"); + continue 'back_to_not_having_a_runtime; + } + Request::End => return Ok(()), + } + } + if let Err(e) = runtime.step() { + log::error!(target: "Auto Splitter", "Unloaded due to failure: {}", e); + continue 'back_to_not_having_a_runtime; + }; + runtime.sleep(); + } + } + }); + + Self { + sender, + join_handle: Some(join_handle), + } + } + + /// Attempts to load a wasm file containing an auto splitter module. This + /// call will block until the auto splitter has either loaded successfully + /// or failed. + pub fn load_script(&self, script: PathBuf) -> Result<(), Error> { + // FIXME: replace with `futures::channel::oneshot` + let (sender, receiver) = bounded(1); + self.sender + .send(Request::LoadScript(script, sender)) + .map_err(|_| Error::ThreadStopped)?; + receiver.recv().map_err(|_| Error::ThreadStopped)??; + Ok(()) + } + + /// Unloads the current auto splitter. This will _not_ return an error if + /// there isn't currently an auto splitter loaded, only if the runtime + /// thread stops unexpectedly. + pub fn unload_script(&self) -> Result<(), Error> { + // FIXME: replace with `futures::channel::oneshot` + let (sender, receiver) = bounded(1); + self.sender + .send(Request::UnloadScript(sender)) + .map_err(|_| Error::ThreadStopped)?; + receiver.recv().map_err(|_| Error::ThreadStopped) + } +} + +enum Request { + LoadScript(PathBuf, Sender>), + UnloadScript(Sender<()>), + End, +} + +// This newtype is required because SharedTimer is an Arc>, so we +// can't implement the trait directly on it. +struct Timer(SharedTimer); + +impl AutoSplitTimer for Timer { + fn state(&self) -> TimerState { + match self.0.read().current_phase() { + TimerPhase::NotRunning => TimerState::NotRunning, + TimerPhase::Running => TimerState::Running, + TimerPhase::Paused => TimerState::Paused, + TimerPhase::Ended => TimerState::Ended, + } + } + + fn start(&mut self) { + self.0.write().start() + } + + fn split(&mut self) { + self.0.write().split() + } + + fn reset(&mut self) { + self.0.write().reset(true) + } + + fn set_game_time(&mut self, time: Duration) { + self.0.write().set_game_time(time.into()); + } + + fn pause_game_time(&mut self) { + self.0.write().pause_game_time() + } + + fn resume_game_time(&mut self) { + self.0.write().resume_game_time() + } + + fn set_variable(&mut self, name: &str, value: &str) { + self.0.write().set_custom_variable(name, value) + } +} diff --git a/src/lib.rs b/src/lib.rs index bff53cf7..259c95f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,8 @@ macro_rules! catch { } pub mod analysis; +#[cfg(feature = "auto-splitting")] +pub mod auto_splitting; pub mod clear_vec; pub mod comparison; pub mod component;