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.
This commit is contained in:
@@ -73,7 +73,7 @@ dist: dist-linux dist-win
|
||||
dist-linux: engine assets
|
||||
@echo "== Assembling $(DIST_DIR) =="
|
||||
rm -rf "$(DIST_DIR)/systemshock" "$(DIST_DIR)/lib" "$(DIST_DIR)/res" \
|
||||
"$(DIST_DIR)/shaders" "$(DIST_DIR)/run.sh"
|
||||
"$(DIST_DIR)/shaders" "$(DIST_DIR)/run.sh" "$(DIST_DIR)/run-mods.sh"
|
||||
mkdir -p "$(DIST_DIR)/lib" "$(DIST_DIR)/res/data" "$(DIST_DIR)/res/sound"
|
||||
cp "$(ENGINE_OUT)/systemshock" "$(DIST_DIR)/"
|
||||
cp -a "$(ENGINE_OUT)/lib/." "$(DIST_DIR)/lib/"
|
||||
@@ -83,6 +83,8 @@ dist-linux: engine assets
|
||||
cp -a "$(ASSETS_DIR)/sound/." "$(DIST_DIR)/res/sound/"
|
||||
cp res/run.sh "$(DIST_DIR)/run.sh"
|
||||
chmod +x "$(DIST_DIR)/run.sh"
|
||||
cp res/run-mods.sh "$(DIST_DIR)/run-mods.sh"
|
||||
chmod +x "$(DIST_DIR)/run-mods.sh"
|
||||
@echo "== Done - run $(DIST_DIR)/run.sh to play =="
|
||||
|
||||
# Windows counterpart of dist-linux. Unlike dist/, the DLLs sit flat
|
||||
@@ -100,6 +102,7 @@ dist-win: engine-win assets
|
||||
cp -a "$(ASSETS_DIR)/data/." "$(DIST_WIN_DIR)/res/data/"
|
||||
cp -a "$(ASSETS_DIR)/sound/." "$(DIST_WIN_DIR)/res/sound/"
|
||||
cp res/run.bat "$(DIST_WIN_DIR)/run.bat"
|
||||
cp res/run-mods.bat "$(DIST_WIN_DIR)/run-mods.bat"
|
||||
@echo "== Done - run $(DIST_WIN_DIR)/run.bat (or systemshock.exe) to play =="
|
||||
|
||||
# Builds both platforms' redistributable archives. `package-linux`/
|
||||
|
||||
+14
@@ -35,3 +35,17 @@ cross-build") additionally bundles:
|
||||
statically into systemshock.exe instead of shipped as a DLL, under the
|
||||
GCC Runtime Library Exception - this doesn't subject the rest of the
|
||||
binary to the GPL.)
|
||||
|
||||
The modding tools' `hacked` build (see modding/README.md; output under
|
||||
modding/dist/*.tgz and *.zip) additionally bundles:
|
||||
|
||||
- InkyBlackness HackEd (https://github.com/inkyblackness/hacked), vendored
|
||||
unmodified as modding/src/hacked/. It is New BSD-licensed.
|
||||
- github.com/sqweek/dialog, vendored and patched (see
|
||||
modding/src/sqweek-dialog-patches/) as modding/src/sqweek-dialog/. It is
|
||||
ISC-licensed.
|
||||
- hacked's further direct Go dependencies (fetched at build time, not
|
||||
vendored in this repository): github.com/inkyblackness/imgui-go (New BSD);
|
||||
github.com/go-gl/glfw, github.com/go-gl/mathgl, and golang.org/x/image
|
||||
(BSD 3-Clause); github.com/go-gl/gl, github.com/asticode/go-astisub, and
|
||||
github.com/lucasb-eyer/go-colorful (MIT).
|
||||
|
||||
@@ -80,7 +80,14 @@ Section "5.1. Installing and playing" has the details.
|
||||
- `res/assets/` - where you place your own purchased copy of the game (see
|
||||
"3. Game assets" below); `res/assets/extract_assets.sh` extracts it into
|
||||
`ss_ee/`.
|
||||
- `res/run.sh` - the launcher script, copied into `dist/` on build.
|
||||
- `res/run.sh` / `res/run.bat` - the launcher scripts, copied into
|
||||
`dist`/`dist-win` on build. `res/run-mods.sh` / `res/run-mods.bat` are
|
||||
companion launchers that additionally load every mod built under the
|
||||
`modding/` subproject's own `dist/mods/` (see
|
||||
[modding/README.md](modding/README.md) section 4.3) - copied into
|
||||
`dist`/`dist-win` the same way, but not into the redistributable
|
||||
`package`/`package-win` archives, since that build output is local and
|
||||
git-ignored.
|
||||
- `res/questshock.config.default.json` - the bundled default configuration
|
||||
(see [CONFIG.md](CONFIG.md)); `res/controller_profiles/` - the per-controller
|
||||
illustrations and labels shown in the Controller Options tab (see
|
||||
|
||||
+22
-3
@@ -1,16 +1,24 @@
|
||||
# Builds the modding subproject - see README.md.
|
||||
#
|
||||
# Targets:
|
||||
# all (default) - alias for `hacked`.
|
||||
# all (default) - `hacked` and `mods`.
|
||||
# build-image - builds the hacked-build-image Docker image
|
||||
# (./hacked-build-image.sh). Only needed once, or after
|
||||
# hacked-build-image/ or hacked/go.mod|go.sum changes.
|
||||
# hacked - compiles hacked/ for Linux and Windows via that image,
|
||||
# offline. Output lands in dist/.
|
||||
# mdlp-image - builds the mdlp-image Docker image (./mdlp-image.sh).
|
||||
# Only needed once, or after mdlp-image/ or
|
||||
# mdlp/go.mod|go.sum changes.
|
||||
# mods - builds every mod under mods/ via that image, offline.
|
||||
# Set MODS="name1 name2" to build only those. Output
|
||||
# lands in dist/mods/.
|
||||
# clean - removes dist/.
|
||||
.PHONY: all build-image hacked clean
|
||||
.PHONY: all build-image hacked mdlp-image mods clean
|
||||
|
||||
all: hacked
|
||||
export MODS
|
||||
|
||||
all: hacked mods
|
||||
|
||||
build-image:
|
||||
./hacked-build-image.sh
|
||||
@@ -26,5 +34,16 @@ hacked:
|
||||
./hacked-build-run-image.sh; \
|
||||
fi
|
||||
|
||||
mdlp-image:
|
||||
./mdlp-image.sh
|
||||
|
||||
# Same already-in-the-image shortcut as `hacked` above, for MDLP_IMAGE.
|
||||
mods:
|
||||
@if [ -n "$$MDLP_IMAGE" ]; then \
|
||||
bash mdlp-image/process-mods.sh; \
|
||||
else \
|
||||
./mdlp-run-image.sh; \
|
||||
fi
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
|
||||
+144
-17
@@ -4,34 +4,59 @@ 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.
|
||||
|
||||
Both pieces here come from the [InkyBlackness](https://inkyblackness.github.io)
|
||||
project and are vendored unmodified, each under its own upstream license (see
|
||||
below) - neither is relicensed by this repository's own MIT license.
|
||||
`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
|
||||
|
||||
- `hacked/` - [InkyBlackness HackEd](https://github.com/inkyblackness/hacked),
|
||||
- `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.
|
||||
- `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-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.
|
||||
- `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](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
|
||||
`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.
|
||||
`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`.
|
||||
- `Makefile` - `hacked` (default) builds `hacked/` via the image; `build-image`
|
||||
(re)builds the image itself; `clean` removes `dist/`.
|
||||
- `dist/` - build output (git-ignored), created by `make hacked`.
|
||||
- `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
|
||||
@@ -51,7 +76,7 @@ to install locally.
|
||||
cd modding
|
||||
|
||||
# 1. Build the hacked-build-image (once, or after hacked-build-image/ or
|
||||
# hacked/go.mod|go.sum changes)
|
||||
# src/hacked/go.mod|go.sum changes)
|
||||
./hacked-build-image.sh
|
||||
|
||||
# 2. Compile hacked/ for Linux and Windows
|
||||
@@ -111,3 +136,105 @@ Rebecca Lansing's first email.
|
||||
`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
|
||||
|
||||
```sh
|
||||
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](../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](../README.md) section 4) alongside the plain
|
||||
`run.sh`/`run.bat`:
|
||||
|
||||
```sh
|
||||
../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:
|
||||
|
||||
```sh
|
||||
../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`.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Build environment for modding/hacked/ (vendored from
|
||||
# Build environment for modding/src/hacked/ (vendored from
|
||||
# https://github.com/inkyblackness/hacked, see ../README.md). Fetches the Go
|
||||
# toolchain and this module's dependencies once, here, at image-build time, so
|
||||
# ../hacked-build-run-image.sh can compile fully offline afterward. Rebuild
|
||||
# with ../hacked-build-image.sh whenever a version below changes, or whenever
|
||||
# ../hacked/go.mod or go.sum changes (its dependencies are baked into the
|
||||
# ../src/hacked/go.mod or go.sum changes (its dependencies are baked into the
|
||||
# image's module cache).
|
||||
FROM ubuntu:22.04
|
||||
|
||||
@@ -40,7 +40,7 @@ ENV GOPATH=/opt/go
|
||||
# container never needs network access to build it. Only go.mod/go.sum are
|
||||
# copied in for this - not the rest of the source - so editing hacked's own
|
||||
# .go files doesn't invalidate this layer's cache.
|
||||
COPY hacked/go.mod hacked/go.sum /tmp/hacked-deps/
|
||||
COPY src/hacked/go.mod src/hacked/go.sum /tmp/hacked-deps/
|
||||
RUN cd /tmp/hacked-deps && go mod download && rm -rf /tmp/hacked-deps
|
||||
|
||||
COPY hacked-build-image/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compiles modding/hacked/ for Linux and Windows (amd64), entirely offline -
|
||||
# every Go module dependency was already fetched into the build-image at
|
||||
# image-build time (see Dockerfile). Baked into the image at
|
||||
# Compiles modding/src/hacked/ for Linux and Windows (amd64), entirely
|
||||
# offline - every Go module dependency was already fetched into the
|
||||
# build-image at image-build time (see Dockerfile). Baked into the image at
|
||||
# /usr/local/bin/build-hacked.sh (see Dockerfile's CMD) - run via
|
||||
# ../hacked-build-run-image.sh, which bind-mounts modding/ at /workspace, not
|
||||
# directly.
|
||||
@@ -29,12 +29,12 @@ echo "== Preparing scratch, patched copies of hacked/ and sqweek-dialog/ =="
|
||||
# scratch copies must keep that same relative layout.
|
||||
rm -rf "$SCRATCH_DIR"
|
||||
mkdir -p "$SCRATCH_DIR"
|
||||
cp -a "$MODDING_ROOT/hacked" "$HACKED_SRC"
|
||||
cp -a "$MODDING_ROOT/sqweek-dialog" "$DIALOG_SRC"
|
||||
for p in "$MODDING_ROOT"/hacked-patches/*.patch; do
|
||||
cp -a "$MODDING_ROOT/src/hacked" "$HACKED_SRC"
|
||||
cp -a "$MODDING_ROOT/src/sqweek-dialog" "$DIALOG_SRC"
|
||||
for p in "$MODDING_ROOT"/src/hacked-patches/*.patch; do
|
||||
patch -p1 -d "$HACKED_SRC" < "$p"
|
||||
done
|
||||
for p in "$MODDING_ROOT"/sqweek-dialog-patches/*.patch; do
|
||||
for p in "$MODDING_ROOT"/src/sqweek-dialog-patches/*.patch; do
|
||||
patch -p1 -d "$DIALOG_SRC" < "$p"
|
||||
done
|
||||
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the mdlp-image (mdlp-image/Dockerfile) locally, tagged with
|
||||
# mdlp-image/VERSION and :latest. Does not push - test it with
|
||||
# ./mdlp-run-image.sh first, then publish with ./mdlp-upload-image.sh.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
ROOT="$(pwd)"
|
||||
|
||||
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||
|
||||
command -v docker >/dev/null 2>&1 \
|
||||
|| fail "docker not found in PATH"
|
||||
|
||||
IMAGE="${MDLP_REGISTRY_IMAGE:-questshock/mdlp}"
|
||||
if [ -f "$ROOT/../registry.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT/../registry.env"
|
||||
IMAGE="${MDLP_REGISTRY_IMAGE:-$IMAGE}"
|
||||
fi
|
||||
|
||||
VERSION="$(<"$ROOT/mdlp-image/VERSION")"
|
||||
[ -n "$VERSION" ] || fail "mdlp-image/VERSION is empty"
|
||||
|
||||
echo "== Building $IMAGE:$VERSION =="
|
||||
docker build \
|
||||
-f "$ROOT/mdlp-image/Dockerfile" \
|
||||
-t "$IMAGE:$VERSION" \
|
||||
-t "$IMAGE:latest" \
|
||||
"$ROOT"
|
||||
|
||||
echo "== Done =="
|
||||
echo "Image: $IMAGE:$VERSION (and :latest)"
|
||||
echo "Test it with ./mdlp-run-image.sh, then publish with ./mdlp-upload-image.sh"
|
||||
@@ -0,0 +1,51 @@
|
||||
# Runtime environment for modding/src/mdlp/ (the first-party Mod Definition
|
||||
# Language Processor - see ../README.md): builds mdlp from source and runs it
|
||||
# against modding/mods/ to produce modding/dist/mods/. Kept as its own image,
|
||||
# separate from hacked-build-image/, because mod processing is expected to
|
||||
# grow its own external tool dependencies (image/audio conversion, etc.)
|
||||
# independent of hacked's own GTK/GLFW/mingw toolchain.
|
||||
FROM ubuntu:22.04
|
||||
|
||||
# go.mod's own "go" directive is a minimum toolchain version, not a version to
|
||||
# fetch - pin a stable release that already satisfies it so `go build` never
|
||||
# needs to fetch a newer toolchain over the network at build time.
|
||||
ARG GO_VERSION=1.27.1
|
||||
|
||||
RUN apt-get update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -sSLO "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
|
||||
&& tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz" \
|
||||
&& rm "go${GO_VERSION}.linux-amd64.tar.gz"
|
||||
ENV PATH=/usr/local/go/bin:${PATH}
|
||||
ENV GOTOOLCHAIN=local
|
||||
ENV GOPATH=/opt/go
|
||||
|
||||
# Populates /opt/go/pkg/mod with every dependency mdlp/go.mod pins, so the
|
||||
# container never needs network access to build it. mdlp/go.mod replaces
|
||||
# github.com/inkyblackness/hacked with the local ../hacked (see ../README.md),
|
||||
# so that module's own go.mod/go.sum have to be present too for dependency
|
||||
# resolution - only these two go.mod/go.sum pairs are copied in for this, not
|
||||
# the rest of either source tree, so editing .go files doesn't invalidate
|
||||
# this layer's cache.
|
||||
COPY src/hacked/go.mod src/hacked/go.sum /tmp/mdlp-deps/hacked/
|
||||
COPY src/mdlp/go.mod src/mdlp/go.sum /tmp/mdlp-deps/mdlp/
|
||||
RUN cd /tmp/mdlp-deps/mdlp && go mod download && rm -rf /tmp/mdlp-deps
|
||||
|
||||
COPY mdlp-image/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY mdlp-image/process-mods.sh /usr/local/bin/process-mods.sh
|
||||
# 755, not +x: see hacked-build-image/Dockerfile's identical comment - the
|
||||
# entrypoint drops to a `builder` user matching the host's UID/GID, which is
|
||||
# never in these root-owned files' group.
|
||||
RUN chmod 755 /usr/local/bin/docker-entrypoint.sh /usr/local/bin/process-mods.sh
|
||||
|
||||
WORKDIR /workspace/modding
|
||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||
CMD ["/usr/local/bin/process-mods.sh"]
|
||||
|
||||
# Marks a shell as already running inside this image, same convention as
|
||||
# hacked-build-image/Dockerfile - lets ../Makefile compile directly instead
|
||||
# of trying to docker-run this image again from inside itself.
|
||||
ENV MDLP_IMAGE=1
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drops from root (needed to read the Go toolchain and module cache under
|
||||
# /usr/local/go and /opt/go) to a user matching the host's UID/GID, so
|
||||
# anything written into the bind-mounted repo (modding/dist/, ...) is owned
|
||||
# by the host user instead of root.
|
||||
set -euo pipefail
|
||||
|
||||
USER_UID="${HOST_UID:-1000}"
|
||||
USER_GID="${HOST_GID:-1000}"
|
||||
|
||||
groupadd -g "$USER_GID" builder 2>/dev/null || true
|
||||
useradd -u "$USER_UID" -g "$USER_GID" -m -s /bin/bash builder 2>/dev/null || true
|
||||
|
||||
# /workspace (the bind-mounted repo) may actually be owned by a group the
|
||||
# host user only has via *supplementary* membership rather than their
|
||||
# primary GID above - e.g. a VirtualBox vboxsf shared folder, which shows
|
||||
# up as root:vboxsf on the host and would otherwise be inaccessible to
|
||||
# `builder` here (falls through to "other", which vboxsf's default mode
|
||||
# leaves with no permissions at all). Join whatever group actually owns
|
||||
# /workspace too, if it differs.
|
||||
WORKSPACE_GID="$(stat -c %g /workspace 2>/dev/null || true)"
|
||||
if [ -n "$WORKSPACE_GID" ] && [ "$WORKSPACE_GID" != "$USER_GID" ]; then
|
||||
groupadd -g "$WORKSPACE_GID" workspace 2>/dev/null || true
|
||||
usermod -aG "$WORKSPACE_GID" builder 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# The Go module cache under /opt/go was populated as root at image-build
|
||||
# time (see Dockerfile) - readable by anyone, but go build also wants to
|
||||
# write GOCACHE (build cache) under there by default. Point it at a
|
||||
# per-run tmpfs-friendly location inside the builder's own home instead of
|
||||
# fighting permissions on the image's baked-in GOPATH.
|
||||
export GOCACHE=/home/builder/.cache/go-build
|
||||
export GOMODCACHE=/opt/go/pkg/mod
|
||||
|
||||
exec gosu builder "$@"
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds modding/src/mdlp/ (the first-party Mod Definition Language
|
||||
# Processor, see ../README.md) and runs it against modding/mods/ to produce
|
||||
# modding/dist/mods/, entirely offline - mdlp's own Go module dependencies
|
||||
# were already fetched into the image at image-build time (see Dockerfile).
|
||||
#
|
||||
# Baked into the image at /usr/local/bin/process-mods.sh (see Dockerfile's
|
||||
# CMD) - run via ../mdlp-run-image.sh, which bind-mounts the whole
|
||||
# repository (not just modding/) at /workspace, since mdlp also needs the
|
||||
# game's own data directory outside modding/.
|
||||
#
|
||||
# Set MODS="name1 name2" (before calling ../mdlp-run-image.sh) to build only
|
||||
# those mods; unset or empty builds every subdirectory under mods/.
|
||||
set -euo pipefail
|
||||
|
||||
MODDING_ROOT="$(pwd)"
|
||||
REPO_ROOT="$(cd .. && pwd)"
|
||||
|
||||
DATA_DIR="${MDLP_DATA_DIR:-$REPO_ROOT/res/assets/ss_ee/data}"
|
||||
OUT_DIR="$MODDING_ROOT/dist/mods"
|
||||
|
||||
[ -d "$DATA_DIR" ] \
|
||||
|| { echo "Game data not found at $DATA_DIR - see the root README.md section 3." >&2; exit 1; }
|
||||
|
||||
echo "== Building mdlp =="
|
||||
BIN_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$BIN_DIR"' EXIT
|
||||
(cd "$MODDING_ROOT/src/mdlp" && go build -trimpath -o "$BIN_DIR/mdlp" .)
|
||||
|
||||
MDLP_ARGS=(-data "$DATA_DIR" -mods "$MODDING_ROOT/mods" -out "$OUT_DIR")
|
||||
if [ -n "${MODS:-}" ]; then
|
||||
MDLP_ARGS+=(-only "$MODS")
|
||||
fi
|
||||
|
||||
echo "== Processing mods =="
|
||||
"$BIN_DIR/mdlp" "${MDLP_ARGS[@]}"
|
||||
|
||||
echo "== Done =="
|
||||
echo "Output: $OUT_DIR"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the mdlp-image to build the mod(s) defined under modding/mods/ (see
|
||||
# README.md) into modding/dist/mods/, entirely offline - every Go module
|
||||
# dependency mdlp needs was already fetched into the image by
|
||||
# ./mdlp-image.sh.
|
||||
#
|
||||
# Mounts the whole repository, not just modding/, because mdlp also needs the
|
||||
# game's own data directory (res/assets/ss_ee/data) as read-only reference
|
||||
# data (see the root README.md section 3 for how that gets populated).
|
||||
#
|
||||
# Usage: MODS="name1 name2" ./mdlp-run-image.sh [command...]
|
||||
# With no arguments, runs the image's baked-in process-mods.sh (its default
|
||||
# CMD), building every mod under mods/ (or only those named in MODS, if
|
||||
# set). Pass a command (e.g. `bash`) to get a shell in the build
|
||||
# environment instead, useful for debugging a failed build.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
MODDING_ROOT="$(pwd)"
|
||||
REPO_ROOT="$(cd .. && pwd)"
|
||||
|
||||
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||
|
||||
command -v docker >/dev/null 2>&1 \
|
||||
|| fail "docker not found in PATH"
|
||||
|
||||
IMAGE="${MDLP_REGISTRY_IMAGE:-questshock/mdlp}"
|
||||
if [ -f "$MODDING_ROOT/../registry.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$MODDING_ROOT/../registry.env"
|
||||
IMAGE="${MDLP_REGISTRY_IMAGE:-$IMAGE}"
|
||||
fi
|
||||
|
||||
IMAGE_TAG="$(<"$MODDING_ROOT/mdlp-image/VERSION")"
|
||||
[ -n "$IMAGE_TAG" ] || fail "mdlp-image/VERSION is empty"
|
||||
IMAGE="$IMAGE:$IMAGE_TAG"
|
||||
|
||||
docker image inspect "$IMAGE" >/dev/null 2>&1 \
|
||||
|| fail "$IMAGE not found locally - run ./mdlp-image.sh first"
|
||||
|
||||
TTY_FLAGS="-i"
|
||||
[ -t 1 ] && TTY_FLAGS="-it"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
docker run --rm $TTY_FLAGS \
|
||||
-v "$REPO_ROOT:/workspace" \
|
||||
-e HOST_UID="$(id -u)" \
|
||||
-e HOST_GID="$(id -g)" \
|
||||
-e MODS="${MODS:-}" \
|
||||
"$IMAGE" \
|
||||
"$@"
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pushes the mdlp-image - already built locally with ./mdlp-image.sh, and
|
||||
# ideally verified with ./mdlp-run-image.sh - to a container registry, so CI
|
||||
# or other machines can reuse it without rebuilding the whole toolchain.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
ROOT="$(pwd)"
|
||||
|
||||
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
|
||||
|
||||
command -v docker >/dev/null 2>&1 \
|
||||
|| fail "docker not found in PATH"
|
||||
|
||||
[ -f "$ROOT/../registry.env" ] \
|
||||
|| fail "../registry.env not found - copy ../registry.env.example to ../registry.env and fill in your registry credentials"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT/../registry.env"
|
||||
|
||||
: "${REGISTRY:?registry.env must set REGISTRY}"
|
||||
: "${REGISTRY_USER:?registry.env must set REGISTRY_USER}"
|
||||
: "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}"
|
||||
MDLP_REGISTRY_IMAGE="${MDLP_REGISTRY_IMAGE:-questshock/mdlp}"
|
||||
|
||||
VERSION="$(<"$ROOT/mdlp-image/VERSION")"
|
||||
[ -n "$VERSION" ] || fail "mdlp-image/VERSION is empty"
|
||||
|
||||
docker image inspect "$MDLP_REGISTRY_IMAGE:$VERSION" >/dev/null 2>&1 \
|
||||
|| fail "$MDLP_REGISTRY_IMAGE:$VERSION not found locally - run ./mdlp-image.sh first"
|
||||
|
||||
echo "== Logging in to $REGISTRY =="
|
||||
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
|
||||
|
||||
echo "== Pushing $MDLP_REGISTRY_IMAGE:$VERSION and :latest =="
|
||||
docker push "$MDLP_REGISTRY_IMAGE:$VERSION"
|
||||
docker push "$MDLP_REGISTRY_IMAGE:latest"
|
||||
|
||||
echo "== Done =="
|
||||
echo "Image: $MDLP_REGISTRY_IMAGE:$VERSION"
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "Logs",
|
||||
"index": 21,
|
||||
"sender": "Sender: #2-4601",
|
||||
"verboseTextFile": "log-021.txt"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Looks like Diego's happy with my work... they're firing up the sleep machine for me now. Gotta admit, when the goons from Tri-Op caught me I thought for sure they'd take me off line. Instead, Diego just asks for a "favor." Hack him into SHODAN, and all is forgiven. Plus, six months in a healing coma earns me a cyberjack interface even TriOp's execs couldn't swing. I'll be king of the Net. Even so, I have just handed the most powerful AI in the system to a fumbling corp VP, and there's no telling what'll happen. They tell me the coma leaves you foggy, so I'll leave myself some reminders. First off, the combo to the healing suite is 451. Second, I've stashed some useful stuff in the maintenance hatch under the grating "north" of the healing suite. Last and best, I finished the system analyzer which'll let me keep an eye on Shodan's processes. It's in the storage closet outside the sleep machine. In ten minutes it's off to bed for half a year. Good night. Questshock Mod Version: 0.0.1
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "Mails",
|
||||
"index": 26,
|
||||
"sender": "Rebecca Lansing1",
|
||||
"title": "Rebecca-001",
|
||||
"verboseTextFile": "mail-026.txt"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Listen to me very carefully HACKER. Something has gone terribly wrong on Citadel station. You might be the last survivor. Communications are being jammed. We know that something on board is attempting to harm humanity. The mining laser is being charged for a possible strike against Earth populations. On your level is the office of a Dr. D'Arcy, who may know something about the laser. The office is located near the central hub, on a balcony, with the medical icon clearly visible. When the laser is destroyed, you will have to reach the bridge and stop whatever is responsible for the catastrophe. We believe that the biological labs and security systems have gone haywire, so be careful. If you pull this off, we will be able to get you out alive. Good luck.
|
||||
@@ -0,0 +1,12 @@
|
||||
module questshock/mdlp
|
||||
|
||||
go 1.25.0
|
||||
|
||||
// hacked/ is vendored locally (see ../../README.md); mdlp reuses its resource
|
||||
// read/write code (ss1/edit, ss1/world, ss1/content/text) instead of
|
||||
// duplicating System Shock's file formats.
|
||||
require github.com/inkyblackness/hacked v0.0.0
|
||||
|
||||
require github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
|
||||
replace github.com/inkyblackness/hacked => ../hacked
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -0,0 +1,301 @@
|
||||
// Command mdlp (Mod Definition Language Processor) turns the declarative mod
|
||||
// definitions under modding/mods/ into hacked-compatible mod folders and zip
|
||||
// archives under modding/dist/mods/. See ../../README.md for the MDL schema.
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
var messageTypeIDs = map[string]resource.ID{
|
||||
"Mails": ids.MailsStart,
|
||||
"Logs": ids.LogsStart,
|
||||
"Fragments": ids.FragmentsStart,
|
||||
}
|
||||
|
||||
var languagesByName = map[string]resource.Language{
|
||||
"Default": resource.LangDefault,
|
||||
"French": resource.LangFrench,
|
||||
"German": resource.LangGerman,
|
||||
}
|
||||
|
||||
// messageDef is one message entry's MDL definition. A field is left
|
||||
// unchanged from the game's own data unless present here - pointers (and the
|
||||
// two *File variants for the body text) distinguish "not set" from a
|
||||
// legitimate zero value.
|
||||
type messageDef struct {
|
||||
Type string `json:"type"`
|
||||
Index int `json:"index"`
|
||||
Language string `json:"language"`
|
||||
|
||||
Title *string `json:"title"`
|
||||
Sender *string `json:"sender"`
|
||||
Subject *string `json:"subject"`
|
||||
|
||||
VerboseText *string `json:"verboseText"`
|
||||
VerboseTextFile *string `json:"verboseTextFile"`
|
||||
TerseText *string `json:"terseText"`
|
||||
TerseTextFile *string `json:"terseTextFile"`
|
||||
|
||||
NextMessage *int `json:"nextMessage"`
|
||||
IsInterrupt *bool `json:"isInterrupt"`
|
||||
ColorIndex *int `json:"colorIndex"`
|
||||
LeftDisplay *int `json:"leftDisplay"`
|
||||
RightDisplay *int `json:"rightDisplay"`
|
||||
}
|
||||
|
||||
// immediateCommander applies a command straight to the mod instead of
|
||||
// keeping an undo history - mdlp is one-shot, it never needs to undo.
|
||||
type immediateCommander struct{ mod *world.Mod }
|
||||
|
||||
func (c immediateCommander) Queue(command cmd.Command) {
|
||||
c.mod.Modify(func(modder world.Modder) {
|
||||
_ = command.Do(modder)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
dataDir := flag.String("data", "", "directory with the game's own .res/.dat files (Static World Data)")
|
||||
modsDir := flag.String("mods", "mods", "directory containing one subdirectory per mod definition")
|
||||
outDir := flag.String("out", "dist/mods", "directory to write built mods into")
|
||||
only := flag.String("only", "", "space-separated list of mod names to build (default: all subdirectories of -mods)")
|
||||
flag.Parse()
|
||||
|
||||
if *dataDir == "" {
|
||||
log.Fatal("-data is required")
|
||||
}
|
||||
|
||||
// hacked's own FileLocation.AbsolutePathFrom (used by ProjectService.
|
||||
// SaveModUnder) assumes an absolute base path - given a relative one, its
|
||||
// filepath.Rel resolution silently cancels out and writes resources into
|
||||
// the current directory instead of under -out. Resolve every path
|
||||
// up front rather than relying on that undocumented assumption.
|
||||
mustAbs := func(name, path string) string {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Fatalf("resolving %s %q: %v", name, path, err)
|
||||
}
|
||||
return abs
|
||||
}
|
||||
*dataDir = mustAbs("-data", *dataDir)
|
||||
*modsDir = mustAbs("-mods", *modsDir)
|
||||
*outDir = mustAbs("-out", *outDir)
|
||||
|
||||
names, err := modNames(*modsDir, *only)
|
||||
if err != nil {
|
||||
log.Fatalf("listing mods: %v", err)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
log.Fatalf("no mods found under %s", *modsDir)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
fmt.Printf("== Building mod %q ==\n", name)
|
||||
modOutDir := filepath.Join(*outDir, name)
|
||||
if err := buildMod(*dataDir, filepath.Join(*modsDir, name), modOutDir, name); err != nil {
|
||||
log.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func modNames(modsDir, only string) ([]string, error) {
|
||||
if strings.TrimSpace(only) != "" {
|
||||
return strings.Fields(only), nil
|
||||
}
|
||||
entries, err := os.ReadDir(modsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var names []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func buildMod(dataDir, modDir, outDir, name string) error {
|
||||
mod := world.NewMod(func([]resource.ID, []resource.ID) {}, func() {})
|
||||
|
||||
entry, err := world.NewManifestEntryFrom([]string{dataDir})
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading game data from %s: %w", dataDir, err)
|
||||
}
|
||||
if err := mod.World().InsertEntry(0, entry); err != nil {
|
||||
return fmt.Errorf("registering game data: %w", err)
|
||||
}
|
||||
|
||||
cp := text.DefaultCodepage()
|
||||
messageCache := text.NewElectronicMessageCache(cp, mod)
|
||||
|
||||
defs, err := filepath.Glob(filepath.Join(modDir, "messages", "*.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, defFile := range defs {
|
||||
if err := applyMessageDef(mod, messageCache, cp, defFile); err != nil {
|
||||
return fmt.Errorf("%s: %w", defFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
modOutDir := filepath.Join(outDir, "mod")
|
||||
if err := os.RemoveAll(modOutDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(modOutDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
registry := &cmd.TransactionBuilder{Commander: immediateCommander{mod: mod}}
|
||||
project := edit.NewProjectService(registry, mod)
|
||||
if err := project.SaveModUnder(modOutDir); err != nil {
|
||||
return fmt.Errorf("saving mod: %w", err)
|
||||
}
|
||||
|
||||
zipPath := filepath.Join(outDir, name+".zip")
|
||||
if err := zipDir(modOutDir, zipPath); err != nil {
|
||||
return fmt.Errorf("packaging zip: %w", err)
|
||||
}
|
||||
fmt.Printf(" mod: %s\n", modOutDir)
|
||||
fmt.Printf(" zip: %s\n", zipPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyMessageDef(mod *world.Mod, cache *text.ElectronicMessageCache, cp text.Codepage, defFile string) error {
|
||||
raw, err := os.ReadFile(defFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var def messageDef
|
||||
if err := json.Unmarshal(raw, &def); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startID, ok := messageTypeIDs[def.Type]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown message type %q", def.Type)
|
||||
}
|
||||
lang := resource.LangDefault
|
||||
if def.Language != "" {
|
||||
l, ok := languagesByName[def.Language]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown language %q", def.Language)
|
||||
}
|
||||
lang = l
|
||||
}
|
||||
|
||||
key := resource.KeyOf(startID, lang, def.Index)
|
||||
msg, err := cache.Message(key)
|
||||
if err != nil {
|
||||
msg = text.EmptyElectronicMessage()
|
||||
}
|
||||
|
||||
defDir := filepath.Dir(defFile)
|
||||
readSidecar := func(name string) (string, error) {
|
||||
body, err := os.ReadFile(filepath.Join(defDir, name))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading %s: %w", name, err)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
if def.Title != nil {
|
||||
msg.Title = *def.Title
|
||||
}
|
||||
if def.Sender != nil {
|
||||
msg.Sender = *def.Sender
|
||||
}
|
||||
if def.Subject != nil {
|
||||
msg.Subject = *def.Subject
|
||||
}
|
||||
switch {
|
||||
case def.VerboseTextFile != nil:
|
||||
if msg.VerboseText, err = readSidecar(*def.VerboseTextFile); err != nil {
|
||||
return err
|
||||
}
|
||||
case def.VerboseText != nil:
|
||||
msg.VerboseText = *def.VerboseText
|
||||
}
|
||||
switch {
|
||||
case def.TerseTextFile != nil:
|
||||
if msg.TerseText, err = readSidecar(*def.TerseTextFile); err != nil {
|
||||
return err
|
||||
}
|
||||
case def.TerseText != nil:
|
||||
msg.TerseText = *def.TerseText
|
||||
}
|
||||
if def.NextMessage != nil {
|
||||
msg.NextMessage = *def.NextMessage
|
||||
}
|
||||
if def.IsInterrupt != nil {
|
||||
msg.IsInterrupt = *def.IsInterrupt
|
||||
}
|
||||
if def.ColorIndex != nil {
|
||||
msg.ColorIndex = *def.ColorIndex
|
||||
}
|
||||
if def.LeftDisplay != nil {
|
||||
msg.LeftDisplay = *def.LeftDisplay
|
||||
}
|
||||
if def.RightDisplay != nil {
|
||||
msg.RightDisplay = *def.RightDisplay
|
||||
}
|
||||
|
||||
data := msg.Encode(cp)
|
||||
id := startID.Plus(def.Index)
|
||||
mod.Modify(func(modder world.Modder) {
|
||||
modder.SetResourceBlocks(lang, id, data)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func zipDir(srcDir, zipPath string) error {
|
||||
f, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
zw := zip.NewWriter(f)
|
||||
defer zw.Close()
|
||||
|
||||
return filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(srcDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := zw.Create(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -12,3 +12,7 @@ REGISTRY_PASSWORD=
|
||||
# same file. REGISTRY/REGISTRY_USER/REGISTRY_PASSWORD above are shared with
|
||||
# it for pushing.
|
||||
HACKED_REGISTRY_IMAGE=cr.ladkau.de/questshock/hacked-builder
|
||||
|
||||
# Optional: modding/mdlp-image.sh's own image name, read from this same file.
|
||||
# REGISTRY/REGISTRY_USER/REGISTRY_PASSWORD above are shared with it too.
|
||||
MDLP_REGISTRY_IMAGE=cr.ladkau.de/questshock/mdlp
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
REM Launches System Shock with every mod built under modding\dist\mods\
|
||||
REM applied (see modding/README.md section 4). Each mod's own
|
||||
REM modding\dist\mods\<name>\mod\ folder is passed to systemshock.exe as a
|
||||
REM separate, absolute argument - see run-mods.sh's comment for why (same
|
||||
REM engine, same one-level directory scan and same relative-path pitfall).
|
||||
cd /d "%~dp0"
|
||||
|
||||
set "MODS_DIR=%~dp0..\modding\dist\mods"
|
||||
set "MOD_ARGS="
|
||||
|
||||
if exist "%MODS_DIR%" (
|
||||
for /d %%M in ("%MODS_DIR%\*") do (
|
||||
if exist "%%M\mod" set "MOD_ARGS=!MOD_ARGS! "%%~fM\mod""
|
||||
)
|
||||
)
|
||||
|
||||
systemshock.exe !MOD_ARGS! %*
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launches System Shock with every mod built under modding/dist/mods/
|
||||
# applied (see modding/README.md section 4). Each mod's own
|
||||
# modding/dist/mods/<name>/mod/ folder is passed to run.sh as a separate,
|
||||
# absolute argument - the engine's ProcessModDirectory()
|
||||
# (engine/src/MacSrc/Modding.c) only scans one directory level deep, so it
|
||||
# can never be pointed at modding/dist/mods/ itself and find the mods nested
|
||||
# a level further inside, and a relative argument would resolve against the
|
||||
# wrong directory once run.sh changes into its own.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
MODS_DIR="$(pwd)/../modding/dist/mods"
|
||||
MOD_ARGS=()
|
||||
if [ -d "$MODS_DIR" ]; then
|
||||
for mod in "$MODS_DIR"/*/mod; do
|
||||
[ -d "$mod" ] && MOD_ARGS+=("$(cd "$mod" && pwd)")
|
||||
done
|
||||
fi
|
||||
|
||||
exec ./run.sh "${MOD_ARGS[@]}" "$@"
|
||||
Reference in New Issue
Block a user