livesplit_core/auto_splitting/
mod.rs

1//! The auto splitting module provides a runtime for running auto splitters that
2//! can control the [`Timer`](crate::timing::Timer). These auto splitters are
3//! provided as WebAssembly modules.
4//!
5//! # Requirements for the Auto Splitters
6//!
7//! The auto splitters must provide an `update` function with the following
8//! signature:
9//!
10//! ```rust
11//! #[no_mangle]
12//! pub extern "C" fn update() {}
13//! ```
14//!
15//! This function is called periodically by the runtime at the configured tick
16//! rate. The tick rate is 120 Hz by default, but can be changed by the auto
17//! splitter.
18//!
19//! In addition the WebAssembly module is expected to export a memory called
20//! `memory`.
21//!
22//! # API exposed to the Auto Splitters
23//!
24//! The following functions are provided to the auto splitters in the module
25//! `env`:
26//!
27//! ```rust
28//! # use core::num::NonZeroU64;
29//!
30//! #[repr(transparent)]
31//! pub struct Address(pub u64);
32//!
33//! #[repr(transparent)]
34//! pub struct NonZeroAddress(pub NonZeroU64);
35//!
36//! #[repr(transparent)]
37//! pub struct ProcessId(NonZeroU64);
38//!
39//! #[repr(transparent)]
40//! pub struct TimerState(u32);
41//!
42//! impl TimerState {
43//!     /// The timer is not running.
44//!     pub const NOT_RUNNING: Self = Self(0);
45//!     /// The timer is running.
46//!     pub const RUNNING: Self = Self(1);
47//!     /// The timer started but got paused. This is separate from the game
48//!     /// time being paused. Game time may even always be paused.
49//!     pub const PAUSED: Self = Self(2);
50//!     /// The timer has ended, but didn't get reset yet.
51//!     pub const ENDED: Self = Self(3);
52//! }
53//!
54//! extern "C" {
55//!     /// Gets the state that the timer currently is in.
56//!     pub fn timer_get_state() -> TimerState;
57//!
58//!     /// Starts the timer.
59//!     pub fn timer_start();
60//!     /// Splits the current segment.
61//!     pub fn timer_split();
62//!     /// Resets the timer.
63//!     pub fn timer_reset();
64//!     /// Sets a custom key value pair. This may be arbitrary information that
65//!     /// the auto splitter wants to provide for visualization.
66//!     pub fn timer_set_variable(
67//!         key_ptr: *const u8,
68//!         key_len: usize,
69//!         value_ptr: *const u8,
70//!         value_len: usize,
71//!     );
72//!
73//!     /// Sets the game time.
74//!     pub fn timer_set_game_time(secs: i64, nanos: i32);
75//!     /// Pauses the game time. This does not pause the timer, only the
76//!     /// automatic flow of time for the game time.
77//!     pub fn timer_pause_game_time();
78//!     /// Resumes the game time. This does not resume the timer, only the
79//!     /// automatic flow of time for the game time.
80//!     pub fn timer_resume_game_time();
81//!
82//!     /// Attaches to a process based on its name.
83//!     pub fn process_attach(name_ptr: *const u8, name_len: usize) -> Option<ProcessId>;
84//!     /// Detaches from a process.
85//!     pub fn process_detach(process: ProcessId);
86//!     /// Checks whether is a process is still open. You should detach from a
87//!     /// process and stop using it if this returns `false`.
88//!     pub fn process_is_open(process: ProcessId) -> bool;
89//!     /// Reads memory from a process at the address given. This will write
90//!     /// the memory to the buffer given. Returns `false` if this fails.
91//!     pub fn process_read(
92//!         process: ProcessId,
93//!         address: Address,
94//!         buf_ptr: *mut u8,
95//!         buf_len: usize,
96//!     ) -> bool;
97//!     /// Gets the address of a module in a process.
98//!     pub fn process_get_module_address(
99//!         process: ProcessId,
100//!         name_ptr: *const u8,
101//!         name_len: usize,
102//!     ) -> Option<NonZeroAddress>;
103//!     /// Gets the size of a module in a process.
104//!     pub fn process_get_module_size(
105//!         process: ProcessId,
106//!         name_ptr: *const u8,
107//!         name_len: usize,
108//!     ) -> Option<NonZeroU64>;
109//!
110//!     /// Sets the tick rate of the runtime. This influences the amount of
111//!     /// times the `update` function is called per second.
112//!     pub fn runtime_set_tick_rate(ticks_per_second: f64);
113//!     /// Prints a log message for debugging purposes.
114//!     pub fn runtime_print_message(text_ptr: *const u8, text_len: usize);
115//!
116//!     /// Adds a new setting that the user can modify. This will return either
117//!     /// the specified default value or the value that the user has set.
118//!     pub fn user_settings_add_bool(
119//!         key_ptr: *const u8,
120//!         key_len: usize,
121//!         description_ptr: *const u8,
122//!         description_len: usize,
123//!         default_value: bool,
124//!     ) -> bool;
125//! }
126//! ```
127
128use crate::timing::{SharedTimer, TimerPhase};
129use livesplit_auto_splitting::{
130    CreationError, InterruptHandle, Runtime as ScriptRuntime, SettingsStore,
131    Timer as AutoSplitTimer, TimerState,
132};
133use snafu::Snafu;
134use std::{fmt, fs, io, path::PathBuf, thread, time::Duration};
135use tokio::{
136    runtime,
137    sync::{mpsc, oneshot, watch},
138    time::{timeout_at, Instant},
139};
140
141/// An error that the [`Runtime`] can return.
142#[derive(Debug, Snafu)]
143pub enum Error {
144    /// The runtime thread unexpectedly stopped.
145    ThreadStopped,
146    /// Failed loading the auto splitter.
147    LoadFailed {
148        /// The underlying error.
149        source: CreationError,
150    },
151    /// Failed reading the auto splitter file.
152    ReadFileFailed {
153        /// The underlying error.
154        source: io::Error,
155    },
156}
157
158/// An auto splitter runtime that allows using an auto splitter provided as a
159/// WebAssembly module to control a timer.
160pub struct Runtime {
161    interrupt_receiver: watch::Receiver<Option<InterruptHandle>>,
162    sender: mpsc::UnboundedSender<Request>,
163}
164
165impl Drop for Runtime {
166    fn drop(&mut self) {
167        if let Some(handle) = &*self.interrupt_receiver.borrow() {
168            handle.interrupt();
169        }
170    }
171}
172
173impl Runtime {
174    /// Starts the runtime. Doesn't actually load an auto splitter until
175    /// [`load_script`][Runtime::load_script] is called.
176    pub fn new(timer: SharedTimer) -> Self {
177        let (sender, receiver) = mpsc::unbounded_channel();
178        let (interrupt_sender, interrupt_receiver) = watch::channel(None);
179        let (timeout_sender, timeout_receiver) = watch::channel(None);
180
181        thread::Builder::new()
182            .name("Auto Splitting Runtime".into())
183            .spawn(move || {
184                runtime::Builder::new_current_thread()
185                    .enable_time()
186                    .build()
187                    .unwrap()
188                    .block_on(run(receiver, timer, timeout_sender, interrupt_sender))
189            })
190            .unwrap();
191
192        thread::Builder::new()
193            .name("Auto Splitting Watchdog".into())
194            .spawn({
195                let interrupt_receiver = interrupt_receiver.clone();
196                move || {
197                    runtime::Builder::new_current_thread()
198                        .enable_time()
199                        .build()
200                        .unwrap()
201                        .block_on(watchdog(timeout_receiver, interrupt_receiver))
202                }
203            })
204            .unwrap();
205
206        Self {
207            interrupt_receiver,
208            sender,
209        }
210    }
211
212    /// Attempts to load a wasm file containing an auto splitter module. This
213    /// call will block until the auto splitter has either loaded successfully
214    /// or failed.
215    pub async fn load_script(&self, script: PathBuf) -> Result<(), Error> {
216        let (sender, receiver) = oneshot::channel();
217        let script = fs::read(script).map_err(|e| Error::ReadFileFailed { source: e })?;
218        self.sender
219            .send(Request::LoadScript(script, sender))
220            .map_err(|_| Error::ThreadStopped)?;
221
222        receiver.await.map_err(|_| Error::ThreadStopped)??;
223
224        Ok(())
225    }
226
227    /// Attempts to load a wasm file containing an auto splitter module. This
228    /// call will block until the auto splitter has either loaded successfully
229    /// or failed.
230    pub fn load_script_blocking(&self, script: PathBuf) -> Result<(), Error> {
231        runtime::Builder::new_current_thread()
232            .enable_time()
233            .build()
234            .unwrap()
235            .block_on(self.load_script(script))
236    }
237
238    /// Unloads the current auto splitter. This will _not_ return an error if
239    /// there isn't currently an auto splitter loaded, only if the runtime
240    /// thread stops unexpectedly.
241    pub async fn unload_script(&self) -> Result<(), Error> {
242        let (sender, receiver) = oneshot::channel();
243        self.sender
244            .send(Request::UnloadScript(sender))
245            .map_err(|_| Error::ThreadStopped)?;
246
247        receiver.await.map_err(|_| Error::ThreadStopped)
248    }
249
250    /// Unloads the current auto splitter. This will _not_ return an error if
251    /// there isn't currently an auto splitter loaded, only if the runtime
252    /// thread stops unexpectedly.
253    pub fn unload_script_blocking(&self) -> Result<(), Error> {
254        runtime::Builder::new_current_thread()
255            .enable_time()
256            .build()
257            .unwrap()
258            .block_on(self.unload_script())
259    }
260}
261
262enum Request {
263    LoadScript(Vec<u8>, oneshot::Sender<Result<(), Error>>),
264    UnloadScript(oneshot::Sender<()>),
265}
266
267// This newtype is required because [`SharedTimer`](crate::timing::SharedTimer)
268// is an Arc<RwLock<T>>, so we can't implement the trait directly on it.
269struct Timer(SharedTimer);
270
271impl AutoSplitTimer for Timer {
272    fn state(&self) -> TimerState {
273        match self.0.read().unwrap().current_phase() {
274            TimerPhase::NotRunning => TimerState::NotRunning,
275            TimerPhase::Running => TimerState::Running,
276            TimerPhase::Paused => TimerState::Paused,
277            TimerPhase::Ended => TimerState::Ended,
278        }
279    }
280
281    fn start(&mut self) {
282        self.0.write().unwrap().start()
283    }
284
285    fn split(&mut self) {
286        self.0.write().unwrap().split()
287    }
288
289    fn reset(&mut self) {
290        self.0.write().unwrap().reset(true)
291    }
292
293    fn set_game_time(&mut self, time: time::Duration) {
294        self.0.write().unwrap().set_game_time(time.into());
295    }
296
297    fn pause_game_time(&mut self) {
298        self.0.write().unwrap().pause_game_time()
299    }
300
301    fn resume_game_time(&mut self) {
302        self.0.write().unwrap().resume_game_time()
303    }
304
305    fn set_variable(&mut self, name: &str, value: &str) {
306        self.0.write().unwrap().set_custom_variable(name, value)
307    }
308
309    fn log(&mut self, message: fmt::Arguments<'_>) {
310        log::info!(target: "Auto Splitter", "{message}");
311    }
312}
313
314async fn run(
315    mut receiver: mpsc::UnboundedReceiver<Request>,
316    timer: SharedTimer,
317    timeout_sender: watch::Sender<Option<Instant>>,
318    interrupt_sender: watch::Sender<Option<InterruptHandle>>,
319) {
320    'back_to_not_having_a_runtime: loop {
321        interrupt_sender.send(None).ok();
322        timeout_sender.send(None).ok();
323
324        let mut runtime = loop {
325            match receiver.recv().await {
326                Some(Request::LoadScript(script, ret)) => {
327                    match ScriptRuntime::new(&script, Timer(timer.clone()), SettingsStore::new()) {
328                        Ok(r) => {
329                            ret.send(Ok(())).ok();
330                            break r;
331                        }
332                        Err(source) => {
333                            ret.send(Err(Error::LoadFailed { source })).ok();
334                        }
335                    };
336                }
337                Some(Request::UnloadScript(ret)) => {
338                    log::warn!(target: "Auto Splitter", "Attempted to unload already unloaded script");
339                    ret.send(()).ok();
340                }
341                None => {
342                    return;
343                }
344            };
345        };
346
347        log::info!(target: "Auto Splitter", "Loaded script");
348        let mut next_step = Instant::now();
349        interrupt_sender.send(Some(runtime.interrupt_handle())).ok();
350        timeout_sender.send(Some(next_step)).ok();
351
352        loop {
353            match timeout_at(next_step, receiver.recv()).await {
354                Ok(Some(request)) => match request {
355                    Request::LoadScript(script, ret) => {
356                        match ScriptRuntime::new(
357                            &script,
358                            Timer(timer.clone()),
359                            SettingsStore::new(),
360                        ) {
361                            Ok(r) => {
362                                ret.send(Ok(())).ok();
363                                runtime = r;
364                                log::info!(target: "Auto Splitter", "Reloaded script");
365                            }
366                            Err(source) => {
367                                ret.send(Err(Error::LoadFailed { source })).ok();
368                                log::info!(target: "Auto Splitter", "Failed to load");
369                            }
370                        }
371                    }
372                    Request::UnloadScript(ret) => {
373                        ret.send(()).ok();
374                        log::info!(target: "Auto Splitter", "Unloaded script");
375                        continue 'back_to_not_having_a_runtime;
376                    }
377                },
378                Ok(None) => return,
379                Err(_) => match runtime.update() {
380                    Ok(tick_rate) => {
381                        next_step += tick_rate;
382                        timeout_sender.send(Some(next_step)).ok();
383                    }
384                    Err(e) => {
385                        log::error!(target: "Auto Splitter", "Unloaded due to failure: {:?}", e);
386                        continue 'back_to_not_having_a_runtime;
387                    }
388                },
389            }
390        }
391    }
392}
393
394async fn watchdog(
395    mut timeout_receiver: watch::Receiver<Option<Instant>>,
396    interrupt_receiver: watch::Receiver<Option<InterruptHandle>>,
397) {
398    const TIMEOUT: Duration = Duration::from_secs(5);
399
400    loop {
401        let instant = *timeout_receiver.borrow();
402        match instant {
403            Some(time) => match timeout_at(time + TIMEOUT, timeout_receiver.changed()).await {
404                Ok(Ok(_)) => {}
405                Ok(Err(_)) => return,
406                Err(_) => {
407                    if let Some(handle) = &*interrupt_receiver.borrow() {
408                        handle.interrupt();
409                    }
410                }
411            },
412            None => {
413                if timeout_receiver.changed().await.is_err() {
414                    return;
415                }
416            }
417        }
418    }
419}