# Modding Tools and reference material for modding System Shock (1994) itself - distinct from the rest of this repository, which is the [QuestShock](../README.md) VR port of the game engine. Everything modding-related lives under this folder. `hacked` and `ss-specs` come from the [InkyBlackness](https://inkyblackness.github.io) 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](https://github.com/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](../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 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 1.1). - `sqweek-dialog-patches/` - patches for `sqweek-dialog/`, same pattern as `hacked-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](https://github.com/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](../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. ### 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 a `go.mod` `replace` directive pointing `github.com/sqweek/dialog` at the local, patched `../sqweek-dialog` instead of the upstream module, so `hacked` (and anything else building against this source, such as `mdlp`/`animgallery`) picks up the crash fix below. - **`02-fix-hashall-race.patch`** - fixes a real data race in `ss1/resource/ChangeNotifier.go`'s `hashAll()`: 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 inside `hashResource()` - resources from the same underlying file share a stateful reader, so concurrent unguarded reads corrupted each other, non-deterministically. Found via `go build -race` after `animgallery` produced 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`** - adds `XInitThreads()` before `gtk_init()` in the Linux backend. Without it, `hacked` segfaults 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/.linux64.tgz` and `dist/.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. ```sh 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 ``` `` is `hacked-`, 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](https://github.com/inkyblackness/hacked/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: ```sh ./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](../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: ```sh ./dist/run.sh "$(pwd)/modding/mymod" ``` The mod-folder argument **must be an absolute path**: `run.sh` `cd`s 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/) 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/ / messages/