Extends the MDL system beyond text overrides: bitmaps/<label>.json names a model/scale and resource list, and mdlp extracts each resource's frames, runs them through an ONNX Runtime CPU upscaler baked into mdlp-image, re-quantizes to the game's palette, and writes the result back - same build-time pipeline as message overrides. Adds animgallery, a standalone viewer that renders any data directory's animated resources as an HTML/GIF gallery with a checkbox-driven MDL JSON generator, and -only-changed to preview just what a mod overrides against the base game. Fixes a real data race in hacked's ChangeNotifier.hashAll() (found via go build -race causing non-deterministic resource counts), via a new numbered patch rather than editing the vendored source. Adds engine-patches/, a general-fixes counterpart to android/engine-patches/ now staged and applied to the desktop builds too (previously unpatched): a real GL_UNPACK_ROW_LENGTH stride bug, an audio error-logging improvement, and two critter-rendering fixes - mod art was losing to the base game's own re-opened resource files, and critters rendered at the wrong size/position once that was fixed, both from how the billboard renderer derives on-screen size and placement straight from bitmap pixel dimensions. Updates README.md and AGENTS.md to document engine-patches/.
Modding
Tools and reference material for modding System Shock (1994) itself - distinct from the rest of this repository, which is the QuestShock VR port of the game engine. Everything modding-related lives under this folder.
hacked and ss-specs come from the InkyBlackness
project; sqweek-dialog is a separate third-party dependency of hacked.
All three are vendored unmodified under src/, each under its own upstream
license (see below) - none is relicensed by this repository's own MIT
license. mdlp (also under src/) is first-party code written for this
repository.
1. Layout
src/- every vendored and first-party source tree, kept apart from the Docker/script scaffolding that builds them:hacked/- InkyBlackness HackEd, the graphical mod editor. A vendored source snapshot (New BSD License), taken from themainbranch on 2026-09-26. To update it, replace this directory with a fresh checkout and update that date.hacked-patches/- numberedpatch -p1files applied to a scratch copy ofhacked/at build time (same pattern as the root project'sandroid/engine-patches/, see the repository root's AGENTS.md section 6) -hacked/itself is never edited in place. See section 1.1 for what each patch does.sqweek-dialog/-github.com/sqweek/dialog,hacked's native file-dialog dependency. Vendored at the exact versionhacked/go.modwould otherwise pull from the module proxy (ISC License), so it can be patched the same way ashacked/itself (see section 1.1).sqweek-dialog-patches/- patches forsqweek-dialog/, same pattern ashacked-patches/(section 1.1).mdlp/- the Mod Definition Language Processor (section 4). First-party Go source, not vendored - edit it directly, no patch system needed.ss-specs/- InkyBlackness ss-specs, the unofficial file-format specifications the editor (and any other modding tool) is written against. Reference documentation only, not built. A vendored snapshot (CC BY-NC-SA 4.0 - non-commercial, unlike the rest of this repository), taken from themainbranch on 2026-09-26.
hacked-build-image/- the Dockerfile (and supporting scripts) for thehackedbuild environment: the Go toolchain plus every dependencysrc/hacked/go.modpins, fetched once at image-build time so the actual build needs no network access. Mirrors the main project's ownbuild-image/(see the repository root's README.md section 4) one level down, for this subproject.hacked-build-image.sh/hacked-build-run-image.sh/hacked-build-upload-image.sh- build the image, run it to compilehacked/, and push it to a registry, respectively. Same pattern as the root project's ownbuild-image.sh/run-image.sh/upload-image.sh.mdlp-image/,mdlp-image.sh/mdlp-run-image.sh/mdlp-upload-image.sh- the same pattern again, formdlp(section 4.2).mods/- mod definitions in the Mod Definition Language (section 4).Makefile-hacked(default) buildshacked/via the image;build-image(re)builds that image;mdlp-imagebuilds themdlpimage;modsprocesses every mod undermods/via that image;cleanremovesdist/.dist/- build output (git-ignored):make hackedunderlinux//win/,make modsundermods/.
Shockolate (the engine ../engine/ vendors) already has its own modding
support (level/resource files, res/data/); this folder is about the
external tools that create and inspect that content, not about changing the
engine itself.
1.1. Patch reference
src/hacked-patches/ and src/sqweek-dialog-patches/ (applied to scratch
copies of src/hacked/ and src/sqweek-dialog/ respectively) are both
plain, numbered patch files applied in order at build time - neither
vendored source is ever modified in place. What each one does:
src/hacked-patches/:
01-use-patched-dialog.patch- adds ago.modreplacedirective pointinggithub.com/sqweek/dialogat the local, patched../sqweek-dialoginstead of the upstream module, sohacked(and anything else building against this source, such asmdlp/animgallery) picks up the crash fix below.02-fix-hashall-race.patch- fixes a real data race inss1/resource/ChangeNotifier.go'shashAll(): it hashes many resources concurrently (one goroutine per ID) to detect what changed after a modification, but only guarded the resource lookup with a mutex, not the actual block-reading insidehashResource()- resources from the same underlying file share a stateful reader, so concurrent unguarded reads corrupted each other, non-deterministically. Found viago build -raceafteranimgalleryproduced a different resource count on every run against the exact same input. The mutex now guards the whole hash computation per resource, not just the lookup.
src/sqweek-dialog-patches/:
01-xinitthreads.patch- addsXInitThreads()beforegtk_init()in the Linux backend. Without it,hackedsegfaults the moment a native GTK3 dialog (file/folder browse) opens while a GLFW/OpenGL context is live: GLFW's own X11 backend installs a process-wide Xlib error handler that conflicts with GTK's unless Xlib has been told up front that it will be used from multiple threads.
2. Building hacked
Packages dist/<version>.linux64.tgz and dist/<version>.win64.zip (native
Linux build, Windows amd64 cross-compiled via mingw-w64 - both from the same
source, in one run).
Prerequisites: Docker - same as the root project, nothing modding-specific to install locally.
cd modding
# 1. Build the hacked-build-image (once, or after hacked-build-image/ or
# src/hacked/go.mod|go.sum changes)
./hacked-build-image.sh
# 2. Compile hacked/ for Linux and Windows
make hacked
<version> is hacked-<date>, not an InkyBlackness release version:
hacked/ is a vendored snapshot here, not its own git checkout, so there is
no tag to derive a real version from (see
hacked-build-image/build-hacked.sh's own comment).
Run the result: dist/linux/hacked/hacked on Linux,
dist\win\hacked\hacked.exe on Windows. Needs OpenGL 3.2; usage is documented
on hacked's own wiki.
2.1. Publishing the build-image
Same registry as the root project's own build-image (see the repository
root's registry.env.example), under a separate image name so the two don't
collide:
./hacked-build-upload-image.sh
Reads REGISTRY/REGISTRY_USER/REGISTRY_PASSWORD and (optionally)
HACKED_REGISTRY_IMAGE from ../registry.env, same as the root scripts.
Don't bump hacked-build-image/VERSION just because the Dockerfile changed -
only once a build with the current version has actually succeeded, same
policy as the root project's own build-image/VERSION.
3. Example: Editing a message
hacked has no inline text editor - every text field is edited by right-click
-> Copy to Clipboard, editing the text in an external editor, then
right-click -> Copy from Clipboard back into the field. Example: changing
Rebecca Lansing's first email.
- Run
dist/linux/hacked/hacked(or the Windows build). - Window -> Project. Under Static World Data, Add... and browse to
the game's whole
res/assets/ss_ee/data/directory (see the root README.md section 3 for where that comes from), so every.res/.datfile is available. - Window -> Messages (F5). Message Type:
Mails. Index:26(Rebecca's first mail). Edit Sender/Subject/Title/the message text via the clipboard round-trip above. - Back in the Project window, under Mod Location, click Save
(first time: Browse... to an empty folder, for example
./modding/mymod). - Test it from the root project's desktop build:
The mod-folder argument must be an absolute path:
./dist/run.sh "$(pwd)/modding/mymod"run.shcds intodist/before launching the engine, so a relative path resolves against the wrong directory and the mod silently fails to load (the engine'sProcessModDirectory()reports nothing on a bad path). When the path is right, the terminal showsProcessModDirectory .../Found resource file: .../Loading mod file ...for confirmation.
4. Mod Definition Language (mods/)
MDL is a small, declarative JSON format for describing a mod's content -
one file per piece of content (a message override, an upscaled sprite, and
so on), naming the game resource it touches and the values to set, rather
than the resource's actual binary data. mdlp (the Mod Definition
Language Processor, mdlp/) reads these files and produces the same kind
of mod folder hacked itself would save, reproducibly and offline. Since a
plain-text description is all that's checked into git, mods stay reviewable
and reproducible from the game's own data alone (see section 4.3) - unlike
the workflow in section 3, whose result only ever lives in whatever you
clicked through in hacked.
4.1. Layout
mods/
<mod-name>/
messages/
<label>.json # one message's overrides
<label>.txt # optional: a text field's body, referenced by ...File
Each messages/*.json overrides one message (email, log, or fragment) by
type and index. Only fields present in the file are changed - everything
else is read from the game's own data (Static World Data), the same
inheritance the Messages window in hacked shows via its "(read-only)"
label:
| Field | Meaning |
|---|---|
type |
Mails, Logs, or Fragments |
index |
The message's index within that type |
language |
Default, French, or German - omit for Default |
title, sender, subject |
Plain strings |
verboseText / verboseTextFile |
The long body, inline or from a sibling file (avoids escaping newlines/quotes in JSON) |
terseText / terseTextFile |
The short body, inline or from a sibling file |
nextMessage, isInterrupt, colorIndex, leftDisplay, rightDisplay |
See ElectronicMessage in src/hacked/ss1/content/text/ElectronicMessage.go |
mods/mymod/ is a worked example matching section 3's Rebecca's-mail edit,
expressed as MDL instead of a hacked session.
4.2. mdlp
src/mdlp/ is first-party Go source (not vendored - edit it directly, no
patch system needed). It reuses hacked's own resource read/write code
(ss1/edit, ss1/world, ss1/content/text) through a go.mod replace
pointing at the sibling ../hacked (both under src/), instead of
reimplementing System Shock's file formats. It needs no GUI, GLFW, or GTK -
only the packages that model resource data.
mdlp-image/ is its own Docker build/runtime environment, separate from
hacked-build-image/: mod processing is expected to grow further external
tool dependencies (image/audio conversion, etc.) of its own over time,
independent of hacked's GTK/GLFW/mingw toolchain.
4.3. Building mods
cd modding
# 1. Build the mdlp-image (once, or after mdlp-image/ or
# src/mdlp/go.mod|go.sum changes)
./mdlp-image.sh
# 2. Process every mod under mods/
make mods
# ...or only some of them:
MODS="mymod" make mods
Needs the game's own data directory at ../res/assets/ss_ee/data (see the
root README.md section 3) as read-only reference data, so
overrides that touch only some fields of a message can inherit the rest.
Output lands in dist/mods/<mod-name>/mod/ (the same shape hacked's own
Project window Save produces) for each mod, plus one combined
dist/questshock-mods.zip packaging all of them together. Nothing but
mods/'s own JSON/text files needs to be checked into git - the game's own
data (already required locally, already git-ignored) plus those files is
always enough to reproduce this output from scratch.
Test every built mod at once with run-mods.sh (Linux) / run-mods.bat
(Windows), copied into the root project's own dist//dist-win/ by
make dist (root README.md section 4) alongside the plain
run.sh/run.bat:
../dist/run-mods.sh
It scans dist/mods/*/mod and passes each one it finds to run.sh as a
separate, absolute argument - the engine's ProcessModDirectory() only scans
one directory level deep (see res/run-mods.sh's own comment), so it can't
simply be pointed at dist/mods/ itself. To test a single mod instead, use
run.sh directly the same way as section 3's step 5:
../dist/run.sh "$(pwd)/dist/mods/mymod/mod"
4.4. Publishing the mdlp-image
Same as hacked-build-image/ (section 2.1): ./mdlp-upload-image.sh reads
REGISTRY/REGISTRY_USER/REGISTRY_PASSWORD and (optionally)
MDLP_REGISTRY_IMAGE from ../registry.env. Don't bump
mdlp-image/VERSION just because the Dockerfile changed - only once a build
with the current version has actually succeeded, same policy as the root
project's own build-image/VERSION.