mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 07:00:30 +02:00
The Steam Frame install script built only the base game. With --retro-rewind it now fetches the Retro Rewind pack from Retro Rewind's update server, as the Quest app and Wheel Wizard do: the full download once, then each published update and its deletions. It also fetches the Retro-WFC payload online play needs, builds both products from one translation, copies the pack beside the disc on the Frame, sets [paths] retro_rewind_root and adds RetroRewind to the Steam library. --retro-rewind-pack uses a RetroRewind6 folder the player already has. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sabhzu6VgoWcHtpSk4C9KZ
681 lines
32 KiB
Bash
Executable File
681 lines
32 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Builds WiiCompiled VR for the Steam Frame from a release and your own disc, and installs it on the
|
|
# Frame. Run it again to update: it fetches the newest release and rebuilds only what changed.
|
|
#
|
|
# Launcher/steam-frame-install.sh --disc PATH [--frame HOST] [options]
|
|
#
|
|
# Or without downloading anything first:
|
|
#
|
|
# curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/openxr-work/Launcher/steam-frame-install.sh \
|
|
# | bash -s -- --disc "/path/to/Mario Kart Wii.wbfs" --frame steamos@<frame-ip>
|
|
#
|
|
# It runs on an x86_64 or ARM64 Linux PC with podman (or docker), or on the Frame itself. The game
|
|
# is built in a Debian ARM64 container, under qemu emulation on x86_64; the first build takes a few
|
|
# hours there, mostly compiling Dawn, and later ones reuse it.
|
|
#
|
|
# Options:
|
|
# --disc PATH your clean PAL RMCP01 disc: an ISO, WBFS or RVZ image (or WIA, CISO, GCZ, NFS,
|
|
# TGC), a .zip or .7z holding one, or an extracted disc folder holding sys/ and
|
|
# files/. Needed for the first build only.
|
|
# --frame HOST where to install: an SSH destination (steamos@<frame-ip>, or an ~/.ssh/config
|
|
# host such as Frame Control's "frame"), or "local" when running on the Frame.
|
|
# Without it the game is only built.
|
|
# --work-dir DIR where the toolchain, Dawn, the build and the extracted disc live
|
|
# (default ~/wiicompiled-frame; about 20 GB)
|
|
# --release TAG the release to build (default: the newest one). Ignored with --source.
|
|
# --source DIR build this source tree instead of a release (default when the script runs
|
|
# from inside one)
|
|
# --jobs N parallel compiles (default: a quarter of the memory in GB; under emulation each
|
|
# compile needs a lot of it)
|
|
# --frame-disc DIR where the extracted disc goes on the Frame: absolute, or relative to the
|
|
# Frame's home (default wiicompiled/disc; with --frame local, the disc folder
|
|
# the build used)
|
|
# --retro-rewind also build Retro Rewind and install it beside the base game. Its pack (about
|
|
# 4 GB) comes from Retro Rewind's own update server, as Wheel Wizard and the
|
|
# Quest app fetch it, and later runs apply only the updates published since.
|
|
# --retro-rewind-pack DIR
|
|
# use this RetroRewind6 folder instead of downloading one (implies
|
|
# --retro-rewind); it is copied into the work dir and never changed
|
|
# -h, --help
|
|
set -euo pipefail
|
|
|
|
# Everything is in main, so bash has read the whole script before running any of it: when it comes
|
|
# through curl | bash, a step that reads stdin cannot eat the rest. (Not indented, for the heredocs.)
|
|
main() {
|
|
trap 'printf "\nsteam-frame-install.sh: stopped by a failed step (line %s); the output above says why.\n" "$LINENO" >&2' ERR
|
|
|
|
repo=mitch030504/Wiicompiled_VR_Frame
|
|
game_id=WiiCompiled
|
|
image=docker.io/library/debian:trixie
|
|
container=wiicompiled-frame-build
|
|
nodtool_version=v2.0.0-alpha.10
|
|
|
|
say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
|
note() { printf ' %s\n' "$*"; }
|
|
fail() {
|
|
printf '\nsteam-frame-install.sh: error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
usage() { awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$script_path"; }
|
|
|
|
script_path=${BASH_SOURCE[0]:-}
|
|
script_dir=""
|
|
if [[ -n "$script_path" && -f "$script_path" ]]; then
|
|
script_dir=$(cd "$(dirname "$script_path")" && pwd)
|
|
fi
|
|
|
|
disc=""
|
|
frame=""
|
|
work_dir="$HOME/wiicompiled-frame"
|
|
release=""
|
|
source_dir=""
|
|
jobs=""
|
|
frame_disc=""
|
|
retro_rewind=0
|
|
retro_pack=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--disc) disc=$2; shift 2 ;;
|
|
--frame) frame=$2; shift 2 ;;
|
|
--work-dir) work_dir=$2; shift 2 ;;
|
|
--release) release=$2; shift 2 ;;
|
|
--source) source_dir=$2; shift 2 ;;
|
|
--jobs) jobs=$2; shift 2 ;;
|
|
--frame-disc) frame_disc=$2; shift 2 ;;
|
|
--retro-rewind) retro_rewind=1; shift ;;
|
|
--retro-rewind-pack) retro_rewind=1; retro_pack=$2; shift 2 ;;
|
|
-h|--help)
|
|
if [[ -n "$script_dir" ]]; then usage; else echo "See the comment at the top of the script."; fi
|
|
exit 0 ;;
|
|
*) fail "unknown argument: $1 (see --help)" ;;
|
|
esac
|
|
done
|
|
|
|
mkdir -p "$work_dir"
|
|
work_dir=$(cd "$work_dir" && pwd)
|
|
host_arch=$(uname -m)
|
|
case "$host_arch" in
|
|
x86_64|aarch64) ;;
|
|
*) fail "this machine is $host_arch; the build needs an x86_64 or ARM64 Linux machine" ;;
|
|
esac
|
|
[[ "$(uname -s)" == Linux ]] || fail "run this on Linux (the build uses a Linux container)"
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
say "Checking the container runtime"
|
|
runtime=""
|
|
for candidate in podman docker; do
|
|
if command -v "$candidate" >/dev/null 2>&1; then runtime=$candidate; break; fi
|
|
done
|
|
[[ -n "$runtime" ]] || fail "neither podman nor docker is installed.
|
|
Arch, CachyOS: sudo pacman -S --needed podman qemu-user-static qemu-user-static-binfmt
|
|
Debian, Ubuntu: sudo apt install podman qemu-user-static binfmt-support
|
|
Fedora: sudo dnf install podman qemu-user-static
|
|
SteamOS (the Frame) already has podman."
|
|
note "using $runtime"
|
|
for tool in curl tar; do
|
|
command -v "$tool" >/dev/null 2>&1 || fail "'$tool' is not installed"
|
|
done
|
|
|
|
platform_args=()
|
|
if [[ "$host_arch" == x86_64 ]]; then
|
|
platform_args=(--platform linux/arm64)
|
|
say "Checking ARM64 emulation"
|
|
seen=$("$runtime" run --rm "${platform_args[@]}" "$image" uname -m 2>&1 | tail -n 1 || true)
|
|
case "$seen" in
|
|
aarch64) note "ARM64 programs run under qemu" ;;
|
|
Linux)
|
|
fail "qemu is registered without the P (preserve argv[0]) flag its build needs, so every
|
|
program loses its first argument (uname -m printed 'Linux'). Register it again with flags POCF;
|
|
the README's 'Other ways to build' shows how." ;;
|
|
*)
|
|
fail "ARM64 programs do not run in containers here ($seen).
|
|
Arch, CachyOS: sudo pacman -S --needed qemu-user-static qemu-user-static-binfmt
|
|
sudo systemctl restart systemd-binfmt
|
|
Debian, Ubuntu: sudo apt install qemu-user-static binfmt-support
|
|
Fedora: sudo dnf install qemu-user-static && sudo systemctl restart systemd-binfmt
|
|
Then run this script again. On hosts whose binfmt_misc is per container (Unraid), see the
|
|
README's 'Other ways to build'." ;;
|
|
esac
|
|
fi
|
|
|
|
if [[ -z "$jobs" ]]; then
|
|
mem_gb=$(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo) / 1024 / 1024 ))
|
|
jobs=$(( mem_gb / 4 ))
|
|
(( jobs < 1 )) && jobs=1
|
|
(( jobs > $(nproc) )) && jobs=$(nproc)
|
|
fi
|
|
[[ "$jobs" =~ ^[1-9][0-9]*$ ]] || fail "--jobs must be a positive number"
|
|
note "$jobs parallel compiles"
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# The source: the tree this script is in, an explicit --source, or a release kept in the work dir.
|
|
if [[ -z "$source_dir" && -z "$release" && -n "$script_dir" && -f "$script_dir/../runtime/CMakeLists.txt" ]]; then
|
|
source_dir=$(cd "$script_dir/.." && pwd)
|
|
fi
|
|
if [[ -n "$source_dir" ]]; then
|
|
source_dir=$(cd "$source_dir" && pwd)
|
|
[[ -f "$source_dir/Launcher/local-build.sh" ]] || fail "$source_dir is not a WiiCompiled VR source tree"
|
|
say "Building the source in $source_dir"
|
|
else
|
|
if [[ -z "$release" ]]; then
|
|
release=$(curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=1" |
|
|
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)
|
|
[[ -n "$release" ]] || fail "could not find the newest release on github.com/$repo (try --release TAG)"
|
|
fi
|
|
source_dir="$work_dir/source"
|
|
have=$(cat "$source_dir/.release-tag" 2>/dev/null || true)
|
|
if [[ "$have" == "$release" ]]; then
|
|
say "Release $release is already unpacked"
|
|
else
|
|
say "Downloading release $release"
|
|
rm -rf "$work_dir/source-new"
|
|
mkdir -p "$work_dir/source-new" "$source_dir"
|
|
curl -fL --progress-bar "https://github.com/$repo/archive/refs/tags/$release.tar.gz" |
|
|
tar -xz --strip-components=1 -C "$work_dir/source-new" ||
|
|
fail "could not download release $release; the Releases page on github.com/$repo lists them"
|
|
# Only files whose content changed are copied, stamped with the current time, so the build
|
|
# recompiles exactly those; the release's own file dates can be older than the last build.
|
|
if command -v rsync >/dev/null 2>&1; then
|
|
rsync -rcE "$work_dir/source-new/" "$source_dir/"
|
|
else
|
|
(cd "$work_dir/source-new" && find . -type f -print0) |
|
|
while IFS= read -r -d '' file; do
|
|
if ! cmp -s "$work_dir/source-new/$file" "$source_dir/$file"; then
|
|
mkdir -p "$source_dir/$(dirname "$file")"
|
|
cp "$work_dir/source-new/$file" "$source_dir/$file"
|
|
touch "$source_dir/$file"
|
|
fi
|
|
done
|
|
fi
|
|
rm -rf "$work_dir/source-new"
|
|
printf '%s\n' "$release" > "$source_dir/.release-tag"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# The disc: the two files the translation reads, and the extracted disc the game reads at run time.
|
|
check_disc_id() {
|
|
# Fails unless the extracted disc in $1 is the release the translation is made for.
|
|
local id
|
|
if [[ ! -f "$1/sys/boot.bin" ]]; then
|
|
note "could not check the disc's game ID: $1/sys/boot.bin is missing"
|
|
return 0
|
|
fi
|
|
id=$(head -c 6 "$1/sys/boot.bin" | tr -dc 'A-Z0-9')
|
|
[[ "$id" == RMCP01 ]] && return 0
|
|
# An extraction of the wrong disc is not kept, so the next run extracts the one it is given.
|
|
[[ "$1" == "$work_dir/disc" ]] && rm -rf "$1"
|
|
case "$id" in
|
|
RMC?01) fail "this is the $id release of Mario Kart Wii. The translation is made for the PAL
|
|
release, RMCP01; other regions' code differs and is not supported." ;;
|
|
*) fail "this disc's game ID is ${id:-unreadable}, not Mario Kart Wii PAL (RMCP01)" ;;
|
|
esac
|
|
}
|
|
|
|
get_nodtool() {
|
|
nodtool="$work_dir/nodtool-$nodtool_version"
|
|
if [[ ! -x "$nodtool" ]]; then
|
|
curl -fL --progress-bar -o "$nodtool.partial" \
|
|
"https://github.com/encounter/nod/releases/download/$nodtool_version/nodtool-linux-$host_arch"
|
|
chmod +x "$nodtool.partial"
|
|
mv "$nodtool.partial" "$nodtool"
|
|
fi
|
|
}
|
|
|
|
archive_kind() {
|
|
# Prints zip or 7z when $1 is such an archive, judged by its first bytes rather than its name.
|
|
case "$(od -An -tx1 -N6 "$1" | tr -d ' \n')" in
|
|
504b0304*|504b0506*|504b0708*) echo zip ;;
|
|
377abcaf271c) echo 7z ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
unpack_archive() {
|
|
# unpack_archive ARCHIVE DEST KIND, with whichever unpacker this machine has.
|
|
local archive=$1 dest=$2 kind=$3 tool
|
|
for tool in 7zz 7z 7za; do
|
|
if command -v "$tool" >/dev/null 2>&1; then
|
|
"$tool" x -y "-o$dest" "$archive" >/dev/null
|
|
return
|
|
fi
|
|
done
|
|
if command -v bsdtar >/dev/null 2>&1; then
|
|
bsdtar -xf "$archive" -C "$dest"
|
|
return
|
|
fi
|
|
if [[ "$kind" == zip ]] && command -v unzip >/dev/null 2>&1; then
|
|
unzip -qo "$archive" -d "$dest"
|
|
return
|
|
fi
|
|
if [[ "$kind" == zip ]] && command -v python3 >/dev/null 2>&1; then
|
|
python3 -m zipfile -e "$archive" "$dest"
|
|
return
|
|
fi
|
|
# Nothing here unpacks it: bsdtar in a container of this machine's own architecture.
|
|
note "no 7-Zip or bsdtar on this machine; unpacking in a container"
|
|
local platform=linux/amd64
|
|
[[ "$host_arch" == aarch64 ]] && platform=linux/arm64
|
|
"$runtime" run --rm --platform "$platform" -e OWNER="$(id -u):$(id -g)" \
|
|
-v "$archive:/archive:ro,z" -v "$dest:/out:z" "$image" bash -c '
|
|
apt-get update -qq && apt-get install -y -qq --no-install-recommends libarchive-tools >/dev/null &&
|
|
bsdtar -xf /archive -C /out && chown -R "$OWNER" /out'
|
|
}
|
|
|
|
find_disc_image() {
|
|
# Prints the disc image nodtool reads under $1, largest first, preferring Mario Kart Wii PAL;
|
|
# another disc is printed if there is no RMCP01, so the ID check names what it is.
|
|
local file id first=""
|
|
while IFS= read -r -d '' file; do
|
|
id=$("$nodtool" --no-color info "$file" 2>/dev/null |
|
|
sed -n 's/^Game ID: *\([A-Z0-9]\{6\}\).*/\1/p' | head -n 1) || true
|
|
[[ -n "$id" ]] || continue
|
|
if [[ "$id" == RMCP01 ]]; then
|
|
printf '%s\n' "$file"
|
|
return 0
|
|
fi
|
|
[[ -n "$first" ]] || first=$file
|
|
done < <(find "$1" -type f -printf '%s\t%p\0' | sort -zrn | cut -zf2-)
|
|
[[ -n "$first" ]] || return 1
|
|
printf '%s\n' "$first"
|
|
}
|
|
|
|
disc_dir="$work_dir/disc"
|
|
if [[ -n "$disc" ]]; then
|
|
[[ -e "$disc" ]] || fail "no disc at $disc"
|
|
if [[ -d "$disc" ]]; then
|
|
disc_dir=$(cd "$disc" && pwd)
|
|
# Some tools put the game's partition in a subfolder (DATA/, for example).
|
|
if [[ ! -f "$disc_dir/sys/main.dol" ]]; then
|
|
for candidate in "$disc_dir"/*/; do
|
|
if [[ -f "$candidate/sys/main.dol" && -f "$candidate/files/rel/StaticR.rel" ]]; then
|
|
disc_dir=${candidate%/}
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
[[ -f "$disc_dir/sys/main.dol" && -f "$disc_dir/files/rel/StaticR.rel" ]] ||
|
|
fail "$disc is not an extracted disc: it needs sys/main.dol and files/rel/StaticR.rel"
|
|
elif [[ ! -f "$disc_dir/sys/main.dol" ]]; then
|
|
get_nodtool
|
|
disc_image=$disc
|
|
if kind=$(archive_kind "$disc"); then
|
|
say "Unpacking the $kind archive (a minute or two)"
|
|
rm -rf "$work_dir/archive"
|
|
mkdir -p "$work_dir/archive"
|
|
unpack_archive "$disc" "$work_dir/archive" "$kind" || {
|
|
rm -rf "$work_dir/archive"
|
|
fail "could not unpack $disc (a damaged or password-protected archive?)"
|
|
}
|
|
disc_image=$(find_disc_image "$work_dir/archive") || {
|
|
rm -rf "$work_dir/archive"
|
|
fail "there is no disc image nodtool reads inside $disc. It reads ISO, WBFS, RVZ, WIA,
|
|
CISO, GCZ, NFS and TGC images; an archive inside the archive has to be unpacked by hand."
|
|
}
|
|
note "found ${disc_image#"$work_dir/archive/"} inside"
|
|
fi
|
|
say "Extracting your disc (a minute or two)"
|
|
rm -rf "$disc_dir.partial"
|
|
# nodtool tells the format from the file's contents, whatever it is named.
|
|
"$nodtool" extract -q "$disc_image" "$disc_dir.partial" ||
|
|
fail "nodtool could not read $disc. It reads ISO, WBFS, RVZ, WIA, CISO, GCZ, NFS and TGC
|
|
images, and those inside a .zip or .7z."
|
|
mv "$disc_dir.partial" "$disc_dir"
|
|
rm -rf "$work_dir/archive"
|
|
fi
|
|
fi
|
|
if [[ ! -f "$disc_dir/sys/main.dol" || ! -f "$disc_dir/files/rel/StaticR.rel" ]]; then
|
|
if [[ -f "$source_dir/Assets/main.dol" && -f "$source_dir/Assets/StaticR.rel" ]]; then
|
|
disc_dir=""
|
|
else
|
|
fail "no disc yet: pass --disc with your disc image or an extracted disc folder"
|
|
fi
|
|
fi
|
|
if [[ -n "$disc_dir" ]]; then
|
|
check_disc_id "$disc_dir"
|
|
note "disc: $disc_dir"
|
|
fi
|
|
mkdir -p "$source_dir/Assets"
|
|
if [[ -n "$disc_dir" ]]; then
|
|
cp "$disc_dir/sys/main.dol" "$disc_dir/files/rel/StaticR.rel" "$source_dir/Assets/"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# Retro Rewind: its pack, as its update server publishes it, and the Retro-WFC payload its online
|
|
# play runs. Same steps as the Quest app (android/.../RetroRewindPack.kt and GameBuild.kt).
|
|
rr_server=https://update.rwfc.net/RetroRewind/
|
|
rr_pack="$work_dir/RetroRewind6"
|
|
rr_payload="$work_dir/retro-wfc/binary/payload.RMCPD00.bin"
|
|
|
|
rr_version_ok() { [[ "$1" =~ ^[0-9]+(\.[0-9]+)+$ ]]; }
|
|
rr_newer() {
|
|
# True when version $1 is newer than $2 (dotted numbers, as 6.12.7).
|
|
[[ "$1" != "$2" && "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n 1)" == "$1" ]]
|
|
}
|
|
rr_fetch() { curl -fsSL --retry 2 "$1"; }
|
|
rr_unpack_update() {
|
|
# rr_unpack_update URL DEST: downloads a published zip and lays its RetroRewind6/ tree over DEST.
|
|
# Only that tree is kept; the Riivolution XML beside it belongs to a Wii setup.
|
|
local url=${1/http:\/\/update.rwfc.net:8000\//https://update.rwfc.net/} dest=$2
|
|
[[ "$url" == https://* ]] || fail "Retro Rewind's server gave a download that is not https: $url"
|
|
rm -rf "$work_dir/rr-download" "$work_dir/rr-download.zip"
|
|
mkdir -p "$work_dir/rr-download"
|
|
curl -fL --retry 2 --progress-bar -o "$work_dir/rr-download.zip" "$url" ||
|
|
fail "could not download $url"
|
|
unpack_archive "$work_dir/rr-download.zip" "$work_dir/rr-download" zip ||
|
|
fail "could not unpack $url"
|
|
[[ -d "$work_dir/rr-download/RetroRewind6" ]] || fail "$url holds no RetroRewind6 folder"
|
|
mkdir -p "$dest"
|
|
cp -a "$work_dir/rr-download/RetroRewind6/." "$dest/"
|
|
rm -rf "$work_dir/rr-download" "$work_dir/rr-download.zip"
|
|
}
|
|
|
|
if (( retro_rewind )); then
|
|
if [[ -n "$retro_pack" ]]; then
|
|
[[ -f "$retro_pack/Binaries/Code.pul" ]] ||
|
|
fail "$retro_pack is not a RetroRewind6 folder: it needs Binaries/Code.pul"
|
|
say "Copying your Retro Rewind pack"
|
|
rm -rf "$rr_pack.partial"
|
|
cp -a "$retro_pack" "$rr_pack.partial"
|
|
rm -rf "$rr_pack" && mv "$rr_pack.partial" "$rr_pack"
|
|
else
|
|
say "Checking Retro Rewind's published version"
|
|
feed=$(rr_fetch "${rr_server}RetroRewindVersion.txt") ||
|
|
fail "could not reach Retro Rewind's update server (${rr_server})"
|
|
# One update per line: <version> <url> <path> <description>, oldest first.
|
|
updates=$(awk 'NF >= 4 && $1 ~ /^[0-9]+(\.[0-9]+)+$/ { print $1, $2 }' <<<"$feed" | sort -V -k1,1)
|
|
[[ -n "$updates" ]] || fail "Retro Rewind's version list is empty"
|
|
latest=$(tail -n 1 <<<"$updates" | cut -d' ' -f1)
|
|
installed=$(tr -d '[:space:]' 2>/dev/null < "$rr_pack/version.txt" || true)
|
|
rr_version_ok "$installed" && [[ -f "$rr_pack/Binaries/Code.pul" ]] || installed=""
|
|
if [[ -z "$installed" ]]; then
|
|
say "Downloading Retro Rewind (about 4 GB)"
|
|
base=$(rr_fetch "${rr_server}RetroRewindInstall.txt" | tr -d '[:space:]') ||
|
|
fail "Retro Rewind's server did not say where its download is"
|
|
rm -rf "$rr_pack.partial"
|
|
rr_unpack_update "$base" "$rr_pack.partial" </dev/null
|
|
[[ -f "$rr_pack.partial/Binaries/Code.pul" && -f "$rr_pack.partial/version.txt" ]] ||
|
|
fail "Retro Rewind's download did not contain the pack"
|
|
rm -rf "$rr_pack" && mv "$rr_pack.partial" "$rr_pack"
|
|
installed=$(tr -d '[:space:]' < "$rr_pack/version.txt")
|
|
fi
|
|
if rr_newer "$latest" "$installed"; then
|
|
deletions=$(rr_fetch "${rr_server}RetroRewindDelete.txt") ||
|
|
fail "could not read Retro Rewind's deletion list"
|
|
previous=$installed
|
|
while read -r version url; do
|
|
rr_newer "$version" "$previous" || continue
|
|
say "Applying Retro Rewind $version"
|
|
rr_unpack_update "$url" "$rr_pack" </dev/null
|
|
# Each update's deletions follow it, before the next update can put a file back.
|
|
# Only paths inside the pack are touched, and none that climb out of it.
|
|
while read -r dversion dpath; do
|
|
rr_version_ok "$dversion" || continue
|
|
rr_newer "$dversion" "$previous" || continue
|
|
rr_newer "$dversion" "$version" && continue
|
|
dpath=${dpath//\\//}
|
|
dpath=${dpath#/}
|
|
[[ "$dpath" == RetroRewind6/?* && "/$dpath/" != */../* ]] || continue
|
|
rm -rf "${rr_pack:?}/${dpath#RetroRewind6/}"
|
|
done <<<"$deletions"
|
|
previous=$version
|
|
done <<<"$updates"
|
|
# Written last, so an interrupted update runs again next time.
|
|
printf '%s\n' "$latest" > "$rr_pack/version.txt"
|
|
fi
|
|
note "Retro Rewind $(tr -d '[:space:]' < "$rr_pack/version.txt")"
|
|
fi
|
|
|
|
# translate-mod builds the payload into the game; without it, going online jumps into code that
|
|
# was never translated. Fetched on every build, as the Quest app does.
|
|
say "Downloading the Retro-WFC payload for online play"
|
|
mkdir -p "$(dirname "$rr_payload")"
|
|
if curl -fsS --retry 2 --max-filesize 16777216 -H 'Accept-Encoding: identity' \
|
|
-o "$rr_payload.partial" 'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
|
|
mv "$rr_payload.partial" "$rr_payload"
|
|
elif [[ -f "$rr_payload" ]]; then
|
|
rm -f "$rr_payload.partial"
|
|
note "rwfc.net did not answer; using the payload from the last build"
|
|
else
|
|
fail "Retro Rewind's online play needs the Retro-WFC payload from rwfc.net, which could not be
|
|
downloaded. Check this machine's internet connection, then run the script again."
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
say "Preparing the ARM64 build container"
|
|
cat > "$work_dir/container-build.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
# Written by steam-frame-install.sh and run inside its build container.
|
|
set -euo pipefail
|
|
if [[ ! -f /var/lib/wiicompiled-build-deps ]]; then
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq --no-install-recommends \
|
|
ca-certificates curl git python3 xz-utils unzip file pkg-config g++ binutils libicu-dev \
|
|
zlib1g-dev libvulkan-dev libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev libxrandr-dev \
|
|
libxinerama-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev libxkbcommon-dev \
|
|
libwayland-dev wayland-protocols libdecor-0-dev libegl-dev libgl-dev libgles-dev libdrm-dev \
|
|
libgbm-dev libasound2-dev libpulse-dev libpipewire-0.3-dev libudev-dev libdbus-1-dev \
|
|
libusb-1.0-0-dev >/dev/null
|
|
touch /var/lib/wiicompiled-build-deps
|
|
fi
|
|
cd /src
|
|
Launcher/prepare-portable-tools.sh --arch aarch64 --destination /work/tools
|
|
T=/work/tools/toolchain-aarch64/bin
|
|
if [[ ! -x /work/dotnet/dotnet ]]; then
|
|
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0 --install-dir /work/dotnet
|
|
fi
|
|
echo "== Dawn (built once; reused until a release changes its patches)"
|
|
Launcher/build-dawn-linux.sh --work-dir /work/dawn --cc "$T/clang" --cxx "$T/clang++" \
|
|
--cmake "$T/cmake" --ninja "$T/ninja" --jobs "$JOBS"
|
|
echo "== The game"
|
|
products=(--output-dir /work/out)
|
|
if [[ "$RETRO_REWIND" == 1 ]]; then
|
|
# Both products from one translation: the base game in out/, Retro Rewind in out-retro-rewind/.
|
|
products=(--profile both --output-dir /work/out-retro-rewind --base-output-dir /work/out
|
|
--retro-rewind-package-dir /work/RetroRewind6 --retro-wfc-offline-dir /work/retro-wfc)
|
|
fi
|
|
Launcher/local-build.sh "${products[@]}" --cc "$T/clang" --cxx "$T/clang++" --fuse-ld lld \
|
|
--cmake "$T/cmake" --ninja "$T/ninja" --dotnet /work/dotnet/dotnet --parallel "$JOBS" \
|
|
--openxr --dawn-package /work/dawn/package --headset steam_frame
|
|
EOF
|
|
chmod +x "$work_dir/container-build.sh"
|
|
|
|
# One long-lived container, so its packages are installed once; recreated if its folders changed.
|
|
want_mounts="$work_dir=/work;$source_dir=/src;"
|
|
if "$runtime" container inspect "$container" >/dev/null 2>&1; then
|
|
mounts=$("$runtime" container inspect -f '{{range .Mounts}}{{.Source}}={{.Destination}};{{end}}' "$container")
|
|
if [[ "$mounts" != *"$work_dir=/work;"* || "$mounts" != *"$source_dir=/src;"* ]]; then
|
|
note "its folders changed; recreating it"
|
|
"$runtime" rm -f "$container" >/dev/null
|
|
fi
|
|
fi
|
|
if ! "$runtime" container inspect "$container" >/dev/null 2>&1; then
|
|
"$runtime" run -d --name "$container" "${platform_args[@]}" \
|
|
-v "$work_dir:/work:z" -v "$source_dir:/src:z" "$image" sleep infinity >/dev/null
|
|
fi
|
|
"$runtime" start "$container" >/dev/null
|
|
note "container $container (mounts ${want_mounts//;/ })"
|
|
|
|
say "Building (the first time takes hours under emulation; a log is in $work_dir/build.log)"
|
|
if ! "$runtime" exec -e JOBS="$jobs" -e RETRO_REWIND="$retro_rewind" "$container" bash /work/container-build.sh 2>&1 | tee "$work_dir/build.log"; then
|
|
fail "the build stopped; the end of $work_dir/build.log says why. Run the script again to resume.
|
|
A machine that froze ran out of memory: pass a lower --jobs."
|
|
fi
|
|
[[ -x "$work_dir/out/$game_id" ]] || fail "the build finished without $work_dir/out/$game_id"
|
|
note "built $work_dir/out/$game_id"
|
|
if (( retro_rewind )); then
|
|
[[ -x "$work_dir/out-retro-rewind/RetroRewind" ]] ||
|
|
fail "the build finished without $work_dir/out-retro-rewind/RetroRewind"
|
|
note "built $work_dir/out-retro-rewind/RetroRewind"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
if [[ -z "$frame" ]]; then
|
|
say "Done"
|
|
note "The game is in $work_dir/out. Run the script again with --frame steamos@<frame-ip> to install it."
|
|
exit 0
|
|
fi
|
|
|
|
say "Installing on the Frame"
|
|
# One SSH connection for every step, so a password is asked for once.
|
|
ssh_opts=(-o ControlMaster=auto -o "ControlPath=${XDG_RUNTIME_DIR:-/tmp}/wiicompiled-ssh-%C" -o ControlPersist=600)
|
|
on_frame() {
|
|
# Runs a bash script, given on stdin, on the Frame; its arguments follow.
|
|
if [[ "$frame" == local ]]; then bash -s -- "$@"; else ssh "${ssh_opts[@]}" "$frame" bash -s -- "$@"; fi
|
|
}
|
|
copy_to_frame() {
|
|
# copy_to_frame SOURCE DEST: DEST is absolute or relative to the Frame's home. A DEST ending in
|
|
# / is an existing folder SOURCE goes into; otherwise SOURCE is copied to that new name.
|
|
local source=$1 dest=$2
|
|
if [[ "$frame" == local ]]; then
|
|
[[ "$dest" == /* ]] || dest="$HOME/$dest"
|
|
cp -R "$source" "$dest"
|
|
elif [[ "$frame_rsync" == yes ]]; then
|
|
# rsync copies a folder without a trailing slash into DEST rather than as DEST.
|
|
[[ -d "$source" && "$dest" != */ ]] && source="$source/"
|
|
rsync -a -e "ssh ${ssh_opts[*]}" "$source" "$frame:$dest"
|
|
else
|
|
scp -rq "${ssh_opts[@]}" "$source" "$frame:$dest"
|
|
fi
|
|
}
|
|
|
|
on_frame <<<'true' || fail "cannot reach $frame over SSH. Turn on Developer Mode and set a user password on the
|
|
Frame (Steam Settings > System), then check the address."
|
|
frame_rsync=no
|
|
if [[ "$frame" != local ]] && command -v rsync >/dev/null 2>&1 &&
|
|
[[ "$(on_frame <<<'command -v rsync >/dev/null && echo yes || true')" == yes ]]; then
|
|
frame_rsync=yes
|
|
fi
|
|
|
|
install_game() {
|
|
# install_game ID OUT: the built game in OUT goes to ~/devkit-game/ID on the Frame.
|
|
local id=$1 out=$2 dir="devkit-game/$1"
|
|
on_frame "$dir" <<'EOF'
|
|
mkdir -p "$HOME/$1"
|
|
EOF
|
|
note "copying $id to ~/$dir"
|
|
(cd "$out" && find . -mindepth 1 -maxdepth 1 ! -name "$id" -print0) |
|
|
while IFS= read -r -d '' item; do copy_to_frame "$out/${item#./}" "$dir/"; done
|
|
# The executable goes in under a new name and is moved into place: that works while it runs.
|
|
copy_to_frame "$out/$id" "$dir/$id.new"
|
|
on_frame "$dir" "$id" <<'EOF'
|
|
cd "$HOME/$1" && mv -f "$2.new" "$2" && chmod -R u=rwX,go=rX . && chmod 755 "$2"
|
|
EOF
|
|
}
|
|
install_game "$game_id" "$work_dir/out"
|
|
if (( retro_rewind )); then install_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
|
|
|
# The Frame's disc folder, absolute or relative to its home.
|
|
if [[ -z "$frame_disc" ]]; then
|
|
if [[ "$frame" == local && -n "$disc_dir" ]]; then frame_disc=$disc_dir; else frame_disc=wiicompiled/disc; fi
|
|
fi
|
|
frame_disc_path=${frame_disc#\~/}
|
|
has_disc=$(on_frame "$frame_disc_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
[[ -f "$d/sys/main.dol" ]] && echo yes || true
|
|
EOF
|
|
)
|
|
if [[ "$has_disc" != yes ]]; then
|
|
[[ -n "$disc_dir" ]] || fail "the Frame has no disc at $frame_disc yet: pass --disc"
|
|
note "copying the extracted disc to $frame_disc (a few GB)"
|
|
on_frame "$frame_disc_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
mkdir -p "$(dirname "$d")" && rm -rf "$d.partial"
|
|
EOF
|
|
copy_to_frame "$disc_dir" "$frame_disc_path.partial"
|
|
on_frame "$frame_disc_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
rm -rf "$d" && mv "$d.partial" "$d"
|
|
EOF
|
|
fi
|
|
|
|
if (( retro_rewind )); then
|
|
# The pack goes beside the disc; copied again only when its version changed.
|
|
frame_pack_path="$(dirname "$frame_disc_path")/RetroRewind6"
|
|
[[ "$frame_disc_path" == */* ]] || frame_pack_path=RetroRewind6
|
|
frame_pack_version=$(on_frame "$frame_pack_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
tr -d '[:space:]' 2>/dev/null < "$d/version.txt" || true
|
|
EOF
|
|
)
|
|
local_pack_version=$(tr -d '[:space:]' 2>/dev/null < "$rr_pack/version.txt" || true)
|
|
if [[ -z "$frame_pack_version" || "$frame_pack_version" != "$local_pack_version" ]]; then
|
|
note "copying the Retro Rewind pack to $frame_pack_path (about 4 GB)"
|
|
on_frame "$frame_pack_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
mkdir -p "$(dirname "$d")" && rm -rf "$d.partial"
|
|
EOF
|
|
copy_to_frame "$rr_pack" "$frame_pack_path.partial"
|
|
on_frame "$frame_pack_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
rm -rf "$d" && mv "$d.partial" "$d"
|
|
EOF
|
|
fi
|
|
note "pointing Retro Rewind at its pack"
|
|
on_frame "$frame_pack_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
config="$HOME/.local/share/WiiCompiled/Config.toml"
|
|
mkdir -p "$(dirname "$config")"
|
|
if [[ ! -f "$config" ]]; then
|
|
printf '[paths]\nretro_rewind_root = "%s"\n' "$d" > "$config"
|
|
elif ! grep -q '^retro_rewind_root *=' "$config"; then
|
|
if grep -q '^\[paths\]' "$config"; then
|
|
sed -i "/^\[paths\]/a retro_rewind_root = \"$d\"" "$config"
|
|
else
|
|
printf '\n[paths]\nretro_rewind_root = "%s"\n' "$d" >> "$config"
|
|
fi
|
|
fi
|
|
EOF
|
|
fi
|
|
|
|
note "pointing the game at the disc"
|
|
on_frame "$frame_disc_path" <<'EOF'
|
|
[[ "$1" = /* ]] && d=$1 || d="$HOME/$1"
|
|
config="$HOME/.local/share/WiiCompiled/Config.toml"
|
|
mkdir -p "$(dirname "$config")"
|
|
if [[ ! -f "$config" ]]; then
|
|
printf '[paths]\ndvd_root = "%s"\n' "$d" > "$config"
|
|
elif ! grep -q '^dvd_root *=' "$config"; then
|
|
if grep -q '^\[paths\]' "$config"; then
|
|
sed -i "/^\[paths\]/a dvd_root = \"$d\"" "$config"
|
|
else
|
|
printf '\n[paths]\ndvd_root = "%s"\n' "$d" >> "$config"
|
|
fi
|
|
fi
|
|
EOF
|
|
|
|
# Steam's library: through Valve's devkit tools when Frame Control or the Devkit Client put them on
|
|
# the Frame, as a Steam Linux Runtime ARM64 title (Steam starts an ARM64 program natively).
|
|
# Registering again is harmless and refreshes the entry, as Valve's Devkit Client does each upload.
|
|
register_game() {
|
|
# register_game ID OUT: adds ~/devkit-game/ID to Steam's library, or says how to.
|
|
local id=$1 out=$2 dir="devkit-game/$1" registered
|
|
registered=$(on_frame "$dir" "$id" <<'EOF'
|
|
if [[ ! -f "$HOME/devkit-utils/steam-client-create-shortcut" ]]; then echo no-tools; exit 0; fi
|
|
parms=$(printf '{"gameid": "%s", "directory": "%s", "argv": ["%s"], "env": {}, "settings": {"steam_play": "0", "compat_tool": "SteamLinuxRuntime_4-arm64"}, "clear_settings": true, "force_appid": "", "lepton_args": ""}' "$2" "$HOME/$1" "$2")
|
|
reply=$(python3 "$HOME/devkit-utils/steam-client-create-shortcut" --parms "$parms" 2>/dev/null | tail -n 1)
|
|
case "$reply" in *'"success"'*) echo added ;; *) echo "failed $reply" ;; esac
|
|
EOF
|
|
)
|
|
case "$registered" in
|
|
added) note "$id is in your Steam library" ;;
|
|
no-tools|failed*)
|
|
[[ "$registered" == failed* ]] && note "Steam did not take the shortcut: ${registered#failed }"
|
|
note "Add $id to your Steam library once: in Frame Control, Send to Frame the folder"
|
|
note " $out (name it $id), or on the Frame in Desktop Mode, Steam >"
|
|
note " Add a Non-Steam Game > ~/$dir/$id. Later runs of this script keep it updated." ;;
|
|
esac
|
|
}
|
|
register_game "$game_id" "$work_dir/out"
|
|
if (( retro_rewind )); then register_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
|
|
|
say "Done"
|
|
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
|
|
if (( retro_rewind )); then note "RetroRewind is beside it, and shares its settings."; fi
|
|
}
|
|
|
|
main "$@" </dev/null
|