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}