modding/mods/ holds message edits as plain JSON (type, index, language, plus only the changed fields - a long body can live in a sibling .txt to avoid escaping in JSON), so mod authoring is reviewable in git instead of living only in hacked's GUI. mdlp (modding/src/mdlp/, its own Docker image mdlp-image/) turns them into the same mod folder/zip hacked's own Save produces, reusing hacked's ss1/edit, ss1/world and ss1/content/text packages via a go.mod replace - no GUI/GLFW/GTK needed. `make mods` builds every mod into dist/mods/<name>/, scoped per-mod so it never touches hacked's own build output; `make` (no target) now builds both. Fixes a bug this surfaced: hacked's FileLocation.AbsolutePathFrom silently writes into the current directory instead of the given output folder when passed a relative path (the GUI never hits this since its folder picker is always absolute) - mdlp now resolves its own paths up front. res/run-mods.sh/.bat are new companion launchers to run.sh/.bat: they pass every dist/mods/*/mod folder to the engine as a separate absolute argument, since ProcessModDirectory() only scans one directory level deep. Wired into `make dist`/`dist-win` only, not the release archives. modding/hacked/, its patches, sqweek-dialog/, its patches, and the new mdlp/ move under modding/src/, separating source from the surrounding Docker/script scaffolding; all path references updated to match. NOTICE.txt gains a section for hacked's distributed build, listing hacked (New BSD), sqweek/dialog (ISC - corrected from a wrong "New BSD" note), and hacked's other direct Go dependencies.
11 KiB
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.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 2's crash fix).sqweek-dialog-patches/- patches forsqweek-dialog/, same pattern ashacked-patches/.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.
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/)
The workflow above is manual and its result (whatever you clicked through in
hacked) isn't checked into git. mods/ holds the same kind of edits as
plain, reviewable JSON instead, and mdlp (the Mod Definition Language
Processor, mdlp/) turns them into the same kind of mod folder and a zip,
reproducibly and offline.
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>/: a mod/ folder (the same shape
hacked's own Project window Save produces) and <mod-name>.zip.
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.