Architecture
Threat model / scope
Section titled “Threat model / scope”Two goals, in priority order:
- Don’t break the game. Mod application must be deterministic and fully reversible.
- Don’t fight anti-tamper. Ravenswatch.exe has a protector. Asset mods never touch the running game: they replace files at install time, so no anti-tamper code path runs. Only Lua mods need the optional loader, which hooks the game at function entries (see Anti-tamper protector).
- Treat a downloaded mod as hostile input. A mod archive comes from
a third party over the network. It must not be able to write outside
mods/, delete anything, ship an executable, or exhaust the disk.
Untrusted archives
Section titled “Untrusted archives”Everything that unpacks a downloaded mod — rsmm install, rsmm update
and the desktop bridge’s json install-mod — goes through one extractor,
rsmm.sdk.archive. It was previously three separate copies that had
drifted apart, which is why the rules now live in a single module:
| Guard | What it stops |
|---|---|
Component-wise containment check (is_relative_to, not startswith) |
Zip-slip, including escapes into a sibling dir sharing the prefix (mods/ → mods-evil/) |
Members read with ZipFile.open and written as regular files |
A symlink member becoming a real symlink that a later member writes through |
safe_dir_name on every id/slug joined onto mods/ |
An archive whose members all start with ../ yielding the mod id .., which --force then deletes |
| Entry count / uncompressed size / compression-ratio caps | Decompression bombs |
DANGEROUS_EXTENSIONS refusal |
Mods shipping .exe / .dll / .ps1 / .sh payloads — including under _root/, which has no exemption |
Every _root/ member reported |
Silently overwriting files in the game install root |
| Staged unpack + swap into place | A corrupt or rejected download destroying a working install as a side effect of failing |
Transport is https-only (file://, and plain HTTP against loopback, are
allowed for offline installs and local development). Downloads are size
capped and time bounded. Integrity is SHA-256 from repo.json plus an
optional Ed25519 signature — see rsmm.sdk.repo.
How v1 works
Section titled “How v1 works”The engine loads every cooked asset from
<install>/DarkTalesResources/_Cooking/<encoded>. <encoded> is the
asset’s filename run through a fixed Caesar substitution cipher (decoder:
tools/find_iyg.py). The cooked bytes have no checksum, signature, or
embedded path; any byte-compatible file at the encoded path is accepted.
Therefore v1 is purely an install-time file applier:
tools/find_iyg.pybuilds theencoded -> decodedmap by walkingDarkTalesResources/UsedRscList.otand applying the cipher.tools/apply_mods.pywalksmods/, parses each manifest, collects override files, and resolves each decoded path to its encoded path.- For each new override: back up the original cooked file to
<file>.rsmm.bak, copy the mod’s file into place. - For each previously-active override no longer wanted: restore from
.rsmm.bak, delete the backup. - State persists in
<install>/DarkTalesResources/_Cooking/.rsmm_state.json.
flowchart LR
A["mods/<id>/<br/>manifest.toml + assets"] --> B["apply_mods.py"]
M["UsedRscList.ot"] -->|"Caesar cipher<br/>(find_iyg.py)"| C["asset_map.json<br/>decoded → encoded"]
C --> B
B -->|"back up original"| D["<file>.rsmm.bak"]
B -->|"copy override"| E["_Cooking/<encoded>"]
B --> F[".rsmm_state.json"]
G["./rsmm restore --all"] -->|"restore from .bak"| E
No DLL is loaded into the game process. No engine API is hooked. No
anti-tamper code path runs. Removing the manager is --restore-all.
Mod format
Section titled “Mod format”mods/<id>/ manifest.toml assets/ <decoded path>/<file> e.g. Ui/BookMenu/Heroes/UI_HeroPortrait_Romeo_Active.png.Texture.dxt[mod]id = "..."name = "..."version = "..."author = "..."description = "..."enabled = trueState + backups
Section titled “State + backups”DarkTalesResources/_Cooking/.rsmm_state.json{ "version": 1, "active": { "<encoded path>": { "mod": "<mod id>", "src_sha1": "<sha1 of mod file>", "orig_sha1": "<sha1 of original cooked file>" }, ... }}Backups live next to their originals as <file>.rsmm.bak. The applier
diffs the wanted set against the active set on every run; a no-op run is
a no-op.
Asset map
Section titled “Asset map”asset_map.json is generated by tools/find_iyg.py. The cipher is in
that script (LOWER, UPPER, SYMBOLS tables). Confirmed clean as of
2026-05-14: 40,687 paths, no inconsistencies. Regenerate after a game
update if UsedRscList.ot changes.
What v1 cannot do
Section titled “What v1 cannot do”- Edit top-level uncooked files (ApplicationSettings.ot,
EngineSettings.ini, UsedRscList.ot). These are loaded from outside
_Cooking/and are special-cased by the engine; v1 doesn’t manage them. Manual edits work, no tooling around them yet. - Build a cooked file from nothing. New items, heroes, enemies and
the like are made by the SDK content kinds, which cook a copy of a shipped
file and edit it (see Authoring mods). A free-form
text-
.ot-> binary-.genre-encoder for arbitrary classes is still not built. - Toggle mods in-game. v1 is install-time only.
v2: in-game UI (research)
Section titled “v2: in-game UI (research)”The end goal is a Mods entry inside the game’s actual book menu, opening a native-styled mod list. Path forward:
tools/ot_decoder.pyis structural (header + class table + section ranges) but does not decode object bodies. Extending it requires the per-class serialization schemas. Best Rosetta we have:ApplicationSettings.ot(text) and the 523*.UsedRscCache.otfiles (per-asset cooking-pipeline manifests with plaintext bucket / path / root-class triples).- Build a text-
.ot-> binary-.genre-encoder, round-trip a known simple file (e.g. one of the 224-byteEnemyCampDifficultyDefinitionfiles: two floats, that’s the whole body). - Modify the MainMenu cooked asset
(
_Cooking/Oi/KgxqJdv/HgdzHqzw.lqkql.ri.KgxqFiuqgx.yqz, decodedOt/GameUis/MainMenu.level.ot.GameStream.gen). The menu is anoCGameStreamcontaining oneBOOK_SPAWNERreferencingBook_Menu\Book_Mesh_Controller.entity.ot. The actual tab/page logic lives inBook_Mesh_Controller, which containsoCDtEntityCpnt3DBookControllerSettingsand 74 other classes. - Inject a “Mods” tab entry; route the click event back into either an external manager or (further out) a DLL we re-introduce once we have a viable injection path that survives anti-tamper.
See FINDINGS.md for the full reverse-engineering record.
The loader, and a parked subsystem
Section titled “The loader, and a parked subsystem”src/loader/— winhttp.dll proxy + MinHook + Lua 5.4. This is the runtime for Lua-scripted mods, installed withrsmm install-loaderand updated withrsmm update-loader. Asset-only mods do not need it.layer/— Vulkan implicit layer that surfaces an ImGui overlay without in-process hooks. Parked: it builds, but pressure-vessel stripsVK_LAYER_PATHso the user must drop the manifest into~/.local/share/vulkan/implicit_layer.d/. Gated byRSMM_OVERLAY=1viaenable_environmentin the layer manifest.
