Files
questshock/res/assets/extract_assets.sh
T
ml c0619a55ab
build / build (push) Successful in 2m22s
Add a language option to the VR menu, and fix a crash switching to it
- New "Language" row on the Main Menu tab (English/French/German),
  backed by a new "general" group in questshock.config.json
- Fixes a device-reproduced crash: switching language used to call
  language_dealfunc(), whose render_run()/opanel_redraw() repaint
  assumes the game's own in-game options panel is on screen - crashes
  (SIGSEGV) when triggered from our independent VR overlay instead.
  Now calls language_change() directly, which is all that's actually
  needed to reload resources and refresh the inventory/MFD/side-icon UI
- Fixes a second crash: this data set ships frnstrng.res/gerstrng.res
  but not mfdfrn.res/mfdger.res, and language_change() closes the
  current resource files before discovering the new ones don't exist -
  leaving no valid resource file open and crashing on the next lookup.
  Now checks both files exist before ever switching, refusing the
  change (with an in-headset popup) instead of corrupting resource state
- extract_assets.sh can now also pull mfdfrn.res/mfdger.res out of a
  System Shock: Classic installer (which includes them, unlike Enhanced
  Edition) if one is present in res/assets/ - entirely optional
- Documented all of the above in README.md/CONFIG.md
2026-09-05 08:09:12 +02:00

156 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Extracts the game data Shockolate needs out of a purchased System Shock:
# Enhanced Edition installer from gog.com, into ./ss_ee/{data,sound}.
#
# Usage:
# 1. Buy System Shock: Enhanced Edition on gog.com and download the
# offline installer (a Windows .exe, e.g.
# setup_system_shock_enhanced_edition_1.2.16_(64bit)_(44378).exe).
# 2. Drop it in this directory (res/assets/).
# 3. Run this script: ./extract_assets.sh
#
# The installer is an Inno Setup package; the actual game data lives
# inside it in sshock.kpf, which is just a zip file. Its res/pc/hd/ and
# res/pc/cdrom/ trees together hold every classic-game data/sound file
# Shockolate's res/ folder expects (res/pc/hd wins the couple of
# filenames present in both: intro.res, objprop.dat), EXCEPT
# mfdfrn.res/mfdger.res (French/German MFD art - see CONFIG.md/README.md),
# which that installer doesn't include at all - see "Optional: French/
# German MFD art" below for where those instead come from. Needs
# innoextract and unzip; if they're not installed locally, this script
# does the extraction inside a throwaway Ubuntu container instead (needs
# docker in that case, nothing else).
#
# Optional: French/German MFD art
# The Enhanced Edition installer's data set doesn't include
# mfdfrn.res/mfdger.res, so French/German are otherwise missing their
# MFD (Multi-Function Display) art and the game refuses to switch to
# them (see CONFIG.md). System Shock: Classic (also gog.com) does
# include both files. If you also own it, drop its offline installer
# (e.g. setup_system_shock_classic_2.0.0.3.exe) in this directory too -
# entirely optional, this script still runs fine without it - and this
# script pulls just those two files out of it into ss_ee/data/ as well.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
ASSETS_DIR="$(pwd)"
OUT_DIR="$ASSETS_DIR/ss_ee"
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
shopt -s nullglob
installers=(setup_system_shock_enhanced_edition*.exe)
classic_installers=(setup_system_shock_classic*.exe)
shopt -u nullglob
case "${#installers[@]}" in
0) fail "no setup_system_shock_enhanced_edition*.exe found in $ASSETS_DIR - buy System Shock: Enhanced Edition on gog.com, download the offline installer, and drop it here" ;;
1) ;;
*) fail "multiple installer .exe files found in $ASSETS_DIR - keep only one: ${installers[*]}" ;;
esac
INSTALLER="${installers[0]}"
# Optional - see "Optional: French/German MFD art" above. Empty (not
# missing/failed) when absent; only a genuine ambiguity (more than one
# candidate file) is a hard failure.
case "${#classic_installers[@]}" in
0) CLASSIC_INSTALLER="" ;;
1) CLASSIC_INSTALLER="${classic_installers[0]}" ;;
*) fail "multiple setup_system_shock_classic*.exe files found in $ASSETS_DIR - keep only one: ${classic_installers[*]}" ;;
esac
# Extracts sshock.kpf from $1 (an installer .exe) and assembles $2 (an
# ss_ee output directory) from its res/pc/{hd,cdrom} trees. Defined as a
# function so the exact same logic can run either natively or, via
# `declare -f`, inside a throwaway container.
run_extraction() {
set -euo pipefail
local installer="$1" out_dir="$2"
# Deliberately not `local`: an EXIT trap set here fires when the whole
# shell process exits, which is after this function - and this
# function's own scope - has already returned, so a `local` var would
# already be unset by then (unbound-variable error under `set -u`).
work="$(mktemp -d)"
trap 'rm -rf "${work:-}"' EXIT
echo "== Extracting sshock.kpf from installer =="
innoextract --silent --include sshock.kpf -d "$work/installer" "$installer"
echo "== Unpacking sshock.kpf (data/sound trees only) =="
mkdir -p "$work/kpf"
unzip -q "$work/installer/sshock.kpf" \
'res/pc/hd/data/*' 'res/pc/hd/sound/*' 'res/pc/cdrom/data/*' \
-d "$work/kpf"
echo "== Assembling $out_dir =="
rm -rf "$out_dir"
mkdir -p "$out_dir/data" "$out_dir/sound"
# cdrom/data first (cutscenes, audio logs, and shared tables only it
# has), then hd/data on top (wins the 2 overlapping filenames:
# intro.res, objprop.dat - the hd install's own copies).
cp -a "$work/kpf/res/pc/cdrom/data/." "$out_dir/data/"
cp -a "$work/kpf/res/pc/hd/data/." "$out_dir/data/"
cp -a "$work/kpf/res/pc/hd/sound/." "$out_dir/sound/"
}
# Extracts mfdfrn.res/mfdger.res from $1 (a System Shock: Classic installer
# .exe) into $2 (an existing ss_ee output directory's data/ subfolder) -
# see "Optional: French/German MFD art" above. Unlike the Enhanced Edition
# installer, Classic's data files sit directly in the Inno Setup package
# (no intermediate zip), so this is just a targeted innoextract --include,
# no unzip step needed. Same declare -f-into-docker structure as
# run_extraction() above.
run_classic_mfd_extraction() {
set -euo pipefail
local installer="$1" out_dir="$2"
work="$(mktemp -d)"
trap 'rm -rf "${work:-}"' EXIT
echo "== Extracting mfdfrn.res/mfdger.res from classic installer =="
innoextract --silent \
--include 'app/SSHOCK/DATA/MFDFRN.RES' --include 'app/SSHOCK/DATA/MFDGER.RES' \
-d "$work/classic" "$installer"
# Original DOS-era filenames are uppercase; Shockolate's resource
# loader expects the lowercase names already used for every other
# res/data/*.res file (case-sensitive on Linux, unlike the DOS/Windows
# filesystems these came from).
cp "$work/classic/app/SSHOCK/DATA/MFDFRN.RES" "$out_dir/data/mfdfrn.res"
cp "$work/classic/app/SSHOCK/DATA/MFDGER.RES" "$out_dir/data/mfdger.res"
}
if command -v innoextract >/dev/null 2>&1 && command -v unzip >/dev/null 2>&1; then
echo "== Using local innoextract/unzip =="
run_extraction "$ASSETS_DIR/$INSTALLER" "$OUT_DIR"
if [ -n "$CLASSIC_INSTALLER" ]; then
run_classic_mfd_extraction "$ASSETS_DIR/$CLASSIC_INSTALLER" "$OUT_DIR"
fi
else
echo "== innoextract/unzip not found locally - using a throwaway docker container =="
command -v docker >/dev/null 2>&1 \
|| fail "docker not found in PATH (needed since innoextract/unzip aren't installed locally)"
docker run --rm \
-v "$ASSETS_DIR:/assets" \
-e HOST_UID="$(id -u)" \
-e HOST_GID="$(id -g)" \
ubuntu:22.04 \
bash -c "
set -euo pipefail
apt-get update -qq >/dev/null
apt-get install -y -qq innoextract unzip >/dev/null
$(declare -f run_extraction)
$(declare -f run_classic_mfd_extraction)
run_extraction '/assets/$INSTALLER' '/assets/ss_ee'
$([ -n "$CLASSIC_INSTALLER" ] && echo "run_classic_mfd_extraction '/assets/$CLASSIC_INSTALLER' '/assets/ss_ee'")
chown -R \"\$HOST_UID:\$HOST_GID\" /assets/ss_ee
"
fi
echo "== Done =="
echo "Assets extracted to $OUT_DIR"
if [ -n "$CLASSIC_INSTALLER" ]; then
echo "Also extracted mfdfrn.res/mfdger.res (French/German MFD art) from $CLASSIC_INSTALLER."
else
echo "Note: mfdfrn.res/mfdger.res (French/German MFD art) are not present - only needed for"
echo "French/German (see CONFIG.md); drop a System Shock: Classic installer here too to get them."
fi