Skip to content

Commit eff78c1

Browse files
committed
Initial upload
0 parents  commit eff78c1

7 files changed

Lines changed: 285 additions & 0 deletions

File tree

.cargo/config.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build]
2+
target = "wasm32-unknown-unknown"
3+
rustflags = [
4+
# The auto splitting runtime supports all the following WASM features.
5+
"-C", "target-feature=+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext,+simd128,+relaxed-simd,+tail-call",
6+
]

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
/target/
4+
5+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7+
Cargo.lock
8+
9+
# These are backup files generated by rustfmt
10+
**/*.rs.bk

Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "livesplit_unleashedrecompiled"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
asr = { git = "https://github.com/LiveSplit/asr", features = ["signature", "derive"] }
10+
bytemuck = { version = "1.22.0", features = ["derive", "min_const_generics"] }
11+
12+
[lib]
13+
crate-type = ["cdylib"]
14+
15+
[profile.release]
16+
lto = true
17+
panic = "abort"
18+
codegen-units = 1
19+
strip = true
20+
21+
[profile.release.build-override]
22+
opt-level = 0

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<h1> <img src="https://raw.githubusercontent.com/SonicSpeedrunning/LiveSplit.UnleashedRecompiled/main/sonic_icon.png" alt="SonicCD" height="75" align="middle" /> Unleashed Recompiled - Load remover</h1>
2+
3+
Load remover for Unleashed Recompiled with optional support for in-game timing

sonic_icon.png

26 KB
Loading

src/client_layer.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use asr::{Address, FromEndian, Process};
2+
use bytemuck::CheckedBitPattern;
3+
4+
pub(crate) fn read_host_path<T: CheckedBitPattern>(
5+
process: &Process,
6+
base_address: Address,
7+
offsets: &[u32],
8+
) -> Option<T> {
9+
let mut address = base_address;
10+
11+
let (&last, path) = offsets.split_last()?;
12+
13+
for &offset in path {
14+
let uaddress = process.read::<u32>(address + offset).ok()?.from_be();
15+
address = base_address + uaddress;
16+
}
17+
18+
process.read::<T>(address + last).ok()
19+
}

src/lib.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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

Comments
 (0)