#!/usr/bin/env bash # Installs the Satisfactory Sync mod into the Steam copy of Satisfactory on Linux (Proton). # # curl -fsSL https://satisfactory-sync.jgabor.se | bash # # It finds the game in your Steam libraries, downloads the latest release, checks its SHA-256, and # replaces FactoryGame/Mods/SatisfactorySync. Nothing else is changed. # SATISFACTORY_DIR= skips the Steam library search. # The whole script is one function called on the last line, so a cut-off download runs nothing. main() { set -euo pipefail local base="${SATISFACTORY_SYNC_URL:-https://satisfactory-sync.jgabor.se}" local game mods stage release file sha256 version actual fail() { echo "satisfactory-sync: $*" >&2 exit 1 } field() { # name, compact JSON sed -n "s/.*\"$1\": *\"\([^\"]*\)\".*/\1/p" <<<"$2" } find_game() { local root library dir for root in "$HOME/.local/share/Steam" "$HOME/.steam/steam" \ "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"; do [[ -f "$root/steamapps/libraryfolders.vdf" ]] || continue while IFS= read -r library; do [[ -f "$library/steamapps/appmanifest_526870.acf" ]] || continue dir="$(sed -n 's/^[[:space:]]*"installdir"[[:space:]]*"\(.*\)"[[:space:]]*$/\1/p' \ "$library/steamapps/appmanifest_526870.acf")" echo "$library/steamapps/common/${dir:-Satisfactory}" return 0 done < <( echo "$root" sed -n 's/^[[:space:]]*"path"[[:space:]]*"\(.*\)"[[:space:]]*$/\1/p' "$root/steamapps/libraryfolders.vdf" ) done return 1 } command -v curl >/dev/null || fail "needs curl" command -v sha256sum >/dev/null || fail "needs sha256sum" command -v unzip >/dev/null || command -v bsdtar >/dev/null || fail "needs unzip or bsdtar" game="${SATISFACTORY_DIR:-}" if [[ -z "$game" ]]; then game="$(find_game)" || fail "Satisfactory not found in your Steam libraries; set SATISFACTORY_DIR to the game folder" fi [[ -d "$game/FactoryGame" ]] || fail "not a Satisfactory folder: $game" if pgrep -f 'FactoryGameSteam(-Win64-Shipping)?\.exe' >/dev/null; then fail "close Satisfactory first" fi mods="$game/FactoryGame/Mods" mkdir -p "$mods" local sml sml="$(sed -n 's/.*"VersionName": *"\([^"]*\)".*/\1/p' "$mods/SML/SML.uplugin" 2>/dev/null || true)" if [[ -z "$sml" ]]; then echo "satisfactory-sync: warning: SML is not installed; the mod needs SML 3.12 or newer" >&2 elif [[ "$(printf '%s\n' 3.12.0 "$sml" | sort -V | head -1)" != 3.12.0 ]]; then echo "satisfactory-sync: warning: SML $sml is installed; the mod needs 3.12 or newer" >&2 fi release="$(curl -fsSL "$base/latest.json" | tr -d '\n')" || fail "could not read $base/latest.json" file="$(field file "$release")" sha256="$(field sha256 "$release")" version="$(field version "$release")" [[ "$file" =~ ^SatisfactorySync-[A-Za-z0-9.+-]+\.zip$ && "$sha256" =~ ^[0-9a-f]{64}$ ]] || fail "unexpected release data from $base/latest.json" # Stage next to Mods, not in it, so a stray copy is never loaded as a second plugin. stage="$(mktemp -d "$game/FactoryGame/.SatisfactorySync-install.XXXXXX")" # Expanded now: the trap runs after main returns, when its locals are gone. # shellcheck disable=SC2064 trap "rm -rf -- $(printf '%q' "$stage")" EXIT echo "Downloading Satisfactory Sync $version" curl -fsSL "$base/download/$file" -o "$stage/package.zip" || fail "download failed" actual="$(sha256sum "$stage/package.zip" | cut -d' ' -f1)" [[ "$actual" == "$sha256" ]] || fail "SHA-256 mismatch: expected $sha256, got $actual" mkdir "$stage/unpacked" if command -v unzip >/dev/null; then unzip -q "$stage/package.zip" -d "$stage/unpacked" else bsdtar -xf "$stage/package.zip" -C "$stage/unpacked" fi [[ -f "$stage/unpacked/SatisfactorySync/SatisfactorySync.uplugin" ]] || fail "the package has an unexpected layout" if [[ -e "$mods/SatisfactorySync" ]]; then mv -- "$mods/SatisfactorySync" "$stage/previous" fi if ! mv -- "$stage/unpacked/SatisfactorySync" "$mods/SatisfactorySync"; then [[ -e "$stage/previous" ]] && mv -- "$stage/previous" "$mods/SatisfactorySync" fail "could not install into $mods" fi echo "Installed Satisfactory Sync $version in $mods/SatisfactorySync" } main "$@"