Architecture
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 anti-tamper logic that crashes on common DLL-injection hook points (CreateFileW, Wine forwarder patches, etc). v1 avoids the runtime path entirely.
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
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
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
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
- 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. - Add new entities, abilities, balance tweaks that require a new
cooked file with new internal structure. Cooked
.genfiles are positionally serialized against per-class schemas that live insideRavenswatch.exe. Building one from scratch needs the text-.ot-> binary-.genre-encoder (research, not built). - Toggle mods in-game. v1 is install-time only.
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.
Parked subsystems
The repo also contains:
loader/— winhttp.dll proxy + MinHook plumbing for in-process hooks. Built, but disabled by default. Anti-tamper crashes both the IO hook (CreateFileW) and the Vulkan present hook on this title.layer/— Vulkan implicit layer that surfaces an ImGui overlay without in-process hooks. 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.
Both are kept for the v2 work but are not part of the v1 pipeline.