|
| 1 | +#![no_std] |
| 2 | +#![warn( |
| 3 | + clippy::complexity, |
| 4 | + clippy::correctness, |
| 5 | + clippy::perf, |
| 6 | + clippy::style, |
| 7 | + clippy::undocumented_unsafe_blocks, |
| 8 | + rust_2018_idioms |
| 9 | +)] |
| 10 | + |
| 11 | +use asr::{ |
| 12 | + future::{next_tick, retry}, |
| 13 | + settings::Gui, |
| 14 | + time::Duration, |
| 15 | + timer::{self, TimerState}, |
| 16 | + watcher::Watcher, |
| 17 | + Address, FromEndian, Process, |
| 18 | +}; |
| 19 | + |
| 20 | +mod client_layer; |
| 21 | + |
| 22 | +asr::panic_handler!(); |
| 23 | +asr::async_main!(stable); |
| 24 | + |
| 25 | +async fn main() { |
| 26 | + let mut settings = Settings::register(); |
| 27 | + asr::set_tick_rate(120.0); |
| 28 | + |
| 29 | + loop { |
| 30 | + // Hook to the target process |
| 31 | + let (process_name, process) = hook_process().await; |
| 32 | + |
| 33 | + process |
| 34 | + .until_closes(async { |
| 35 | + // Once the target has been found and attached to, set up some default watchers |
| 36 | + let mut watchers = Watchers::default(); |
| 37 | + |
| 38 | + // Perform memory scanning to look for the addresses we need |
| 39 | + let memory = Memory::init(&process, process_name).await; |
| 40 | + |
| 41 | + loop { |
| 42 | + // Splitting logic. Adapted from OG LiveSplit: |
| 43 | + // Order of execution |
| 44 | + // 1. update() will always be run first. There are no conditions on the execution of this action. |
| 45 | + // 2. If the timer is currently either running or paused, then the isLoading, gameTime, and reset actions will be run. |
| 46 | + // 3. If reset does not return true, then the split action will be run. |
| 47 | + // 4. If the timer is currently not running (and not paused), then the start action will be run. |
| 48 | + settings.update(); |
| 49 | + update_loop(&process, &memory, &mut watchers); |
| 50 | + |
| 51 | + if [TimerState::Running, TimerState::Paused].contains(&timer::state()) { |
| 52 | + match is_loading(&watchers, &settings) { |
| 53 | + Some(true) => timer::pause_game_time(), |
| 54 | + Some(false) => timer::resume_game_time(), |
| 55 | + _ => (), |
| 56 | + } |
| 57 | + |
| 58 | + match game_time(&watchers, &settings, &memory) { |
| 59 | + Some(x) => timer::set_game_time(x), |
| 60 | + _ => (), |
| 61 | + } |
| 62 | + |
| 63 | + match reset(&watchers, &settings) { |
| 64 | + true => timer::reset(), |
| 65 | + _ => match split(&watchers, &settings) { |
| 66 | + true => timer::split(), |
| 67 | + _ => (), |
| 68 | + }, |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + if timer::state().eq(&TimerState::NotRunning) { |
| 73 | + watchers.igt_buffer = Duration::ZERO; |
| 74 | + if start(&watchers, &settings) { |
| 75 | + timer::start(); |
| 76 | + timer::pause_game_time(); |
| 77 | + |
| 78 | + match is_loading(&watchers, &settings) { |
| 79 | + Some(true) => timer::pause_game_time(), |
| 80 | + Some(false) => timer::resume_game_time(), |
| 81 | + _ => (), |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + next_tick().await; |
| 87 | + } |
| 88 | + }) |
| 89 | + .await; |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +#[derive(Gui)] |
| 94 | +struct Settings { |
| 95 | + /// Use IGT instead of LRT |
| 96 | + #[default = false] |
| 97 | + igt: bool, |
| 98 | +} |
| 99 | + |
| 100 | +#[derive(Default)] |
| 101 | +struct Watchers { |
| 102 | + is_loading: Watcher<bool>, |
| 103 | + igt: Watcher<Duration>, |
| 104 | + igt_buffer: Duration, |
| 105 | +} |
| 106 | + |
| 107 | +struct Memory { |
| 108 | + base_client_ptr: Address, |
| 109 | +} |
| 110 | + |
| 111 | +impl Memory { |
| 112 | + async fn init(game: &Process, _main_module_name: &str) -> Self { |
| 113 | + retry(|| { |
| 114 | + let mut ranges = game.memory_ranges(); |
| 115 | + let mut range = ranges.next()?; |
| 116 | + loop { |
| 117 | + if range.size().ok()? == 0x1000 { |
| 118 | + let val = range.address().ok()?; |
| 119 | + range = ranges.next()?; |
| 120 | + |
| 121 | + if range.size().ok()? == 0xFFFFF000 { |
| 122 | + return Some(Self { |
| 123 | + base_client_ptr: val, |
| 124 | + }); |
| 125 | + } |
| 126 | + } else { |
| 127 | + range = ranges.next()?; |
| 128 | + } |
| 129 | + } |
| 130 | + }) |
| 131 | + .await |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +async fn hook_process() -> (&'static str, Process) { |
| 136 | + retry(|| { |
| 137 | + PROCESS_NAMES |
| 138 | + .iter() |
| 139 | + .find_map(|&name| Some((name, Process::attach(name)?))) |
| 140 | + }) |
| 141 | + .await |
| 142 | +} |
| 143 | + |
| 144 | +fn update_loop(game: &Process, memory: &Memory, watchers: &mut Watchers) { |
| 145 | + // Loading state represent the current status of the loading screen |
| 146 | + let loading_state = client_layer::read_host_path::<u32>( |
| 147 | + game, |
| 148 | + memory.base_client_ptr, |
| 149 | + &[0x833678A0, 0x4, 0xE0, 0x13C], |
| 150 | + ) |
| 151 | + .unwrap_or_default() |
| 152 | + .from_be(); |
| 153 | + |
| 154 | + // This shows whether the game is effectively stuck in a loading state, regardless of the laoding screen shown |
| 155 | + let is_loading = |
| 156 | + client_layer::read_host_path::<u8>(game, memory.base_client_ptr, &[0x83367A4C]) |
| 157 | + .map(|val| val != 0) |
| 158 | + .unwrap_or(false); |
| 159 | + |
| 160 | + watchers |
| 161 | + .is_loading |
| 162 | + .update_infallible(is_loading || (loading_state != 0 && loading_state != 2)); |
| 163 | + |
| 164 | + // We want to store the internal ID of the current level. In reality we are just checking this for the world map (which should return an empty string) |
| 165 | + let stage = client_layer::read_host_path::<u8>( |
| 166 | + game, |
| 167 | + memory.base_client_ptr, |
| 168 | + &[0x83367900, 0x8, 0xAC, 0x0], |
| 169 | + ) |
| 170 | + .unwrap_or_default(); |
| 171 | + |
| 172 | + let igt = if stage == 0 { |
| 173 | + Duration::ZERO |
| 174 | + } else { |
| 175 | + client_layer::read_host_path::<f32>(game, memory.base_client_ptr, &[0x83367900, 0x8, 0x5C]) |
| 176 | + .map(|val| val.from_be()) |
| 177 | + .map(|val| { |
| 178 | + if val.is_nan() || val < 0.0 { |
| 179 | + Duration::ZERO |
| 180 | + } else { |
| 181 | + Duration::milliseconds((val * 100.0) as i64 * 10) |
| 182 | + } |
| 183 | + }) |
| 184 | + .unwrap_or_default() |
| 185 | + }; |
| 186 | + |
| 187 | + let old_igt = watchers.igt.pair.map(|val| val.current).unwrap_or_default(); |
| 188 | + |
| 189 | + if igt < old_igt { |
| 190 | + watchers.igt_buffer += old_igt; |
| 191 | + } |
| 192 | + |
| 193 | + watchers.igt.update_infallible(igt); |
| 194 | +} |
| 195 | + |
| 196 | +fn start(_watchers: &Watchers, _settings: &Settings) -> bool { |
| 197 | + false |
| 198 | +} |
| 199 | + |
| 200 | +fn split(_watchers: &Watchers, _settings: &Settings) -> bool { |
| 201 | + false |
| 202 | +} |
| 203 | + |
| 204 | +fn reset(_watchers: &Watchers, _settings: &Settings) -> bool { |
| 205 | + false |
| 206 | +} |
| 207 | + |
| 208 | +fn is_loading(watchers: &Watchers, settings: &Settings) -> Option<bool> { |
| 209 | + match settings.igt { |
| 210 | + true => Some(true), |
| 211 | + false => watchers.is_loading.pair.map(|val| val.current), |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +fn game_time(watchers: &Watchers, settings: &Settings, _memory: &Memory) -> Option<Duration> { |
| 216 | + match settings.igt { |
| 217 | + false => None, |
| 218 | + true => watchers |
| 219 | + .igt |
| 220 | + .pair |
| 221 | + .map(|val| val.current + watchers.igt_buffer), |
| 222 | + } |
| 223 | +} |
| 224 | + |
| 225 | +const PROCESS_NAMES: &[&str] = &["UnleashedRecomp.exe", "UnleashedRecomp"]; |
0 commit comments