Skip to content

Releases: Benehiko/elden-ring-mods

ermod-engine v0.6.0

Choose a tag to compare

@Benehiko Benehiko released this 06 Oct 12:09
v0.6.0
740843a

Game build 2.7.1.0, as in v0.5.0. Every player in a co-op session needs the
same game build and this engine version.

Co-op: every player gets the runes

A joiner used to get nothing for enemies the host's game was running, and a
quarter of a boss's runes. Now every player in a session gets the full runes
of every enemy any player kills, and of every boss, however far apart they
stand. Solo play is unchanged. Each character's own rune bonuses (a rune
talisman, for example) still apply on top, as they do solo.

sdk.coop: decide who gets what

A new SDK module, sdk.coop (permission coop), for mods that decide
things per player: coop.active(), coop.is_host(), coop.get_distance()
(metres to the nearest other player, and their Steam id),
coop.get_distance(id) (metres to one player, by Steam id),
coop.distances() and coop.players() (each other player's Steam id,
handle, distance and position, nearest first). Its
coop.set_rune_rates({ enemy = 0.25, boss = 0.5 }) sets the share of runes
the player on that machine takes. The full amount (1) is the default.
sdk.watch.stat.coop_distance reports the distance as a watchable value.
See examples/coop_runes.lua and
Co-op runes.

Mods can run boss fights

sdk.bosses can now set up and run a fight, not only revive a boss:

  • bosses.warp(id) travels to the boss's grace (boss.grace) and puts the
    player beside the boss once it has loaded.
  • bosses.wake(id) makes the boss fight, and bosses.kill(id) kills it
    through the game's own death routine, so the game records the defeat.
  • bosses.reset(id) clears the defeat flag, and for an evergaol its own
    state too. It takes effect at the next map load.
  • hooks.event.on_boss_defeated gives { id, flag, kind } when the game
    records a defeat. It does not fire for the defeat flags a map load sets
    again.
  • New fields: boss.kind (evergaol, minor_erdtree, field or
    arena), boss.phases (how many kills the game needs before it records
    the defeat), boss.evergaol (the evergaol's name) and boss.spawns
    (where the boss stood before, at the start of, and through its fight).

Evergaols can be opened from a mod. For the ten base-game evergaols,
boss.can_open is true, and bosses.warp(id, { open = true }) stands the
player on the pad, answers "Enter evergaol?" and steps towards the boss.
bosses.open(id) does the same when the player is already near it.
hooks.event.on_evergaol_entered fires when the gaol takes the player in,
whether the player or the SDK opened it. Shadow of the Erdtree's 40 bosses
now have phases and coordinates recorded too. The boss at row 1039510800
is now correctly named a Night's Cavalry (nights_cavalry_1039510800), and
161 of the 210 boss names are measured in the game.

A woken boss fights for real: bosses.wake does not protect the player. See
Bringing bosses back.

on_death carries the character's death count

The on_death event now has ev.deaths, which is the character's lifetime
death count, including this death. It is the same value
sdk.watch.stat.deaths reads. When two deaths happen between frames, two
events fire, each with its own count. examples/death_ping.lua and
examples/overlay.lua show the difference between this count and a mod's
own count since it loaded.

Fixed: a stutter in mods that watch every boss

Each sdk.bosses lookup re-read the whole boss table. A mod that checked
every boss once a second (examples/boss_watch.lua) spent about 16 ms in a
single frame and made the game stutter. Lookups now take about 0.03 ms.

Fixed: co-op session check on game build 2.7.1.0

On 2.7.1.0, every runtime log said handshake-context signature did not match, and ermod-engine check-build reported a failure, because the
engine looked for a value that only matched 2.6.2.0. The engine now finds it
on any build. check-build passes on 2.7.1.0 and prints what it found.


Installing

Two archives, pick yours:

Machine Archive
Linux, x86-64 (Steam + Proton) ermod-engine-v0.6.0-linux-x86_64.tar.gz
macOS, Apple Silicon (CrossOver, Whisky or protium) ermod-engine-v0.6.0-macos-aarch64.tar.gz

Both carry the same Windows runtime; only the launcher differs. You need Elden
Ring installed through Steam and launched normally at least once.

1. Download your archive, plus SHA256SUMS and SHA256SUMS.sigstore.json,
into one directory, and verify them (below).

2. Unpack it anywhere you like; nothing is installed system-wide:

tar -xzf ermod-engine-v0.6.0-linux-x86_64.tar.gz   # or the macos-aarch64 one
cd ermod-engine-v0.6.0-linux-x86_64

On macOS, clear the quarantine first (below).

3. Check it finds your game, without launching anything:

./ermod-engine --dry-run

It names your install, your Proton or Wine, and what it would stage. If it
cannot find something, it says which check failed.

4. Play:

./ermod-engine                  # launch the game with mods

Mods go in ~/.local/share/ermod/mods (one .lua file each; examples in the
mods repository).
In the game, ` opens the engine's menu. The modded game plays on its own
profile and never opens your own save; the first launch offers to copy your
characters in.

Co-op, every player on the same release and the same game build:

./ermod-engine coop id                              # your Steam ID and addresses
./ermod-engine coop host                            # the host
./ermod-engine coop join <host-id>@<host-address>   # everyone else

INSTALL.md in the archive is the full guide, as is
docs/install.md;
docs/coop.md
covers co-op, including playing across networks.

Verifying this download

Every release is signed. The signature covers SHA256SUMS, which pins each
archive by hash, and travels beside it as SHA256SUMS.sigstore.json. Download
your archive, SHA256SUMS and SHA256SUMS.sigstore.json into one directory,
then run these two steps from it, in this order.

1. Check the signature over the checksums. Install
cosign (on a
Mac: brew install cosign; on Linux: your distribution's package, or a binary
from cosign's releases page), then:

cosign verify-blob SHA256SUMS \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity 18033717+Benehiko@users.noreply.github.com \
  --certificate-oidc-issuer https://github.com/login/oauth

It must print Verified OK. Anything else means SHA256SUMS was not signed by
us: stop, and do not run anything from the download.

2. Check your archive against the checksums.

sha256sum --ignore-missing -c SHA256SUMS            # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS        # macOS

Your archive must be listed as OK. --ignore-missing skips the archives
you did not download.

Step 1 is what makes step 2 mean anything: checksums downloaded from the same
page as the archive only prove the two agree, not that either came from us.

macOS: "cannot be opened" or "Apple could not verify"

The macOS launcher is signed ad hoc, not with an Apple Developer ID, and is not
notarized. A web browser marks what it downloads as quarantined, and macOS
refuses to run a quarantined binary it cannot trace to a registered developer.
The message reads "ermod-engine" cannot be opened because the developer cannot
be verified
, or, on macOS 15 and later, Apple could not verify
"ermod-engine" is free of malware
.

Verify the download first (above), then clear the quarantine from the
unpacked directory:

tar -xzf ermod-engine-v0.6.0-macos-aarch64.tar.gz
xattr -dr com.apple.quarantine ermod-engine-v0.6.0-macos-aarch64

Or, after the first refusal: System Settings → Privacy & Security → scroll to
the message about ermod-engine → Open Anyway.

Downloads made with curl or gh release download are not quarantined and
need neither step. Linux has no equivalent; there is nothing to do there.

ermod-engine v0.5.0

Choose a tag to compare

@Benehiko Benehiko released this 05 Oct 15:02
v0.5.0
5e309b9

Game build 2.7.1.0, as in v0.4.0. Every player in a co-op session needs the
same game build and this engine version.

Frame trace: find what makes frames slow

The engine can now record every frame's timing while you play, split into
the game's CPU work, the time spent waiting in Present (GPU, vsync or
driver), and the engine's own work, and flag the stutters.

  • Turn it on at launch in engine.cfg with frame_trace_cpu = "off" | "light" | "normal" | "detailed" or frame_trace_gpu = true | false
    (both off by default; the Engine tab keeps them when it saves a rebind),
    or at any time with ermod-engine trace start [--cpu LEVEL] [--gpu] and
    ermod-engine trace stop.
  • ermod-engine trace report [<file>] prints frame-time percentiles, the
    stutters, which side each one was on, and the worst ones.
  • --cpu samples the game's busiest threads and the report names the game
    functions and DLLs that run during stutters, with call paths through the
    game's own code. --gpu times each of the game's GPU submissions.
  • When tracing is off, the tracer itself costs the game one memory read
    per frame. Separately, the always-on stutter counter behind
    sdk.perf.spikes() runs every frame whether or not you trace. It was
    measured at about 0.5 µs a frame (median), and at most 6 µs, which is
    under 0.04 % of a 60 fps frame.
  • Mods can count stutters too: sdk.perf.spikes() returns how many frames
    the trace's spike rule has flagged, the last one's length and its frame
    number, and the bundled performance monitor shows them.

The sample rates behind light, normal and detailed (50, 100 and
250 Hz) are placeholders. The first attempt to measure their cost to frame
times was inconclusive. See
Finding stutters.

Co-op: a trace mod for debugging the shared world

A new read-only SDK module, sdk.trace (permission trace), shows what each
machine believes about a co-op session. It covers the session role, the
players and their mounts, and every enemy near the player: who owns it,
whether its owner's updates arrive, and the HP its owner last reported. It
also has the multiplayer-area barrier counters. examples/coop_trace.lua
draws all of it and flags frozen enemies, corpses standing on the other
screen, and players faded out. It logs rows keyed by the enemy's handle,
which is the same on every machine.

Every machine in a session sends its view to the others once a second, so
the host collects the whole session. Its overlay shows each joiner's view
beside its own, with the enemies the two machines disagree on. Its log
holds every machine's rows and a line whenever a disagreement starts or
ends. The overlay is in collapsible sections, with characters sorted and
the flagged ones first. Mods can use the same widgets: sdk.ui.collapsing
and sdk.ui.tree. See
trace in the scripting guide.

Co-op: a joiner arrives beside the host, so players see each other right

A joiner used to load where its own save stood, and its game then placed
things relative to a different spot than the host's. Players appeared a few
metres off and faced past each other, and a player on Torrent vanished from
the other screen. Now coop join asks the host where it stands before your
game starts and loads your character right there, the way a summon arrives
at the host. Your own respawn grace is unchanged: if you die, you respawn at
your grace, not at the host.

The host must be in its world when you join. If the host is on Torrent,
coop join says so and waits up to 90 seconds for it to get off. If the
host does not answer within a few seconds, you load where your save stands
and the log says why. Both players need this version.

A joiner is also never pulled back to a grace in the middle of a session
any more; calling Torrent used to trigger that.

Co-op: Torrent stays under its rider after a death

After a player died and respawned somewhere else, the other machine drew
their Torrent, and the rider on it, far away (exactly one map tile), so a
mounted player vanished. Each machine now tells the others how its game
places its own horse, and the others place it the same way.

A player who stays on Torrent while the other player dies (or travels) is
no longer left floating on that player's screen until they whistle again:
the returning player's game puts them back on their horse.

Co-op: a joiner's death keeps the time of day

A joiner who died came back at 07:00 while the host's world stayed at its
own time. The joiner's respawn now keeps the running clock, as the host's
already did.

Every boss by name

Almost every boss now has a name in sdk.bosses.id (210 of 212):
sdk.bosses.id.starscourge_radahn, sdk.bosses.id.messmer_the_impaler.
Every name is the game's own, spelled as the game spells it.
boss.name_measured is true where the name was read off the boss itself in
the game, and false where it is assigned by encounter and not yet checked
live; a mod that needs certainty can check it.

Co-op: a joiner sees the host's dead enemies dead

An enemy the host has already killed no longer stands alive in a joiner's
world. A joiner also takes the host's health for an enemy the host had
damaged before the joiner arrived, where before it only matched when both
players' enemies sat in the same order in memory.

Co-op: enemies near a joiner come alive away from the host

The host's game only runs the enemies around the host. A joiner exploring
elsewhere met enemies that stood frozen and ignored it. The joiner's game now
runs the enemies the host is not running.

The overlay stays off the game's HUD

The ermod menu and mod windows now open in the top-right corner instead of
over your health, FP and stamina bars. The overlay also scales with your
resolution: text and spacing grow and shrink with the display height (1080p
is the reference), and no window takes more than 40% of the screen's width
or 70% of its height. Anything longer scrolls.

For mod authors: x/y in sdk.ui.window options now count inward from
the window's anchor corner, which defaults to "top_right". To keep a
window where it was, pass anchor = "top_left". "bottom_left" and
"bottom_right" are available too. Positions and sizes are in 1080p pixels
and are scaled to the display.


Installing

Two archives, pick yours:

Machine Archive
Linux, x86-64 (Steam + Proton) ermod-engine-v0.5.0-linux-x86_64.tar.gz
macOS, Apple Silicon (CrossOver, Whisky or protium) ermod-engine-v0.5.0-macos-aarch64.tar.gz

Both carry the same Windows runtime; only the launcher differs. You need Elden
Ring installed through Steam and launched normally at least once.

1. Download your archive, plus SHA256SUMS and SHA256SUMS.sigstore.json,
into one directory, and verify them (below).

2. Unpack it anywhere you like; nothing is installed system-wide:

tar -xzf ermod-engine-v0.5.0-linux-x86_64.tar.gz   # or the macos-aarch64 one
cd ermod-engine-v0.5.0-linux-x86_64

On macOS, clear the quarantine first (below).

3. Check it finds your game, without launching anything:

./ermod-engine --dry-run

It names your install, your Proton or Wine, and what it would stage. If it
cannot find something, it says which check failed.

4. Play:

./ermod-engine                  # launch the game with mods

Mods go in ~/.local/share/ermod/mods (one .lua file each; examples in the
mods repository).
In the game, ` opens the engine's menu. The modded game plays on its own
profile and never opens your own save; the first launch offers to copy your
characters in.

Co-op, every player on the same release and the same game build:

./ermod-engine coop id                              # your Steam ID and addresses
./ermod-engine coop host                            # the host
./ermod-engine coop join <host-id>@<host-address>   # everyone else

INSTALL.md in the archive is the full guide, as is
docs/install.md;
docs/coop.md
covers co-op, including playing across networks.

Verifying this download

Every release is signed. The signature covers SHA256SUMS, which pins each
archive by hash, and travels beside it as SHA256SUMS.sigstore.json. Download
your archive, SHA256SUMS and SHA256SUMS.sigstore.json into one directory,
then run these two steps from it, in this order.

1. Check the signature over the checksums. Install
cosign (on a
Mac: brew install cosign; on Linux: your distribution's package, or a binary
from cosign's releases page), then:

cosign verify-blob SHA256SUMS \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity 18033717+Benehiko@users.noreply.github.com \
  --certificate-oidc-issuer https://github.com/login/oauth

It must print Verified OK. Anything else means SHA256SUMS was not signed by
us: stop, and do not run anything from the download.

2. Check your archive against the checksums.

sha256sum --ignore-missing -c SHA256SUMS            # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS        # macOS

Your archive must be listed as OK. --ignore-missing skips the archives
you did not download.

Step 1 is what makes step 2 mean anything: checksums downloaded from the same
page as the archive only prove the two agree, not that either came from us.

macOS: "cannot be opened" or "Apple could not verify"

The macOS launcher is signed ad hoc, not with an Apple Developer I...

Read more

ermod-engine v0.4.0

Choose a tag to compare

@Benehiko Benehiko released this 03 Oct 19:14
v0.4.0
066ccc9

Game build 2.7.1.0, as in v0.3.2. Every player in a co-op session needs the
same game build and this engine version.

Before you update: two changes for mod authors

  • One rules permission for every game rule. A mod that changes a rule
    now asks for "rules", instead of a permission named after the rule
    (boss_spectate). A manifest that still lists a rule's name is refused as
    an unknown permission. Which rules a mod changes is still checked, and a
    conflict between two mods still names the rule.
  • ermod-engine mod check is now mod verify, with no alias.

Every mod is verified before the game loads it

mod verify runs each mod on your machine in a sandboxed process that can
open no files, start no programs and reach no network, and the game loads only
mods whose exact bytes this engine build has verified. A mod that crashes or
misbehaves in the check is rejected. The engine menu shows each mod's
verification.

Co-op: spirit ashes are shared

A spirit one player summons now appears in every player's game, and leaves
when it is dismissed. A Mimic Tear looks like its owner on
every screen, and the blob it starts as goes away once the copy appears.

Co-op: the whole map is open

A session was confined to the area it began in: a white wall at the area's
edge and a warp back inside past it, so a host could not even leave the
Stranded Graveyard. In co-op there is no wall and no warp back.

Co-op: arriving together

  • Two players who load onto the same spot are moved a step apart instead of
    standing inside each other, which could trap and kill one of them.
  • A joiner whose save was written on horseback stays mounted, on their own
    Torrent. They used to land on the host's horse and be thrown.
  • A character that had spent time joining as a guest could make a co-op load
    drop both players from the sky. Every launch now repairs the save.
  • A player who drops out and rejoins quickly gets their character and horse
    again.

Co-op: losing to the Grafted Scion

While a teammate fights on, losing to the Grafted Scion in the Chapel of
Anticipation is a death you spectate from, as for any boss. Your loss
cutscene used to carry the living teammate into the next map with you. The
last player's loss plays as usual.

Summon spirits anywhere

The new rule spirit_summon_anywhere (off by default) lets spirit ashes be
summoned outside summoning pools, and the spirit stays. See
examples/summon_anywhere.lua.

Every item by name: sdk.items

sdk.items.<table>.<name> is the row id of any item, spell, skill, Ash of
War or class, named as the game names it (6794 of them), and
sdk.items.file.<table> is its param file, so a mod can write
sdk.params rows by name. See examples/class_flasks.lua.

Fixed: "?" in the engine menu

Dashes in menu text, the join-request window's title among them, showed as
"?". They draw correctly.

Mods can bring bosses back

A mod with the bosses permission can revive any boss in the base game or
Shadow of the Erdtree, the one-off field bosses included:
sdk.bosses.revive(10000850) brings Margit back, and
sdk.bosses.revive_all({ dlc = false }) brings back every base-game boss.
sdk.bosses.all lists all 212 encounters (map, DLC or not, rune reward), taken
from the game's own data. A revived boss is back the next time its map loads:
rest at a grace, warp or die. Its one-off drop does not come back. See
bosses in the scripting guide
and examples/boss_rematch.lua.

Events and param files are enums, like stats

Every game property a mod names now has a typed spelling on the SDK:
sdk.hooks.on(sdk.hooks.event.on_death, fn) and
sdk.params.row(sdk.params.file.CharaInitParam, 3000), alongside the existing
sdk.watch.stat.hp. The type stubs declare each one as a LuaLS ---@enum,
so an editor completes the names and flags one that does not exist; in the
game, indexing a name that does not exist (sdk.hooks.event.on_levelup) is an
error where it is written. sdk.params.file lists only files the engine has
a paramdef for, under the names the game loads them by (ItemLotParam_map,
ItemLotParam_enemy). Existing mods that pass plain strings keep working.

Mods can watch HP, stats, runes and deaths

A mod with the watch permission can call sdk.watch.on(sdk.watch.stat.hp, fn) and be
told the old and new value on the frame it changes, or sdk.watch.get("vigor")
to read one at any time. Watchable: deaths, runes, level, the eight
stats, hp and hp_max. FP, stamina and flasks are not yet available. See
watch in the scripting guide.

Make a character without character creation

ermod-engine character new --class samurai --keepsake golden-seed --name Sam
writes a fresh character of any of the ten starting classes into your
profile's save and points Continue at it. It loads in the Chapel of
Anticipation with the class's weapons, armour, spells and stats, read from
the game's own regulation. character list and character delete manage the
slots. --regulation builds the class as a baked mod defines it, and the
dev save set fields (grace=, level=, runes= …) apply in the same
command. See
Profiles, and your own save.

Shell completion

ermod-engine completion bash|zsh|fish prints a completion script. Every
command, option, class, keepsake, profile and path completes.

Fixed: "Save data is corrupted" after authoring a fresh character

Changing the Steam id, grace or level of a character that had not yet picked
up a tutorial message wrote those fields 0x3FC bytes from where they belong.
The game then refused the save. Such characters now walk and write
correctly.


Installing

Two archives, pick yours:

Machine Archive
Linux, x86-64 (Steam + Proton) ermod-engine-v0.4.0-linux-x86_64.tar.gz
macOS, Apple Silicon (CrossOver, Whisky or protium) ermod-engine-v0.4.0-macos-aarch64.tar.gz

Both carry the same Windows runtime; only the launcher differs. You need Elden
Ring installed through Steam and launched normally at least once.

1. Download your archive, plus SHA256SUMS and SHA256SUMS.sigstore.json,
into one directory, and verify them (below).

2. Unpack it anywhere you like; nothing is installed system-wide:

tar -xzf ermod-engine-v0.4.0-linux-x86_64.tar.gz   # or the macos-aarch64 one
cd ermod-engine-v0.4.0-linux-x86_64

On macOS, clear the quarantine first (below).

3. Check it finds your game, without launching anything:

./ermod-engine --dry-run

It names your install, your Proton or Wine, and what it would stage. If it
cannot find something, it says which check failed.

4. Play:

./ermod-engine                  # launch the game with mods

Mods go in ~/.local/share/ermod/mods (one .lua file each; examples in the
mods repository).
In the game, ` opens the engine's menu. The modded game plays on its own
profile and never opens your own save; the first launch offers to copy your
characters in.

Co-op, every player on the same release and the same game build:

./ermod-engine coop id                              # your Steam ID and addresses
./ermod-engine coop host                            # the host
./ermod-engine coop join <host-id>@<host-address>   # everyone else

INSTALL.md in the archive is the full guide, as is
docs/install.md;
docs/coop.md
covers co-op, including playing across networks.

Verifying this download

Every release is signed. The signature covers SHA256SUMS, which pins each
archive by hash, and travels beside it as SHA256SUMS.sigstore.json. Download
your archive, SHA256SUMS and SHA256SUMS.sigstore.json into one directory,
then run these two steps from it, in this order.

1. Check the signature over the checksums. Install
cosign (on a
Mac: brew install cosign; on Linux: your distribution's package, or a binary
from cosign's releases page), then:

cosign verify-blob SHA256SUMS \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity 18033717+Benehiko@users.noreply.github.com \
  --certificate-oidc-issuer https://github.com/login/oauth

It must print Verified OK. Anything else means SHA256SUMS was not signed by
us: stop, and do not run anything from the download.

2. Check your archive against the checksums.

sha256sum --ignore-missing -c SHA256SUMS            # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS        # macOS

Your archive must be listed as OK. --ignore-missing skips the archives
you did not download.

Step 1 is what makes step 2 mean anything: checksums downloaded from the same
page as the archive only prove the two agree, not that either came from us.

macOS: "cannot be opened" or "Apple could not verify"

The macOS launcher is signed ad hoc, not with an Apple Developer ID, and is not
notarized. A web browser marks what it downloads as quarantined, and macOS
refuses to run a quarantined binary it cannot trace to a registered developer.
The message reads "ermod-engine" cannot be opened because the developer cannot
be verified
, or, on macOS 15 and later, Apple could not verify
"ermod-engine" is free of malware
.

Verify the download first (above), then clear the quarantine from the
unpacked directory:

tar -xzf ermod-engine-v0.4.0-macos-aarch64.tar.gz
...
Read more

ermod-engine v0.3.2

Choose a tag to compare

@Benehiko Benehiko released this 02 Oct 11:05
v0.3.2
6538d01

Game build 2.7.1.0, as in v0.3.1. Every player in a co-op session needs the
same game build and this engine version: an older engine neither says goodbye
nor drops a player who left.

Co-op: leaving actually leaves, and a crashed player is dropped

"Leave co-op" in the engine menu, the new ermod-engine coop leave, and
quitting the game now tell every other player. Their games remove you and
despawn your character at once, and you keep playing alone in your own
world. Until now Leave told no one, and your character stood frozen in every
other world for the rest of their session.

A player whose game crashes cannot say goodbye. The others now drop it after
45 seconds without a word from it, and its character disappears. A joiner
whose host is gone leaves the session as well.

Every player needs this version: an older engine neither says goodbye nor
drops anyone.

Co-op: coop join stops when the game closes

If the game exits while coop join waits for the host to admit you, the command
now says so and ends. It used to print still waiting until its 15-minute limit.

Co-op: a joiner no longer hangs on a map load on game build 2.7.1.0

A player who joined a host and then warped to another map (for example from a
dungeon grace to the host's grace) could freeze on a loading screen. The engine
used an address from the previous game build to tell the game who hosts the
session, so the game never learned it. On 2.7.1.0 the load now finishes after a
short wait.

Co-op: coop join says when your game is stuck on a loading screen

After you join, coop join keeps watching your game. If the game sits in one
map-load step for about 30 seconds, it says so, calls it the known joiner hang,
and points you to ermod-runtime.log to send with a bug report. It says so once,
and says when the load finishes.


Installing

Two archives, pick yours:

Machine Archive
Linux, x86-64 (Steam + Proton) ermod-engine-v0.3.2-linux-x86_64.tar.gz
macOS, Apple Silicon (CrossOver, Whisky or protium) ermod-engine-v0.3.2-macos-aarch64.tar.gz

Both carry the same Windows runtime; only the launcher differs. You need Elden
Ring installed through Steam and launched normally at least once.

1. Download your archive, plus SHA256SUMS and SHA256SUMS.sigstore.json,
into one directory, and verify them (below).

2. Unpack it anywhere you like; nothing is installed system-wide:

tar -xzf ermod-engine-v0.3.2-linux-x86_64.tar.gz   # or the macos-aarch64 one
cd ermod-engine-v0.3.2-linux-x86_64

On macOS, clear the quarantine first (below).

3. Check it finds your game, without launching anything:

./ermod-engine --dry-run

It names your install, your Proton or Wine, and what it would stage. If it
cannot find something, it says which check failed.

4. Play:

./ermod-engine                  # launch the game with mods

Mods go in ~/.local/share/ermod/mods (one .lua file each; examples in the
mods repository).
In the game, ` opens the engine's menu. The modded game plays on its own
profile and never opens your own save; the first launch offers to copy your
characters in.

Co-op, every player on the same release and the same game build:

./ermod-engine coop id                              # your Steam ID and addresses
./ermod-engine coop host                            # the host
./ermod-engine coop join <host-id>@<host-address>   # everyone else

INSTALL.md in the archive is the full guide, as is
docs/install.md;
docs/coop.md
covers co-op, including playing across networks.

Verifying this download

Every release is signed. The signature covers SHA256SUMS, which pins each
archive by hash, and travels beside it as SHA256SUMS.sigstore.json. Download
your archive, SHA256SUMS and SHA256SUMS.sigstore.json into one directory,
then run these two steps from it, in this order.

1. Check the signature over the checksums. Install
cosign (on a
Mac: brew install cosign; on Linux: your distribution's package, or a binary
from cosign's releases page), then:

cosign verify-blob SHA256SUMS \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity 18033717+Benehiko@users.noreply.github.com \
  --certificate-oidc-issuer https://github.com/login/oauth

It must print Verified OK. Anything else means SHA256SUMS was not signed by
us: stop, and do not run anything from the download.

2. Check your archive against the checksums.

sha256sum --ignore-missing -c SHA256SUMS            # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS        # macOS

Your archive must be listed as OK. --ignore-missing skips the archives
you did not download.

Step 1 is what makes step 2 mean anything: checksums downloaded from the same
page as the archive only prove the two agree, not that either came from us.

macOS: "cannot be opened" or "Apple could not verify"

The macOS launcher is signed ad hoc, not with an Apple Developer ID, and is not
notarized. A web browser marks what it downloads as quarantined, and macOS
refuses to run a quarantined binary it cannot trace to a registered developer.
The message reads "ermod-engine" cannot be opened because the developer cannot
be verified
, or, on macOS 15 and later, Apple could not verify
"ermod-engine" is free of malware
.

Verify the download first (above), then clear the quarantine from the
unpacked directory:

tar -xzf ermod-engine-v0.3.2-macos-aarch64.tar.gz
xattr -dr com.apple.quarantine ermod-engine-v0.3.2-macos-aarch64

Or, after the first refusal: System Settings → Privacy & Security → scroll to
the message about ermod-engine → Open Anyway.

Downloads made with curl or gh release download are not quarantined and
need neither step. Linux has no equivalent; there is nothing to do there.

ermod-engine v0.3.1

Choose a tag to compare

@Benehiko Benehiko released this 30 Sep 19:31
v0.3.1
12dd09b

Game build 2.7.1.0, as in v0.3.0. Every player in a co-op session needs
the same game build and this engine version: a v0.3.1 joiner waits for
the host to admit it, which a v0.3.0 host never does.

Co-op: a player joins only once the host has let them in (2026-09-30)

A player who connects to your session is no longer part of it straight away.
They become a member only once their game-changing mods are exactly yours and
the host has let them in. Until then they cannot change anything in your world
(respawns, resting, enemy health, spectating), and a player the host refused
never reaches the game at all. If a member's mods stop matching mid-session,
they are paused until they match again.

coop join says joined only once the host has admitted you. If the host
refuses you, or does not answer in time, it names the host and gives the
reason.

Co-op: the host can approve each player who joins (2026-09-30)

Set in engine.cfg:

coop_join_approval = "ask"   # default "auto": every player whose mods match is let in

With ask, a player whose mods match waits. While the ermod menu is closed,
a notice tells the host who is waiting; the menu lists them with Allow and
Refuse. The joiner's game does not start joining until the host allows
it, so the host can take as long as they need. Allow has been tested live;
Refuse is covered by tests but has not yet been run in a live session.

Skipping the title menu no longer plays the title music in the world (2026-09-30)

With the title menu skipped, the title theme kept playing over the world. It
now stops the same way it does when you press Continue yourself.

Faster, checked address updates after a game patch (2026-09-30)

Behind the scenes: the addresses the engine uses are now carried from one game
build to the next by a tool that compares the two game executables, with every
carried address checked against the hand-verified tables. This release changes
nothing for supported builds; it makes support for the next game patch quicker
to deliver and less likely to be wrong.


Installing

Two archives, pick yours:

Machine Archive
Linux, x86-64 (Steam + Proton) ermod-engine-v0.3.1-linux-x86_64.tar.gz
macOS, Apple Silicon (CrossOver, Whisky or protium) ermod-engine-v0.3.1-macos-aarch64.tar.gz

Both carry the same Windows runtime; only the launcher differs. You need Elden
Ring installed through Steam and launched normally at least once.

1. Download your archive, plus SHA256SUMS and SHA256SUMS.sigstore.json,
into one directory, and verify them (below).

2. Unpack it anywhere you like; nothing is installed system-wide:

tar -xzf ermod-engine-v0.3.1-linux-x86_64.tar.gz   # or the macos-aarch64 one
cd ermod-engine-v0.3.1-linux-x86_64

On macOS, clear the quarantine first (below).

3. Check it finds your game, without launching anything:

./ermod-engine --dry-run

It names your install, your Proton or Wine, and what it would stage. If it
cannot find something, it says which check failed.

4. Play:

./ermod-engine                  # launch the game with mods

Mods go in ~/.local/share/ermod/mods (one .lua file each; examples in the
mods repository).
In the game, ` opens the engine's menu. The modded game plays on its own
profile and never opens your own save; the first launch offers to copy your
characters in.

Co-op, every player on the same release and the same game build:

./ermod-engine coop id                              # your Steam ID and addresses
./ermod-engine coop host                            # the host
./ermod-engine coop join <host-id>@<host-address>   # everyone else

INSTALL.md in the archive is the full guide, as is
docs/install.md;
docs/coop.md
covers co-op, including playing across networks.

Verifying this download

Every release is signed. The signature covers SHA256SUMS, which pins each
archive by hash, and travels beside it as SHA256SUMS.sigstore.json. Download
your archive, SHA256SUMS and SHA256SUMS.sigstore.json into one directory,
then run these two steps from it, in this order.

1. Check the signature over the checksums. Install
cosign (on a
Mac: brew install cosign; on Linux: your distribution's package, or a binary
from cosign's releases page), then:

cosign verify-blob SHA256SUMS \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity 18033717+Benehiko@users.noreply.github.com \
  --certificate-oidc-issuer https://github.com/login/oauth

It must print Verified OK. Anything else means SHA256SUMS was not signed by
us: stop, and do not run anything from the download.

2. Check your archive against the checksums.

sha256sum --ignore-missing -c SHA256SUMS            # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS        # macOS

Your archive must be listed as OK. --ignore-missing skips the archives
you did not download.

Step 1 is what makes step 2 mean anything: checksums downloaded from the same
page as the archive only prove the two agree, not that either came from us.

macOS: "cannot be opened" or "Apple could not verify"

The macOS launcher is signed ad hoc, not with an Apple Developer ID, and is not
notarized. A web browser marks what it downloads as quarantined, and macOS
refuses to run a quarantined binary it cannot trace to a registered developer.
The message reads "ermod-engine" cannot be opened because the developer cannot
be verified
, or, on macOS 15 and later, Apple could not verify
"ermod-engine" is free of malware
.

Verify the download first (above), then clear the quarantine from the
unpacked directory:

tar -xzf ermod-engine-v0.3.1-macos-aarch64.tar.gz
xattr -dr com.apple.quarantine ermod-engine-v0.3.1-macos-aarch64

Or, after the first refusal: System Settings → Privacy & Security → scroll to
the message about ermod-engine → Open Anyway.

Downloads made with curl or gh release download are not quarantined and
need neither step. Linux has no equivalent; there is nothing to do there.

ermod-engine v0.3.0

Choose a tag to compare

@Benehiko Benehiko released this 30 Sep 05:29
v0.3.0
f86b5f6

Game build 2.7.1.0, with co-op (2026-09-29)

The engine now supports Elden Ring 2.7.1.0, the latest game patch. Mods
load, param edits apply, the intro logos are skipped, the character sheet reads
correctly, and co-op works: coop host and coop join form a session, and
the co-op rule fixes (death, grace, warp, Torrent) and character sync are
active. Tested host + joiner on 2.7.1.0, in both a development and a release
build.

Grace travel and skipping the title menu work on 2.7.1.0 too. Every address
the patch moved was re-derived and checked against the 2.7.1.0 game before use;
anything that could not be checked is switched off rather than guessed. Every
player in a session needs the same game build.

Co-op players see each other again after a warp (2026-09-29)

After one player travelled, host and joiner could stand side by side and each
see only themselves: the other player's character was not rebuilt after the
load. It now is, after a warp as well as after a death.

Co-op joiners no longer crash on release builds (2026-09-29)

A joiner running a release build crashed the moment it joined a session, on
its first co-op message to the host. The cause was a diagnostic in the engine,
not the game, and it only misbehaved in optimised builds; it is gone. Debug and
release builds now behave the same.

The engine refuses to call a game function at a wrong address (2026-09-29)

If a game function the engine calls is not where this game build puts it, the
engine now refuses the call and says so, instead of jumping into the middle of
other code and crashing the game.

--ignore-build-guard: run an unsupported game build anyway (2026-09-29)

A game build the engine has no verified addresses for normally runs unmodded.
--ignore-build-guard runs it anyway, as the newest build the engine does
have complete addresses for (2.7.0.0). Every command that launches the game
takes it: ermod-engine --ignore-build-guard, coop host --ignore-build-guard, coop join … --ignore-build-guard. It applies to that
one launch, and both the launcher and ermod-runtime.log say when it is in
effect.

It is unsafe by design: after a patch some of those addresses will have
moved. Expect crashes, and wrong reads or writes to the characters in your
modded profile; your own save is still never opened. A supported build runs as
itself with or without the flag; it only matters after a game patch the engine
does not know yet.

macOS: protium, and choosing the Wine setup (2026-09-29)

CrossOver stays the default. If protium
is installed (~/.local/bin/protium or on PATH) and one of its prefixes
holds Elden Ring, the engine uses it instead. It launches through
protium run --prefix <name>, so the prefix's own settings apply.
--backend auto|crossover|whisky|protium, or macos_backend = "…" in
engine.cfg, picks one outright. A named backend never falls back to
another vendor's Wine. Setup guide: docs/macos-protium.md.


Installing

Two archives, pick yours:

Machine Archive
Linux, x86-64 (Steam + Proton) ermod-engine-v0.3.0-linux-x86_64.tar.gz
macOS, Apple Silicon (CrossOver, Whisky or protium) ermod-engine-v0.3.0-macos-aarch64.tar.gz

Both carry the same Windows runtime; only the launcher differs. You need Elden
Ring installed through Steam and launched normally at least once.

1. Download your archive, plus SHA256SUMS and SHA256SUMS.sigstore.json,
into one directory, and verify them (below).

2. Unpack it anywhere you like; nothing is installed system-wide:

tar -xzf ermod-engine-v0.3.0-linux-x86_64.tar.gz   # or the macos-aarch64 one
cd ermod-engine-v0.3.0-linux-x86_64

On macOS, clear the quarantine first (below).

3. Check it finds your game, without launching anything:

./ermod-engine --dry-run

It names your install, your Proton or Wine, and what it would stage. If it
cannot find something, it says which check failed.

4. Play:

./ermod-engine                  # launch the game with mods

Mods go in ~/.local/share/ermod/mods (one .lua file each; examples in the
mods repository).
In the game, ` opens the engine's menu. The modded game plays on its own
profile and never opens your own save; the first launch offers to copy your
characters in.

Co-op, every player on the same release and the same game build:

./ermod-engine coop id                              # your Steam ID and addresses
./ermod-engine coop host                            # the host
./ermod-engine coop join <host-id>@<host-address>   # everyone else

INSTALL.md in the archive is the full guide, as is
docs/install.md;
docs/coop.md
covers co-op, including playing across networks.

Verifying this download

Every release is signed. The signature covers SHA256SUMS, which pins each
archive by hash, and travels beside it as SHA256SUMS.sigstore.json. Download
your archive, SHA256SUMS and SHA256SUMS.sigstore.json into one directory,
then run these two steps from it, in this order.

1. Check the signature over the checksums. Install
cosign (on a
Mac: brew install cosign; on Linux: your distribution's package, or a binary
from cosign's releases page), then:

cosign verify-blob SHA256SUMS \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity 18033717+Benehiko@users.noreply.github.com \
  --certificate-oidc-issuer https://github.com/login/oauth

It must print Verified OK. Anything else means SHA256SUMS was not signed by
us: stop, and do not run anything from the download.

2. Check your archive against the checksums.

sha256sum --ignore-missing -c SHA256SUMS            # Linux
shasum -a 256 --ignore-missing -c SHA256SUMS        # macOS

Your archive must be listed as OK. --ignore-missing skips the archives
you did not download.

Step 1 is what makes step 2 mean anything: checksums downloaded from the same
page as the archive only prove the two agree, not that either came from us.

macOS: "cannot be opened" or "Apple could not verify"

The macOS launcher is signed ad hoc, not with an Apple Developer ID, and is not
notarized. A web browser marks what it downloads as quarantined, and macOS
refuses to run a quarantined binary it cannot trace to a registered developer.
The message reads "ermod-engine" cannot be opened because the developer cannot
be verified
, or, on macOS 15 and later, Apple could not verify
"ermod-engine" is free of malware
.

Verify the download first (above), then clear the quarantine from the
unpacked directory:

tar -xzf ermod-engine-v0.3.0-macos-aarch64.tar.gz
xattr -dr com.apple.quarantine ermod-engine-v0.3.0-macos-aarch64

Or, after the first refusal: System Settings → Privacy & Security → scroll to
the message about ermod-engine → Open Anyway.

Downloads made with curl or gh release download are not quarantined and
need neither step. Linux has no equivalent; there is nothing to do there.

ermod-engine v0.2.0

Choose a tag to compare

@Benehiko Benehiko released this 29 Sep 17:59
v0.2.0
785c6e3

Co-op: everyone runs the same game-changing mods (2026-09-29)

Players in a session must run the same mods that change how the game plays:
a game rule, or a param write. Mods that only draw, monitor or log may
differ. A joiner whose set differs is held at the door. The menu lists the
host's mods, downloaded for them and checked byte for byte, each with an
Enable button, and any game-changing mod the host does not run with a
Switch off button. Once they match, the joiner joins by itself. Nothing
is written to the mods directory until the player presses Enable. In the
session, a joiner cannot load a game-changing mod the host does not run, or
switch off one the host does; cosmetic mods load and reload freely.

The menu no longer fights the camera (2026-09-29)

While the ermod menu is open, or Insert has given mod windows focus, the
mouse pointer moves freely and the camera stays still: the game's cursor
recentring is suspended and it reads the keyboard and mouse as idle. A
gamepad keeps working. The menu can also be driven from the keyboard: Tab
or the arrow keys to move, Space or Enter to press.

Mod packs and game rules (2026-09-28)

A mod can be a mod pack: its manifest lists other mods by name
(mods = { "level60", "boss-rules" }). A pack and its members take
precedence over mods loaded on their own. When a pack and a standalone mod
set the same thing to different values, the pack's value is the one that
lands. The standalone mod still loads, and the log says which of its
settings was not applied.

Every mod's configuration (sdk.params writes, and the new sdk.rules) is
now checked before any of it reaches the game. The engine runs each mod's
entry point with its writes held back, compares them, and applies only what
passes. Two packs that set the same thing to different values are both
refused, along with their members. The same goes for two standalone mods.
The same value from both is fine. A mod already running keeps its place, so
a pack added later that disagrees with it is the one refused. Configuration
can only be changed from a mod's entry point; a write from an event handler
is an error.

sdk.rules holds engine-wide game rules. A mod sets a rule only with the
permission of the same name. The first rule is boss_spectate (default on):
off, a player who dies in a fog-wall boss fight respawns instead of being
held to watch a teammate.

The world gate is the game's own (2026-09-07)

Everything that waits for "a world" — dev world, dev wait-world, dev state, dev warp, dev summon, the co-op rig — now waits for the player to
be standing in one: the game's title, in-game and map-move step machines at
their resting steps with no title menu held. It used to be a null test on one
allocation, which passed on the title screen. dev state says
world-loading while the map step is still walking and not-in-world at the
title. dev world --skip-menu now presses the game's own Continue instead
of replaying it, because the replay loaded the world under the title window.

The first release. Everything below has been proven on a live, offline
launch of the real game unless it says otherwise.

Supported game build: 2.6.2.0. On any other build the engine logs that it
does not recognise the game, disables every hook and lets the vanilla game
run. It never guesses.

What it is

ermod-engine launches Elden Ring with Easy Anti-Cheat left out — under
Proton on Linux, in a Wine bottle on macOS — and injects a runtime that can
run Lua mods in the live game and load a modded regulation.bin without
touching the game install.

Playing with mods

  • Lua mods, live. Drop .lua files in the engine's mods directory and
    they run in the game. Each mod is sandboxed in its own VM with only the
    modules it declares; a mod that misbehaves is disabled on the spot and the
    others carry on.
  • Mods can read and write the game's live parameter tables — the same
    data a regulation.bin holds, edited in the running game.
  • Mods can draw — an in-game overlay (Insert toggles input focus),
    report frame timing, and persist their own settings between sessions.
  • A modded regulation.bin loads without ModEngine. Point the engine at
    an ermod-engine dev apply artifact and the game reads it instead of its
    own file.
    The game's own regulation.bin is never overwritten, so Steam's integrity
    check has nothing to revert.

Getting to the game faster

  • ermod-engine world is the testing loop in one command. Stop the
    game, author the save Continue will load (--save FILE, and
    field=value arguments applied by the engine itself), relaunch muted and
    headless,
    drive to a loaded world, print the character — about thirty seconds, no
    input, nothing on the desktop. It replaces the tools/world.sh script, so
    a mod author has it with the binary rather than with a checkout.
  • --headless keeps the game off your desktop. The launch runs inside a
    headless gamescope session — its own nested display that is never shown —
    so the game does not open a window and does not take the keyboard or
    mouse. Meant for unattended test runs on a machine someone is also using;
    ermod-engine shot, screen, key, state and drive all work
    unchanged, because they act from inside the game rather than through the
    display. Needs gamescope installed; the launcher says so if it is not.
  • The logo screens are skipped by default. The publisher and engine cards
    shown every launch. The game already has its own way of skipping them; the
    engine just always takes it, so nothing of ours runs on that path. Turn it
    off with skip_title_cards = false in engine.cfg, or on the Engine tab.
  • The opening movie can be skipped, and the story cutscenes with it,
    each behind its own switch (skip_boot_cinematics,
    skip_ingame_cinematics, both off by default; also on the Engine tab).
    The engine presses the game's own [Esc] Skip the moment a movie it is
    told to skip opens, so the game ends it exactly as it ends one you skipped
    yourself — about three seconds in. The opening movie is proven live; the
    in-game switch is the same code against cutscenes not yet reached in a
    test. Four attempts to end the movie through Bink's own API came first and
    each was ruled out by a live run (one dropped every frame of the picture
    while the audio ran on; three crashed the game inside Bink) — written up
    in docs/skip-cinematics.md.
  • ermod-engine key now moves the game. It sends a real key event
    (SendInput from inside the process) as well as feeding the input hooks,
    and that turned out to be the only route the game's screens react to; the
    earlier message-queue delivery reached the pump and moved nothing. A
    test can now drive the game from PRESS ANY BUTTON through the policy
    dialogs, the main menu and character creation to the opening movie
    without a person at the keyboard — the sequence is in
    docs/skip-cinematics.md.
  • The engine can tell which screen the game is on, and drive it by
    looking.
    ermod-engine screen names the menu in front of it (a frame
    capture reduced to a small fingerprint and matched against references
    learned from the running game; --learn teaches it a new one), and
    ermod-engine drive new-game takes a freshly launched game from PRESS ANY BUTTON to the world — through whatever dialogs happen to appear —
    in under a minute, unattended. It replaced a timing script that broke on
    every unexpected dialog. docs/screen-drive.md.
  • The engine can set the character, instead of driving the game to build
    one.
    ermod-engine state prints the live character sheet — level, the
    eight stats, runes — and ermod-engine state level=60 vigor=40 runes=500000 writes it, printing the sheet as it stands afterwards so a
    clamped value is visible rather than silent. Proven live on a fresh
    Vagabond: the first read returned the class's own starting numbers, field
    for field, and after a write the game's own HUD showed the new rune total.
    It writes save-side fields only — it cannot warp or spawn, because where
    the player stands is not a field but the output of the game's
    map-streaming machinery. Writes are refused outside a loaded world, since
    the title screen's player data is zeroed and a write there would be
    discarded unread, and every value is clamped to its real range (stats to
    the game's own 99). docs/state-injection.md.
  • The engine reads a save file itself, with no Rust and no game.
    ermod-engine save show [<save.sl2>] [--slot N] [--all] prints the
    characters in a save — name, level, runes, the eight stats, the save
    version, the map each stands in, and where each one actually is: position,
    the grace they last rested at, their spawn point, and the save's Steam
    account. It reads the file and never writes it. With no file it reads the
    active profile's save; your vanilla save is only read when you name it. It
    agrees field for field with the Rust tool it replaced, on every character
    of every save tested, across nine save versions.
  • The engine writes a save too, and there is no Rust left in the project.
    ermod-engine save set [<save.sl2>] --slot N field=value ... writes level,
    the eight stats, runes, name, Steam id, event flags, the volume sliders and
    the position block, recomputing every section checksum and the player-data
    hash. save set --slot N grace="The First Step" puts a character at any
    overworld or legacy-dungeon grace, in the Lands Between or the Land of
    Shadow, so a test starts on Continue already
    standing there. Unlike the tool it replaced, it writes bytes in place: a set
    touches the slot you named and nothing else.
  • ermod-engine save graces lists every grace it can place a character
    at — with its map, position, unlock flag and whether it can be stood at —
    and needs no save file. **`save regen-g...
Read more