The loader and the ABI
Two DLLs
Section titled “Two DLLs”The loader comes in two parts:
powrprof.dll, the stub, in the folder ofenshrouded.exeorenshrouded_server.exe. Windows loads it with the game. It forwards every export to the system’spowrprof.dll, then, on a thread of its own, loads the DLLs inmods/chain/and then the loader. Without the loader the game runs vanilla.mods/enshwick/enshwick.dll, the loader. It identifies the game build, loads the mods, initialises them, arms their hooks and answers their calls.
On a game build the loader has no table for, it finds what it needs by code
patterns; a mod whose entries are not found stays off with the reason, and
build_patterns = false keeps such a build vanilla instead; see
Finding the game.
How mods are found
Section titled “How mods are found”Every *.dll directly in mods/ is a candidate, taken in file name order. The
loader loads each and keeps it when it exports the four required functions of
abi::exports
and was built for the loader’s ABI version. The fifth, enshwick_mod_shutdown,
is optional. A DLL without enshwick_mod_abi is a native mod: it stays loaded
and runs on its own, and the status summary and the Mods screen list it. One
with only part of the interface is logged and unloaded. The file name is free; the id in the manifest
names the mod. Every folder in mods/ with a pack.toml is a
data pack, which the loader runs as a mod of its
own; its entry’s file is <id>/pack.toml.
The mod list and the lock
Section titled “The mod list and the lock”mods/enshwick/mods.toml names every mod the loader has found, by id, with
its file. The loader writes it on first start with every mod turned on, adds
mods it finds later, and keeps everything else, comments included:
[you.trading-post]enabled = false # off until the next releasefile = "trading_post.dll"A file whose entry says enabled = false is never loaded, so none of its code
runs; loader.log says trading_post.dll skipped: off in mods/enshwick/mods.toml. links_off = ["you.trading-post:price"] in an entry keeps the
mod on and only its link on that extension point off. Changes take effect at the next start. A list that
is not valid TOML is left as it is and every mod loads, with a warning.
enshwick on the console and the status summary name the mods that are off.
mods/enshwick/enshwick.lock is written at every start: the loader’s version
and SHA-256, the game build, and each running mod’s id, version, file and
SHA-256, in load order, then under [[replaced]] the mods a present mod
replaces (see replaces in the manifest). Copying it with the DLLs sets up the same server
again, and comparing two locks shows where two servers differ. What changed
since the last start goes to loader.log, such as
since the last start: you.trading-post 0.3.0 -> 0.3.1.
The ABI
Section titled “The ABI”A mod and the loader are built separately, so they talk through a C ABI,
enshwick::abi:
ABI_VERSIONis a hash of the layout of every ABI type, with each field’s type and each function pointer’s signature. A loader loads only mods built for its own version and tells the player which Enshwick release to rebuild any other mod for. Structs that cross the boundary on their own start with aHeader(size and ABI version).HostApiis the loader’s table of functions, passed to the mod at init.
A mod rarely touches the ABI. Load
wraps the table during init, and
host()
returns a Host afterwards, from any thread, for logging, fault,
hosting() and the raw table. Each mod DLL links its own copy of the crate,
so host() always answers for the mod that calls it. The
ABI reference has the details.
The manifest
Section titled “The manifest”manifest!
builds the static data the loader reads before it runs any of the mod’s code.
Fields go in this order; only id and side are required:
let manifest = enshwick::manifest!( id: "you.trading-post", // <author>.<name>, each of [a-z0-9-] side: BOTH, // SERVER, CLIENT or BOTH version: "0.2.0", // the crate's version when left out name: "Trading post", // what players read; the id when left out description: "Trade with travelling merchants", authors: ["Your Name"], license: "MIT OR Apache-2.0", homepage: "https://github.com/you/trading-post", join: true, // counts in the join check reload: true, // a development server may reload it requires: ["market-stalls" => ">=0.2, <1.0"], after: ["market-stalls"], recommends: ["caravans" => ">=0.3, <1.0"], breaks: ["old-market" => "<0.5"], replaces: ["barter"], // a mod this one folds in was: ["trade-post"], // ids this mod had before a rename builds: [{ side: SERVER, guid: "1c129660cd6efc4f9f0b21652d5ff2bf", age: 387, symbols: ["survivors.spawn_return" => 0x4_05cb], constants: ["survivors.bed_id" => 0x48], }],);idis<author>.<name>, each part of[a-z0-9-], at most 64 characters in all. It names the mod in logs, its settings filemod-configs/<id>.tomland its data foldermod-data/<id>, and is the namespace of its content, stats, points, services and events (you.trading-post:prices). An invalid id fails the build;enshwickis the loader’s own and is refused.side: see Sides and hosting.versionis SemVer. Leave it out so the DLL reports the version you release; the loader names it in every message about the mod.name,description,authors,licenseandhomepageare what players read: on the Mods screen, in the status summary and in the join check’s notes, wherehomepageis the place a player who lacks the mod is sent to. Each butnamecomes from the crate’sCargo.tomlwhen left out (homepagefalls back torepository); a mod without a name is shown by its id. The mod’s texts translate its name and description undermod.nameandmod.description.join: true: see The join check.reload: true: a development server may reload the mod while it runs; see Testing.developer: true: the mod is a developer tool, such as a probe that logs what the game does. While it runs, every player reads a warning that developer tools are on; see The interface.requiresnames mods that must be present in a Cargo-style version range; they load first.afternames mods that load first when present.recommendsnames mods that work well beside this one: they load first when present, and the loader notes inloader.logand the status summary when one is missing or outside its range. The mod loads either way.breaksnames mods this one does not work beside, in a version range (""for any): when one of them loads, this mod is skipped withdoes not work beside <id> <version>; remove one of them.- A dev build, version
0.0.0-dev, meets every range ofrequiresandrecommends, and no range ofbreaksnames it, as two dev builds join each other. replacesnames mods this one takes the place of, such as mods it folds in. Players rarely delete the old DLL, so without it both would run. While this mod is present, the loader never starts a replaced mod: it unloads that DLL right after reading its manifest and logs once<old> is replaced by <new>; delete mods/<file>, whichenshwickon the console and the status summary repeat and the lock lists under[[replaced]]. Their settings stay where they are; take the ones this mod keeps with a migration.wasnames the ids this mod had before a rename. It replaces them asreplacesdoes, arequiresor an extension point named by an old id finds this mod, and on the first start the loader copiesmod-configs/<old>.toml, its texts file and everything inmod-data/<old>/(each world’s settings and stores, a hold) to the new id, where the new id has no such file yet, and counts the migrations that ran under the old id as run. The old files stay, for a player who goes back.migrations.tomlrecords the copy under[carried], so it happens once. The mod may keep its content, stats, item data, points, services and events under the old id,<old>:<name>, so the ids and GUIDs saved worlds hold stay; a bare stat or item-data name takes the first id underwas. The console, the Mods screen and other mods’ links find the mod by an old id too.pointslists the extension points the mod defines, by their input types. Their schemas go into the manifest and into the DLL’s.enshptsection, wherecargo enshwick pointreads them to write other mods’ definitions.buildspins symbols and constants for game builds the loader’s table does not cover, per side, by the build’s GUID and age; see Finding the game.
The mod
Section titled “The mod”A mod implements
Mod on its
state type:
manifest()returns themanifest!;init(load)registers hooks, settings, events and content, and returns the state or anOff;ready(&'static self, host), optional, runs once the hooks are armed;shutdown(&'static self), optional, runs before a development reload unloads a mod withreload: true.
Off::new(reason) turns the mod off with a reason for the log. Every fallible
call on Load returns Result<_, Off>, so ? is the usual way out.
declare_mod!
exports the type: enshwick_mod_abi, enshwick_mod_query,
enshwick_mod_init, enshwick_mod_ready and enshwick_mod_shutdown, each
catching panics. It also
adds instance(), the mod’s state once init has returned it.
struct Quiet;
impl enshwick::Mod for Quiet { fn manifest() -> &'static enshwick::abi::Manifest { enshwick::manifest!(id: "you.quiet", side: SERVER) }
fn init(_load: &mut enshwick::Load<'_>) -> Result<Self, enshwick::Off> { Err(enshwick::Off::new("nothing to do on this build")) }}
enshwick::declare_mod!(Quiet);