Перейти до вмісту
EnshwickEnshwickальфа
Завантажити

Before you reach into the game

Цей контент ще не доступний вашою мовою.

The API has a call for most of what mods do. When it has none, a Rust mod reaches into the game itself. Each way to do that costs more to keep working than the one before it.

An API call keeps working when the game updates, since the loader finds the game’s code for it on every build. It also composes: when several mods answer the same call, the loader runs them in load order. A mod that reaches into the game must fit each new build itself. The loader still chains every mod’s hooks on one function, but each link must call on for the others to run (Hooks).

The same altar limit, first as the API’s call:

fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
load.on_altar_limit(|limit| limit.allowed = 16)
}

Then as a hook on the game’s own lookup:

use enshwick::{Load, Mod, Next, Off};
struct AltarCount;
enshwick::detour! {
static MAX_ALTARS: unsafe extern "system" fn(balancing: u64, level: u32) -> u8 = max_altars;
}
fn max_altars(next: Next<'_, unsafe extern "system" fn(u64, u32) -> u8>, balancing: u64, level: u32) -> u8 {
match AltarCount::instance() {
Some(_) => 16,
// SAFETY: the original takes the arguments the game passed.
None => unsafe { next.call(balancing, level) },
}
}
impl Mod for AltarCount {
fn init(load: &mut Load<'_>) -> Result<Self, Off> {
let max_altars = load.symbol("building.max_altars")?;
// SAFETY: the game's altar limit has the type `MAX_ALTARS` declares.
unsafe { load.hook(&MAX_ALTARS, max_altars) }?;
Ok(Self)
}
}
enshwick::declare_mod!(AltarCount);

The hook states the function’s type in unsafe code, and the compiler cannot check it. When a game update changes that function, the mod must change too. Where a call exists, use it. Where other mods would need the same hook, ask for a call.

WayWhat the mod doesWhen the game updates
The game’s own typesreads and writes components and events by their reflected namesthe loader reads the new build’s reflection
Game resources as valuesreads a resource of the game’s archive by its type name and serves a changed copythe loader reads the new build’s reflection
Hooksputs a function in front of a game functiona system found by name, or a symbol the loader pins, keeps working; a mod’s own pinned symbol needs a new row
Mid-function hooksruns code at one instruction inside a functiona pinned symbol needs a new row, and a byte pattern finds the place while those bytes stay
Code patchesrewrites bytes of the game’s codeas for a mid-function hook

Finding the game shows how a mod asks the loader for a function, a field or a value by name. A mod that asks by name never holds an address. Code in the game’s frame, Pacing and threads and ECS inside systems cover what code inside the game’s frame may do.

A Luau mod has no hooks or code patches: a script runs in a sandbox without pointers (Where Luau differs from Rust). It reads and writes the game’s types and resources as values, through enshwick.typed and enshwick.resources.

A lookup that finds nothing returns Off: the mod stays off, and the game runs vanilla where it would have acted. A panic in a hook turns only that mod off. A hook with the wrong type, or a patch that writes the wrong bytes, can crash the game. The crash file names the mod (The life of a mod).