#!/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). 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). 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) 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]}" # 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/" } 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" 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) run_extraction '/assets/$INSTALLER' '/assets/ss_ee' chown -R \"\$HOST_UID:\$HOST_GID\" /assets/ss_ee " fi echo "== Done ==" echo "Assets extracted to $OUT_DIR" echo "Note: mfdfrn.res/mfdger.res (French/German MFD art) are not present in this" echo "installer and are only needed for those localizations - not for English play."