c0619a55ab
build / build (push) Successful in 2m22s
- 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
156 lines
7.1 KiB
Bash
Executable File
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
|