Files
questshock/modding/README.md
T
ml 389abd7de7 Add a first-party MDL mod-build system and reorganize modding/ under src/
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.
2026-09-26 15:41:56 +02:00

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 the main branch on 2026-09-26. To update it, replace this directory with a fresh checkout and update that date.
    • hacked-patches/ - numbered patch -p1 files applied to a scratch copy of hacked/ at build time (same pattern as the root project's android/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 version hacked/go.mod would otherwise pull from the module proxy (ISC License), so it can be patched the same way as hacked/ itself (see section 2's crash fix).
    • sqweek-dialog-patches/ - patches for sqweek-dialog/, same pattern as hacked-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 the main branch on 2026-09-26.
  • hacked-build-image/ - the Dockerfile (and supporting scripts) for the hacked build environment: the Go toolchain plus every dependency src/hacked/go.mod pins, fetched once at image-build time so the actual build needs no network access. Mirrors the main project's own build-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 compile hacked/, and push it to a registry, respectively. Same pattern as the root project's own build-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, for mdlp (section 4.2).
  • mods/ - mod definitions in the Mod Definition Language (section 4).
  • Makefile - hacked (default) builds hacked/ via the image; build-image (re)builds that image; mdlp-image builds the mdlp image; mods processes every mod under mods/ via that image; clean removes dist/.
  • dist/ - build output (git-ignored): make hacked under linux//win/, make mods under mods/.

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.

  1. Run dist/linux/hacked/hacked (or the Windows build).
  2. 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/.dat file is available.
  3. 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.
  4. Back in the Project window, under Mod Location, click Save (first time: Browse... to an empty folder, for example ./modding/mymod).
  5. Test it from the root project's desktop build:
    ./dist/run.sh "$(pwd)/modding/mymod"
    
    The mod-folder argument must be an absolute path: run.sh cds into dist/ before launching the engine, so a relative path resolves against the wrong directory and the mod silently fails to load (the engine's ProcessModDirectory() reports nothing on a bad path). When the path is right, the terminal shows ProcessModDirectory ... / 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.