ermod-engine

Changelog

On this page

What the engine does, in the words of someone using it rather than building it. Engine releases are published on the open repo's Releases page; the supported game build is part of every entry, because it decides whether the engine does anything at all.

Unreleased

on_death tells a mod the character's death count

sdk.hooks.on(sdk.hooks.event.on_death, function(ev) ... end) now gets ev.deaths: how many times the character has died, this death included. It is the game's own lifetime counter, saved with the character, and the same number sdk.watch.get(sdk.watch.stat.deaths) has always read. A mod no longer has to count deaths itself to show a total. examples/death_ping.lua and examples/overlay.lua show it beside their own count since loading.

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.

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 and Playing co-op.

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.

v0.4.0 (2026-10-03)

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

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

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.

v0.3.2 (2026-10-02)

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.

Logs live in the data directory

The engine now writes ermod-runtime.log and ermod-launcher.log to ~/.local/share/ermod/logs/, beside your mods and profiles. To report a problem, copy them from there. Deleting or rebuilding the Wine prefix keeps them. The first launch moves any logs an older engine left in the prefix's system32. ermod-engine paths prints both paths.

v0.3.1 (2026-09-30)

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.

v0.3.0 (2026-09-29)

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: Installing the engine, macOS.

v0.2.0 (2026-09-29)

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

Getting to the game faster

Writing mods

Your characters

A window, if you would rather not use a terminal

On a Mac

Safety

Knowing what you are running