Files
mitch030504--Wiicompiled_VR…/Launcher/steam-frame-install.sh
T
Claude 1a42dfc382 Install script: take a .zip or .7z holding the disc image
An archive is recognised by its first bytes, unpacked into the work dir
with whichever unpacker the machine has (7-Zip, bsdtar, unzip, Python's
zipfile, or bsdtar in a container of the machine's own architecture), and
every file in it is offered to nodtool, largest first. The RMCP01 image
wins; another region's disc is passed on so the ID check names it. The
unpacked files are removed once the disc is extracted, or when no disc is
found.

Tested each unpacker with zip and 7z archives holding an RVZ in a
subfolder, an archive holding NTSC and PAL images, an NTSC-only one, one
with no disc, a password-protected zip and truncated archives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
2026-10-05 07:13:56 +00:00

507 lines
23 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)
# -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=""
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 ;;
-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
# ---------------------------------------------------------------------------------------------
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"
Launcher/local-build.sh --output-dir /work/out --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" "$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 [[ -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
game_dir="devkit-game/$game_id"
on_frame "$game_dir" <<'EOF'
mkdir -p "$HOME/$1"
EOF
note "copying the game to ~/$game_dir"
(cd "$work_dir/out" && find . -mindepth 1 -maxdepth 1 ! -name "$game_id" -print0) |
while IFS= read -r -d '' item; do copy_to_frame "$work_dir/out/${item#./}" "$game_dir/"; done
# The executable goes in under a new name and is moved into place: that works while it runs.
copy_to_frame "$work_dir/out/$game_id" "$game_dir/$game_id.new"
on_frame "$game_dir" "$game_id" <<'EOF'
cd "$HOME/$1" && mv -f "$2.new" "$2" && chmod -R u=rwX,go=rX . && chmod 755 "$2"
EOF
# 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
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.
registered=$(on_frame "$game_dir" "$game_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 "it is in your Steam library" ;;
no-tools|failed*)
[[ "$registered" == failed* ]] && note "Steam did not take the shortcut: ${registered#failed }"
note "Add it to your Steam library once: in Frame Control, Send to Frame the folder"
note " $work_dir/out (name it $game_id), or on the Frame in Desktop Mode, Steam >"
note " Add a Non-Steam Game > ~/$game_dir/$game_id. Later runs of this script keep it updated." ;;
esac
say "Done"
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
}
main "$@" </dev/null