[
    {
        "UpdateStageName": "Instance Directory Creation",
        "UpdateSourcePlatform": "Linux",
        "UpdateSource": "CreateDirectory",
        "UpdateSourceArgs": "{{$FullBaseDir}}game",
        "SkipOnFailure": false
    },
    {
        "UpdateStageName": "Proton GE Download",
        "UpdateSourcePlatform": "Linux",
        "UpdateSource": "Executable",
        "UpdateSourceData": "/bin/bash",
        "UpdateSourceArgs": "-c \"cd \\\"{{$FullRootDir}}\\\"; mkdir -p \\\"{{$FullRootDir}}.proton/compatdata\\\" \\\"{{$FullBaseDir}}.steam/steam\\\" \\\"{{$FullBaseDir}}.config/protonfixes\\\" || exit 1; ProtonVersion=\\\"{{SpecificReleaseVersion}}\\\"; ProtonVersion=\\\"${ProtonVersion// /}\\\"; if [[ -x \\\"{{$FullRootDir}}.proton/proton\\\" && -f \\\"{{$FullRootDir}}.proton/version\\\" ]]; then read _ InstalledVersion < \\\"{{$FullRootDir}}.proton/version\\\"; else InstalledVersion=\\\"\\\"; fi; if [[ -z \\\"$ProtonVersion\\\" ]]; then ProtonVersion=$(wget -qO- https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases/latest | jq -r .tag_name); fi; if [[ ! \\\"$ProtonVersion\\\" =~ ^GE-Proton[0-9]+-[0-9]+$ ]]; then echo \\\"Invalid Proton GE release version specified\\\" && exit 1; elif [[ -n \\\"$InstalledVersion\\\" && \\\"$InstalledVersion\\\" == \\\"$ProtonVersion\\\"* ]]; then echo \\\"Proton GE version $ProtonVersion already installed. Skipping\\\" && exit 0; else rm -f GE-Proton.tar.gz >/dev/null 2>&1; suffix=\\\"\\\"; ver=\\\"${ProtonVersion#GE-Proton}\\\"; major=\\\"${ver%%-*}\\\"; minor=\\\"${ver#*-}\\\"; if (( major > 11 || (major == 11 && minor >= 4) )); then suffix=\\\"-x86_64\\\"; fi; wget -qO GE-Proton.tar.gz https://github.com/GloriousEggroll/proton-ge-custom/releases/download/$ProtonVersion/${ProtonVersion}${suffix}.tar.gz || { echo \\\"Proton GE download failed from GitHub. Aborting\\\"; exit 1; }; rm -rf \\\"{{$FullRootDir}}.proton/compatdata/\\\"* >/dev/null 2>&1; tar -xzf GE-Proton.tar.gz -C \\\"{{$FullRootDir}}.proton\\\" --strip-components=1 && rm -f GE-Proton.tar.gz >/dev/null 2>&1 && echo \\\"Proton GE version $ProtonVersion installed\\\" && { STEAM_COMPAT_DATA_PATH=\\\"{{$FullRootDir}}.proton/compatdata\\\" STEAM_COMPAT_CLIENT_INSTALL_PATH=\\\"{{$FullBaseDir}}.steam/steam\\\" HOME=\\\"{{$FullBaseDir}}\\\" WINEDEBUG=-all \\\"{{$FullRootDir}}.proton/proton\\\" runinprefix cmd /c exit >/dev/null 2>&1; exit 0; }; fi\"",
        "SkipOnFailure": false
    },
    {
        "UpdateStageName": "Game Download and Install",
        "UpdateSourcePlatform": "Linux",
        "UpdateSource": "Executable",
        "UpdateSourceData": "/bin/bash",
        "UpdateSourceArgs": "-c \"#!/bin/bash\n# Installs Farming Simulator 25 into the instance directory, for running under Proton.\n# The product key is posted to the GIANTS download portal to get the download link, then the disc image is\n# fetched, its payload read out, and the installer run under Proton.\n#\n# Unlike Farming Simulator 19, the payload is not unpacked with innoextract: FS25 ships Inno Setup 6.3, and\n# no released innoextract understands it (1.9 stops at 6.0.5). The installer is run under Wine instead, where\n# it reports \\\"User privileges: Administrative\\\" and so does not hit the admin check that blocks it on Windows.\nset -uo pipefail\n\nKEY=\\\"\\\"; DOWNLOAD_URL=\\\"\\\"; GAME_DIR=\\\"\\\"; WORK_DIR=\\\"\\\"; PROTON=\\\"\\\"; COMPAT_DIR=\\\"\\\"; STEAM_DIR=\\\"\\\"; HOME_DIR=\\\"\\\"\nwhile [[ $# -gt 0 ]]; do\n    case \\\"$1\\\" in\n        --key) KEY=\\\"$2\\\"; shift 2 ;;\n        --url) DOWNLOAD_URL=\\\"$2\\\"; shift 2 ;;\n        --gamedir) GAME_DIR=\\\"${2%/}\\\"; shift 2 ;;\n        --workdir) WORK_DIR=\\\"${2%/}\\\"; shift 2 ;;\n        --proton) PROTON=\\\"$2\\\"; shift 2 ;;\n        --compatdata) COMPAT_DIR=\\\"${2%/}\\\"; shift 2 ;;\n        --steamdir) STEAM_DIR=\\\"${2%/}\\\"; shift 2 ;;\n        --home) HOME_DIR=\\\"${2%/}\\\"; shift 2 ;;\n        *) echo \\\"ERROR: unknown argument '$1'\\\"; exit 1 ;;\n    esac\ndone\n[[ -n \\\"$GAME_DIR\\\" && -n \\\"$WORK_DIR\\\" ]] || { echo \\\"ERROR: --gamedir and --workdir are required\\\"; exit 1; }\n\nPORTAL=\\\"https://eshop.giants-software.com/downloads.php\\\"\nUA=\\\"Mozilla/5.0 (Windows NT 10.0; Win64; x64)\\\"\n# Far larger than Farming Simulator 19: a ~21GB image, read out to ~21GB alongside it, installing to ~40GB.\n# The image is deleted before the install runs, so the peak is the payload plus the installed game.\nREQUIRED_GB=70\n\n# dedicatedServer.exe appears early, so only a marker written at the end proves the install finished.\nINSTALLED_MARKER=\\\"$GAME_DIR/.amp-install-complete\\\"\nif [[ -f \\\"$INSTALLED_MARKER\\\" ]]; then\n    echo \\\"Farming Simulator 25 $(cat \\\"$INSTALLED_MARKER\\\" 2>/dev/null) already installed. Skipping\\\"\n    echo \\\"Delete the game folder inside this instance to force a reinstall\\\"\n    exit 0\nfi\nif [[ -f \\\"$GAME_DIR/dedicatedServer.exe\\\" ]]; then\n    echo \\\"A previous install did not finish; installing over it\\\"\nfi\n\nIMG=\\\"$WORK_DIR/fs25_download.img\\\"\nUNPACK=\\\"$WORK_DIR/fs25_unpack\\\"\n\n# A previous attempt that got as far as unpacking leaves the payload behind. Reuse it rather than spending\n# another 21GB download: the install step is the one most likely to need retrying, and with the payload\n# already present neither the product key nor a download link is needed.\nREUSE=0\nshopt -s nullglob nocaseglob\nfor cand in \\\"$UNPACK\\\"/Setup.exe \\\"$UNPACK\\\"/*.exe; do [[ -f \\\"$cand\\\" ]] && REUSE=1 && break; done\nshopt -u nullglob nocaseglob\nif (( REUSE )); then\n    echo \\\"Reusing the installer already unpacked in $UNPACK\\\"\n    rm -f \\\"$IMG\\\"\n    # The payload is already on disk, so only the installed game still needs room.\n    REQUIRED_GB=45\nfi\n\nif (( ! REUSE )) && [[ -z \\\"$DOWNLOAD_URL\\\" ]]; then\n    if [[ -z \\\"$KEY\\\" ]]; then\n        echo \\\"ERROR: No product key is set, so the download link cannot be looked up.\\\"\n        echo \\\"       Set the Product Key setting on this instance, then update it again.\\\"\n        exit 1\n    fi\n    echo \\\"Looking up the download link from the GIANTS download portal...\\\"\n    PAGE=$(curl -sS -A \\\"$UA\\\" --data-urlencode \\\"activationKey=$KEY\\\" --data-urlencode \\\"foobar=DOWNLOAD\\\" \\\"$PORTAL\\\") || {\n        echo \\\"ERROR: Could not reach the GIANTS download portal.\\\"; exit 1; }\n    # Titles are served from a per-product path, not the /eshop/ prefix Farming Simulator 19 uses.\n    mapfile -t LINKS < <(grep -oE 'https://cdn[0-9]*\\.giants-software\\.com/[^\\\"]+' <<<\\\"$PAGE\\\" | sort -u)\n    if [[ ${#LINKS[@]} -eq 0 ]]; then\n        echo \\\"ERROR: The download portal returned no downloads for that product key.\\\"\n        echo \\\"       Check the Product Key setting, or confirm the key at the portal in a browser.\\\"\n        exit 1\n    fi\n    DOWNLOAD_URL=$(printf '%s\\n' \\\"${LINKS[@]}\\\" | grep -E 'FarmingSimulator25[^/]*\\.img$' | head -1)\n    if [[ -z \\\"$DOWNLOAD_URL\\\" ]]; then\n        echo \\\"ERROR: No Farming Simulator 25 download was found for that product key.\\\"\n        exit 1\n    fi\n    echo \\\"Found ${DOWNLOAD_URL##*/}\\\"\nfi\n\nmkdir -p \\\"$GAME_DIR\\\" \\\"$WORK_DIR\\\" || exit 1\n\nFREE_GB=$(( $(df -P -k \\\"$GAME_DIR\\\" | awk 'NR==2{print $4}') / 1024 / 1024 ))\nif [[ $FREE_GB -lt $REQUIRED_GB ]]; then\n    echo \\\"ERROR: Only ${FREE_GB}GB is free on the instance drive, and installing Farming Simulator 25 needs about ${REQUIRED_GB}GB.\\\"\n    exit 1\nfi\n\nif (( ! REUSE )); then\nrm -rf \\\"$UNPACK\\\"; rm -f \\\"$IMG\\\"\n\necho \\\"Downloading Farming Simulator 25. This is a very large download and will take a while\\\"\n# Content-Length gives the total up front so progress can be reported; without it the download\n# still runs, just with no percentage.\nTOTAL=$(curl -fsSLI -A \\\"$UA\\\" -e \\\"$PORTAL\\\" \\\"$DOWNLOAD_URL\\\" 2>/dev/null \\\n        | tr -d '\\r' | awk 'BEGIN{IGNORECASE=1} /^content-length:/{n=$2} END{print n+0}')\ncurl -fsS -A \\\"$UA\\\" -e \\\"$PORTAL\\\" -o \\\"$IMG\\\" \\\"$DOWNLOAD_URL\\\" &\nCURL_PID=$!\nwhile kill -0 \\\"$CURL_PID\\\" 2>/dev/null; do\n    sleep 10\n    CUR=$(stat -c%s \\\"$IMG\\\" 2>/dev/null || echo 0)\n    (( TOTAL > 0 && CUR > 0 )) && awk -v c=\\\"$CUR\\\" -v t=\\\"$TOTAL\\\" \\\n        'BEGIN { printf \\\"Downloading: %.1f%% (%.2f GB of %.2f GB)\\n\\\", c*100/t, c/1073741824, t/1073741824 }'\ndone\nif ! wait \\\"$CURL_PID\\\"; then\n    echo \\\"ERROR: The download failed.\\\"\n    echo \\\"       Clear the Download Link setting so a fresh link is looked up from your key.\\\"\n    rm -f \\\"$IMG\\\"; exit 1\nfi\necho \\\"Downloaded $(awk -v b=\\\"$(stat -c%s \\\"$IMG\\\")\\\" 'BEGIN{printf \\\"%.2f\\\", b/1073741824}') GB\\\"\n\n# --- ISO9660 extraction, without mounting (mounting needs root) ---\nu8()  { echo \\\"${BYTES[$1]}\\\"; }\nu32() { echo $(( ${BYTES[$1]} | ${BYTES[$(($1+1))]}<<8 | ${BYTES[$(($1+2))]}<<16 | ${BYTES[$(($1+3))]}<<24 )); }\n\nextract_iso() {\n    local img=\\\"$1\\\" dest=\\\"$2\\\"\n    local magic\n    magic=$(dd if=\\\"$img\\\" bs=1 skip=32769 count=5 2>/dev/null)\n    [[ \\\"$magic\\\" == \\\"CD001\\\" ]] || { echo \\\"ERROR: the download is not an ISO9660 disc image\\\"; return 1; }\n\n    mapfile -t BYTES < <(dd if=\\\"$img\\\" bs=2048 skip=16 count=1 2>/dev/null | od -An -tu1 -v | tr -s ' ' '\\n' | grep -v '^$')\n    local root_lba root_size\n    root_lba=$(u32 158); root_size=$(u32 166)\n    mapfile -t BYTES < <(dd if=\\\"$img\\\" bs=2048 skip=\\\"$root_lba\\\" count=$(( (root_size + 2047) / 2048 )) 2>/dev/null | od -An -tu1 -v | tr -s ' ' '\\n' | grep -v '^$')\n\n    mkdir -p \\\"$dest\\\"\n    local p=0 len lba size flags filen name first i c\n    while (( p < root_size )); do\n        len=$(u8 $p)\n        if (( len == 0 )); then p=$(( (p / 2048 + 1) * 2048 )); continue; fi\n        lba=$(u32 $((p+2))); size=$(u32 $((p+10)))\n        flags=$(u8 $((p+25))); filen=$(u8 $((p+32))); first=${BYTES[$((p+33))]}\n        if (( filen == 1 && first <= 1 )); then p=$(( p + len )); continue; fi\n        name=\\\"\\\"\n        for (( i=0; i<filen; i++ )); do\n            c=${BYTES[$((p+33+i))]}\n            name+=$(printf \\\"\\\\$(printf '%03o' \\\"$c\\\")\\\")\n        done\n        p=$(( p + len ))\n        (( flags & 2 )) && continue\n        name=\\\"${name%%;*}\\\"; name=\\\"${name%.}\\\"\n        [[ -n \\\"$name\\\" ]] || continue\n        dd if=\\\"$img\\\" bs=2048 skip=\\\"$lba\\\" count=$(( (size + 2047) / 2048 )) 2>/dev/null | head -c \\\"$size\\\" > \\\"$dest/$name\\\"\n    done\n}\n\necho \\\"Reading the disc image\\\"\nextract_iso \\\"$IMG\\\" \\\"$UNPACK\\\" || { rm -f \\\"$IMG\\\"; exit 1; }\nrm -f \\\"$IMG\\\"\nfi   # end of download/extract, skipped when reusing an unpacked payload\n\n# ISO9660 cannot store a hyphen, so slices named \\\"Setup-1a.bin\\\" are written as \\\"SETUP_1A.BIN\\\". Wine matches\n# filenames case-insensitively, so only the separator has to be restored. Hardlinks cost no space.\nshopt -s nullglob nocaseglob\nfor f in \\\"$UNPACK\\\"/*_*.bin; do\n    base=\\\"${f##*/}\\\"\n    alt=\\\"${base//_/-}\\\"\n    [[ \\\"$alt\\\" == \\\"$base\\\" || -e \\\"$UNPACK/$alt\\\" ]] && continue\n    ln \\\"$f\\\" \\\"$UNPACK/$alt\\\" 2>/dev/null || cp \\\"$f\\\" \\\"$UNPACK/$alt\\\"\ndone\nSETUP=\\\"\\\"\nfor cand in \\\"$UNPACK\\\"/Setup.exe \\\"$UNPACK\\\"/*.exe; do [[ -f \\\"$cand\\\" ]] && SETUP=\\\"$cand\\\" && break; done\nshopt -u nullglob nocaseglob\n[[ -n \\\"$SETUP\\\" ]] || { echo \\\"ERROR: the installer was not found in the disc image\\\"; rm -rf \\\"$UNPACK\\\"; exit 1; }\n\n[[ -n \\\"$PROTON\\\" && -x \\\"$PROTON\\\" ]] || { echo \\\"ERROR: Proton was not found at '$PROTON'\\\"; exit 1; }\n[[ -n \\\"$COMPAT_DIR\\\" ]] || { echo \\\"ERROR: --compatdata is required to run the installer under Proton\\\"; exit 1; }\n# Proton will build the prefix itself, but only if the compatdata directory it is pointed at already exists.\nmkdir -p \\\"$COMPAT_DIR\\\" \\\"${STEAM_DIR:-$COMPAT_DIR}\\\" || exit 1\nexport STEAM_COMPAT_DATA_PATH=\\\"$COMPAT_DIR\\\"\n[[ -n \\\"$STEAM_DIR\\\" ]] && export STEAM_COMPAT_CLIENT_INSTALL_PATH=\\\"$STEAM_DIR\\\"\n[[ -n \\\"$HOME_DIR\\\" ]] && { mkdir -p \\\"$HOME_DIR\\\"; export HOME=\\\"$HOME_DIR\\\"; }\nexport WINEDEBUG=\\\"${WINEDEBUG:--all}\\\"\n\n# Wine maps the Linux root at Z:, so the installer needs its destination in that form.\nwinpath() { printf 'Z:%s' \\\"$(sed 's|/|\\\\|g' <<<\\\"$1\\\")\\\"; }\nLOG=\\\"$WORK_DIR/fs25_install.log\\\"\n\n# Inno's installer engine creates a window even under /VERYSILENT, so it needs a display. Without one Wine\n# reports \\\"no driver could be loaded\\\" and Inno surfaces that as a bare \\\"System Error. Code: 2. File not\n# found.\\\", which looks nothing like the actual cause. A headless X server is enough; nothing needs to see it.\nXVFB_PID=\\\"\\\"\nif command -v Xvfb >/dev/null 2>&1; then\n    for n in $(seq 90 120); do\n        [[ -e \\\"/tmp/.X${n}-lock\\\" || -e \\\"/tmp/.X11-unix/X${n}\\\" ]] && continue\n        Xvfb \\\":$n\\\" -screen 0 1024x768x24 >/dev/null 2>&1 &\n        XVFB_PID=$!\n        export DISPLAY=\\\":$n\\\"\n        sleep 2\n        break\n    done\nfi\nif [[ -z \\\"$XVFB_PID\\\" ]]; then\n    echo \\\"ERROR: Xvfb is not installed, and the installer cannot run without a display.\\\"\n    exit 1\nfi\n\necho \\\"Installing Farming Simulator 25. This takes a long time\\\"\nPROTON_LOG=\\\"$WORK_DIR/fs25_proton.log\\\"\n\\\"$PROTON\\\" runinprefix \\\"$SETUP\\\" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /NOCANCEL /SP- \\\n    \\\"/DIR=$(winpath \\\"$GAME_DIR\\\")\\\" \\\"/LOG=$(winpath \\\"$LOG\\\")\\\" >\\\"$PROTON_LOG\\\" 2>&1\nrc=$?\n[[ -n \\\"$XVFB_PID\\\" ]] && kill \\\"$XVFB_PID\\\" >/dev/null 2>&1\n\nif [[ ! -f \\\"$GAME_DIR/dedicatedServer.exe\\\" ]]; then\n    echo \\\"ERROR: The installer finished (exit $rc) but no dedicated server was installed.\\\"\n    if [[ -f \\\"$LOG\\\" ]]; then\n        echo \\\"--- installer log ---\\\"\n        tail -20 \\\"$LOG\\\" | sed 's/^/    /'\n        echo \\\"---------------------\\\"\n    fi\n    echo \\\"Full Wine output: $PROTON_LOG\\\"\n    # The unpacked payload is kept so a retry does not need the whole download again.\n    echo \\\"The unpacked installer has been left in $UNPACK for a retry.\\\"\n    exit 1\nfi\n\nrm -rf \\\"$UNPACK\\\"\ncat \\\"$GAME_DIR/VERSION\\\" 2>/dev/null > \\\"$INSTALLED_MARKER\\\"\necho \\\"Farming Simulator 25 $(cat \\\"$GAME_DIR/VERSION\\\" 2>/dev/null) installed\\\"\n\" fs-install --key \"{{ActivationKey}}\" --url \"{{DownloadURL}}\" --gamedir \"{{$FullBaseDir}}game\" --workdir \"{{$FullBaseDir}}work\" --proton \"{{$FullRootDir}}.proton/proton\" --compatdata \"{{$FullRootDir}}.proton/compatdata\" --steamdir \"{{$FullBaseDir}}.steam/steam\" --home \"{{$FullBaseDir}}\"",
        "SkipOnFailure": false,
        "ProcessToolOutput": true
    }
]
