mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Add --update and --check to the install script, and an Updates tab in the game
--update reuses the options an install was made with (saved to install.conf in the work dir) and builds nothing when the Frame already has the newest release and Retro Rewind pack; --check only reports. The installed release is stamped as .release-tag beside the game on the Frame. An install built on the Frame itself (--frame local) also gets a systemd user service the game's new Updates tab starts: the update runs in the background at low priority, writes each step and the build percentage to a status file the tab shows, and reopens the game at the end if it was closed. Progress does not go through Steam notifications: on the Frame, Steam receives them from steam_notif_daemon but does not display them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyuWx5hNrMpf9R78FZFcbA
This commit is contained in:
6 files changed
+851
-30
No files matched your search
+271
-25
@@ -1,8 +1,10 @@
|
||||
#!/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.
|
||||
# Frame. Run it again with --update to update: it fetches the newest release and rebuilds only what
|
||||
# changed, with the options it was installed with.
|
||||
#
|
||||
# Launcher/steam-frame-install.sh --disc PATH [--frame HOST] [options]
|
||||
# Launcher/steam-frame-install.sh --update
|
||||
#
|
||||
# Or without downloading anything first:
|
||||
#
|
||||
@@ -36,32 +38,84 @@
|
||||
# --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
|
||||
# --update update an earlier install: its options are reused (any given here win), and
|
||||
# nothing is built when the Frame already has the newest release and Retro
|
||||
# Rewind pack. Follows releases unless it was installed with --source.
|
||||
# --check only say whether an update is available; nothing is changed
|
||||
# -h, --help
|
||||
#
|
||||
# Installed with --frame local, the game can update itself: Settings > Updates in the game checks
|
||||
# for a new release, and its Update button runs this script with --update in the background,
|
||||
# through a systemd user service this script sets up. The tab shows the update's progress while the
|
||||
# game stays open, and a game closed meanwhile is opened again when the update is done.
|
||||
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
|
||||
# What the error path below reads, set before anything can fail into it.
|
||||
mode=install
|
||||
steam_game_id=""
|
||||
trap 'on_error "$LINENO"' 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
|
||||
# Where the game keeps its settings, and where an update it starts reports back to it. Resolved as
|
||||
# the game resolves it (RuntimeConfigFile::ApplicationDataDirectory).
|
||||
game_data="${XDG_DATA_HOME:-$HOME/.local/share}/$game_id"
|
||||
|
||||
say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
||||
say() {
|
||||
printf '\n\033[1m==> %s\033[0m\n' "$*"
|
||||
status running "$*"
|
||||
}
|
||||
note() { printf ' %s\n' "$*"; }
|
||||
fail() {
|
||||
printf '\nsteam-frame-install.sh: error: %s\n' "$*" >&2
|
||||
finish_failed "${1%%$'\n'*}"
|
||||
}
|
||||
on_error() {
|
||||
printf '\nsteam-frame-install.sh: stopped by a failed step (line %s); the output above says why.\n' "$1" >&2
|
||||
finish_failed "a step failed (line $1)"
|
||||
}
|
||||
finish_failed() {
|
||||
trap - ERR
|
||||
status failed "$1"
|
||||
relaunch
|
||||
exit 1
|
||||
}
|
||||
usage() { awk 'NR == 1 { next } /^#/ { sub(/^# ?/, ""); print; next } { exit }' "$script_path"; }
|
||||
|
||||
# The game's Updates tab reads this: "<state>\t<unix time>\t<text>", state running, available,
|
||||
# uptodate, done or failed. Written only when the game asked for this run
|
||||
# (WIICOMPILED_UPDATE_STATUS names the file).
|
||||
status() {
|
||||
local file=${WIICOMPILED_UPDATE_STATUS:-}
|
||||
[[ -n "$file" ]] || return 0
|
||||
{ printf '%s\t%s\t%s\n' "$1" "$(date +%s)" "$2" > "$file.new" && mv -f "$file.new" "$file"; } 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Progress goes to that file rather than to Steam notifications: on the Frame, SteamOS's
|
||||
# steam_notif_daemon accepts a notify-send and Steam then shows nothing, even when given the
|
||||
# notification directly with steam -ifrunning (tried on a Frame, 2026-10).
|
||||
# An update the game started ends by opening the game again if it was closed meanwhile, which is
|
||||
# how its result gets seen; a game still open shows the result in its Updates tab.
|
||||
game_running() { pgrep -f "devkit-game/($game_id|RetroRewind)/" >/dev/null 2>&1; }
|
||||
relaunch() {
|
||||
[[ -n "$steam_game_id" ]] || return 0
|
||||
command -v steam >/dev/null 2>&1 || return 0
|
||||
if game_running; then return 0; fi
|
||||
note "opening $game_id again"
|
||||
timeout 60 steam "steam://rungameid/$steam_game_id" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
script_path=${BASH_SOURCE[0]:-}
|
||||
script_dir=""
|
||||
if [[ -n "$script_path" && -f "$script_path" ]]; then
|
||||
script_dir=$(cd "$(dirname "$script_path")" && pwd)
|
||||
script_path="$script_dir/$(basename "$script_path")"
|
||||
fi
|
||||
|
||||
disc=""
|
||||
@@ -85,15 +139,151 @@ while [[ $# -gt 0 ]]; do
|
||||
--frame-disc) frame_disc=$2; shift 2 ;;
|
||||
--retro-rewind) retro_rewind=1; shift ;;
|
||||
--retro-rewind-pack) retro_rewind=1; retro_pack=$2; shift 2 ;;
|
||||
--update) mode=update; shift ;;
|
||||
--check) mode=check; shift ;;
|
||||
-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
|
||||
# What --update reuses: the options given, before defaults fill the rest in.
|
||||
jobs_given=$jobs
|
||||
source_given=$source_dir
|
||||
|
||||
mkdir -p "$work_dir"
|
||||
work_dir=$(cd "$work_dir" && pwd)
|
||||
|
||||
# The game's Update button leaves a request file; the service that runs this script moves it here.
|
||||
if [[ -n "${WIICOMPILED_UPDATE_REQUEST:-}" && -f "$WIICOMPILED_UPDATE_REQUEST" ]]; then
|
||||
steam_game_id=$(sed -n 's/^steam_game_id=\([0-9]*\)$/\1/p' "$WIICOMPILED_UPDATE_REQUEST" | head -n 1)
|
||||
rm -f "$WIICOMPILED_UPDATE_REQUEST"
|
||||
fi
|
||||
|
||||
if [[ "$mode" != install && -f "$work_dir/install.conf" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
frame) [[ -n "$frame" ]] || frame=$value ;;
|
||||
frame_disc) [[ -n "$frame_disc" ]] || frame_disc=$value ;;
|
||||
jobs) [[ -n "$jobs" ]] || jobs=$value ;;
|
||||
source) [[ -n "$source_dir" ]] || source_dir=$value ;;
|
||||
retro_rewind) if [[ "$value" == 1 ]]; then retro_rewind=1; fi ;;
|
||||
retro_rewind_pack) [[ -n "$retro_pack" ]] || retro_pack=$value ;;
|
||||
esac
|
||||
done < "$work_dir/install.conf"
|
||||
jobs_given=$jobs
|
||||
source_given=$source_dir
|
||||
fi
|
||||
save_settings() {
|
||||
cat > "$work_dir/install.conf" <<EOF
|
||||
# Written by steam-frame-install.sh after each install: the options --update reuses.
|
||||
frame=$frame
|
||||
frame_disc=$frame_disc
|
||||
jobs=$jobs_given
|
||||
source=$source_given
|
||||
retro_rewind=$retro_rewind
|
||||
retro_rewind_pack=$retro_pack
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Talking to the Frame, and the versions involved. Up here because --update and --check read them
|
||||
# before anything is built.
|
||||
# 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
|
||||
}
|
||||
read_on_frame() {
|
||||
# Prints the contents of $1 on the Frame, with its whitespace removed, or nothing.
|
||||
on_frame "$1" <<'EOF'
|
||||
[[ "$1" = /* ]] && f=$1 || f="$HOME/$1"
|
||||
tr -d '[:space:]' 2>/dev/null < "$f" || true
|
||||
EOF
|
||||
}
|
||||
frame_game_dir="devkit-game/$game_id"
|
||||
|
||||
newest_release() {
|
||||
curl -fsSL "https://api.github.com/repos/$repo/releases?per_page=1" |
|
||||
sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1
|
||||
}
|
||||
|
||||
# Retro Rewind's own versioning, as its update server publishes it.
|
||||
rr_server=https://update.rwfc.net/RetroRewind/
|
||||
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_published_version() {
|
||||
rr_fetch "${rr_server}RetroRewindVersion.txt" |
|
||||
awk 'NF >= 4 && $1 ~ /^[0-9]+(\.[0-9]+)+$/ { print $1 }' | sort -V | tail -n 1
|
||||
}
|
||||
# Where the pack sits on the Frame: beside the disc, as the install below puts it.
|
||||
frame_pack_path_for() {
|
||||
local disc=${1#\~/}
|
||||
[[ -n "$disc" ]] || disc=wiicompiled/disc
|
||||
if [[ "$disc" == */* ]]; then printf '%s/RetroRewind6\n' "$(dirname "$disc")"; else printf 'RetroRewind6\n'; fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# --update and --check: what the Frame has against what is published. Nothing is built when the
|
||||
# Frame is already current, so an update that has nothing to do costs one web request.
|
||||
if [[ "$mode" != install ]]; then
|
||||
[[ -n "$frame" ]] || fail "--$mode updates an install made by this script, and there is none in
|
||||
$work_dir. Install first (see --help), or pass --work-dir and --frame."
|
||||
say "Checking for an update"
|
||||
reasons=()
|
||||
if [[ -n "$source_dir" ]]; then
|
||||
# A source install has no published version to compare against; it rebuilds what changed.
|
||||
reasons+=("it was installed from the source tree in $source_dir, which is rebuilt as it is")
|
||||
else
|
||||
if [[ -z "$release" ]]; then
|
||||
release=$(newest_release) ||
|
||||
fail "could not reach github.com to ask for the newest release. Check this machine's
|
||||
internet connection, then try again."
|
||||
[[ -n "$release" ]] || fail "could not find the newest release on github.com/$repo"
|
||||
fi
|
||||
frame_installed_release=$(read_on_frame "$frame_game_dir/.release-tag") ||
|
||||
fail "cannot reach $frame over SSH to ask which release it has."
|
||||
if [[ "$frame_installed_release" == "$release" ]]; then
|
||||
note "the Frame has release $release"
|
||||
elif [[ -z "$frame_installed_release" ]]; then
|
||||
reasons+=("the Frame does not say which release it has; release $release would be installed")
|
||||
else
|
||||
reasons+=("release $release (the Frame has $frame_installed_release)")
|
||||
fi
|
||||
fi
|
||||
if (( retro_rewind )) && [[ -z "$retro_pack" ]]; then
|
||||
rr_published=$(rr_published_version) ||
|
||||
fail "could not reach Retro Rewind's update server (${rr_server})"
|
||||
rr_installed=$(read_on_frame "$(frame_pack_path_for "$frame_disc")/version.txt")
|
||||
if ! rr_version_ok "$rr_installed"; then
|
||||
reasons+=("the Frame does not say which Retro Rewind pack it has")
|
||||
elif rr_newer "$rr_published" "$rr_installed"; then
|
||||
reasons+=("Retro Rewind $rr_published (the Frame has $rr_installed)")
|
||||
else
|
||||
note "the Frame has Retro Rewind $rr_installed"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( ${#reasons[@]} == 0 )); then
|
||||
say "Up to date"
|
||||
status uptodate "Up to date${release:+ (}${release}${release:+)}"
|
||||
relaunch
|
||||
exit 0
|
||||
fi
|
||||
note "an update is available:"
|
||||
for line in "${reasons[@]}"; do note " $line"; done
|
||||
if [[ "$mode" == check ]]; then
|
||||
status available "${reasons[0]}"
|
||||
exit 0
|
||||
fi
|
||||
status running "Starting the update${release:+ to }$release"
|
||||
fi
|
||||
|
||||
host_arch=$(uname -m)
|
||||
case "$host_arch" in
|
||||
x86_64|aarch64) ;;
|
||||
@@ -344,16 +534,10 @@ 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/
|
||||
# Its server, versions and comparisons are up with the other version helpers.
|
||||
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.
|
||||
@@ -499,8 +683,31 @@ fi
|
||||
"$runtime" start "$container" >/dev/null
|
||||
note "container $container (mounts ${want_mounts//;/ })"
|
||||
|
||||
build_progress() {
|
||||
# Passes the build's output on, and keeps the game's status file at the stage it is on and how
|
||||
# far through it ninja is, from the "[done/total]" lines it prints.
|
||||
local line stage=Building percent last=-1
|
||||
while IFS= read -r line; do
|
||||
printf '%s\n' "$line"
|
||||
[[ -n "${WIICOMPILED_UPDATE_STATUS:-}" ]] || continue
|
||||
if [[ "$line" == "== "* ]]; then
|
||||
stage=${line#== }
|
||||
stage=${stage%% (*}
|
||||
last=-1
|
||||
status running "$stage"
|
||||
elif [[ "$line" =~ ^\[([0-9]+)/([0-9]+)\] ]] && (( BASH_REMATCH[2] > 0 )); then
|
||||
percent=$(( BASH_REMATCH[1] * 100 / BASH_REMATCH[2] ))
|
||||
if (( percent != last )); then
|
||||
last=$percent
|
||||
status running "$stage, $percent% built"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
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
|
||||
if ! "$runtime" exec -e JOBS="$jobs" -e RETRO_REWIND="$retro_rewind" "$container" bash /work/container-build.sh 2>&1 |
|
||||
tee "$work_dir/build.log" | build_progress; 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
|
||||
@@ -520,12 +727,7 @@ if [[ -z "$frame" ]]; then
|
||||
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
|
||||
}
|
||||
# on_frame and the SSH options it uses are up with the version helpers.
|
||||
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.
|
||||
@@ -563,6 +765,11 @@ EOF
|
||||
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
|
||||
# Which release is on the Frame, for --check and the game's Updates tab. A source build has no
|
||||
# release to name, and the stamp is removed so neither claims a release this is not.
|
||||
on_frame "$dir" "$release" <<'EOF'
|
||||
cd "$HOME/$1" && if [[ -n "$2" ]]; then printf '%s\n' "$2" > .release-tag; else rm -f .release-tag; fi
|
||||
EOF
|
||||
}
|
||||
install_game "$game_id" "$work_dir/out"
|
||||
@@ -594,13 +801,8 @@ 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
|
||||
)
|
||||
frame_pack_path=$(frame_pack_path_for "$frame_disc_path")
|
||||
frame_pack_version=$(read_on_frame "$frame_pack_path/version.txt")
|
||||
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)"
|
||||
@@ -672,9 +874,53 @@ esac
|
||||
register_game "$game_id" "$work_dir/out"
|
||||
if (( retro_rewind )); then register_game RetroRewind "$work_dir/out-retro-rewind"; fi
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Updating from inside the game. Only an install on the Frame itself can do it, since that is the
|
||||
# only one with the build here; installed from a PC, the game's Updates tab says to update there.
|
||||
if [[ "$frame" == local ]]; then
|
||||
note "setting up the game's Update button"
|
||||
update_script="$work_dir/steam-frame-install.sh"
|
||||
# A copy, so an update never runs from the source tree it is about to overwrite.
|
||||
cp -f "$source_dir/Launcher/steam-frame-install.sh" "$update_script"
|
||||
chmod +x "$update_script"
|
||||
mkdir -p "$game_data" "$HOME/.config/systemd/user"
|
||||
cat > "$game_data/update.conf" <<EOF
|
||||
# Written by steam-frame-install.sh: what the game's Updates tab runs. Remove this file to take the
|
||||
# Update button away, and the tab goes back to saying how to update from a PC.
|
||||
script=$update_script
|
||||
work_dir=$work_dir
|
||||
service=wiicompiled-update.service
|
||||
EOF
|
||||
# Its own service, so the update outlives the game that started it: Steam stops the game's own
|
||||
# process group when the game closes.
|
||||
cat > "$HOME/.config/systemd/user/wiicompiled-update.service" <<EOF
|
||||
[Unit]
|
||||
Description=WiiCompiled VR update
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=WIICOMPILED_UPDATE_STATUS=$game_data/update-status
|
||||
Environment=WIICOMPILED_UPDATE_REQUEST=$game_data/update-request
|
||||
# Below the game in line for the processor and the storage, so it can keep running meanwhile.
|
||||
Nice=19
|
||||
IOSchedulingClass=idle
|
||||
ExecStart=$update_script --update --work-dir "$work_dir"
|
||||
EOF
|
||||
systemctl --user daemon-reload >/dev/null 2>&1 ||
|
||||
note "(systemd did not reload, so the Update button will not work until the Frame restarts)"
|
||||
fi
|
||||
|
||||
save_settings
|
||||
|
||||
say "Done"
|
||||
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
|
||||
status done "Updated${release:+ to }${release}"
|
||||
if [[ "$mode" == update ]]; then
|
||||
note "Updated${release:+ to }$release."
|
||||
else
|
||||
note "Start $game_id from your library in the headset. Settings: left shoulder button, VR tab."
|
||||
fi
|
||||
if (( retro_rewind )); then note "RetroRewind is beside it, and shares its settings."; fi
|
||||
relaunch
|
||||
}
|
||||
|
||||
main "$@" </dev/null
|
||||
@@ -106,10 +106,33 @@ shoulder button, go to the **VR** tab and set the [recommended settings](#recomm
|
||||
|
||||
### Updating
|
||||
|
||||
Run the same command again; you can leave out `--disc`. The script downloads the newest release,
|
||||
puts only the files that changed over the old source, rebuilds what they touch, and replaces the
|
||||
game on the Frame. Dawn is only rebuilt when a release changes its patches (the release notes say
|
||||
so), so an update usually takes minutes. `--release <tag>` builds a given release instead.
|
||||
```bash
|
||||
Launcher/steam-frame-install.sh --update
|
||||
```
|
||||
|
||||
`--update` reuses the options the install was made with, so there is nothing to repeat: it reads
|
||||
them from `install.conf` in the work dir, and any option you do pass wins over the saved one. It
|
||||
downloads the newest release, puts only the files that changed over the old source, rebuilds what
|
||||
they touch, and replaces the game on the Frame. Dawn is only rebuilt when a release changes its
|
||||
patches (the release notes say so), so an update usually takes minutes. When the Frame already has
|
||||
the newest release, and the newest Retro Rewind pack if you use one, it says so and builds nothing.
|
||||
|
||||
`--check` only reports whether an update is available. `--release <tag>` builds a given release
|
||||
instead. Running the whole install command again works as it always did.
|
||||
|
||||
### Updating from inside the game
|
||||
|
||||
An install built on the Frame itself (`--frame local`, [below](#other-ways-to-build)) can update
|
||||
itself: the settings panel grows an **Updates** tab with the release you are running and a **Check
|
||||
for updates** button, and when there is one, **Update now** builds it in the background. The tab
|
||||
shows each step and how far the build is while you keep playing (the game may stutter while it
|
||||
compiles), and says when to restart the game to use the update.
|
||||
|
||||
The update runs as a systemd user service (`wiicompiled-update.service`), behind the game in line
|
||||
for the processor, so it carries on if you close the game, and then opens the game again when it is
|
||||
done. Keep the Frame on its charger for it. Installed from a PC, the tab says so and the update is
|
||||
run there. Progress is not shown as Steam notifications: on the Frame, Steam receives them from
|
||||
SteamOS's notification service but does not display them.
|
||||
|
||||
## Recommended settings
|
||||
|
||||
@@ -314,7 +337,8 @@ curl -fsSL https://raw.githubusercontent.com/mitch030504/Wiicompiled_VR_Frame/op
|
||||
```
|
||||
|
||||
It is native ARM64, so no emulation, but the Frame has less memory and cooling than a PC; keep it
|
||||
on its charger. The game then reads the disc straight from `~/wiicompiled-frame/disc`. By hand, the
|
||||
on its charger. The game then reads the disc straight from `~/wiicompiled-frame/disc`, and this is
|
||||
the install that can [update itself from inside the game](#updating-from-inside-the-game). By hand, the
|
||||
steps under [Building by hand](#building-by-hand) work the same in a container started without
|
||||
`--platform linux/arm64`, with `nodtool-linux-aarch64` in place of `nodtool-linux-x86_64`.
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@ and nothing built from it may be distributed, so there is no ready-built game he
|
||||
- **Adaptive resolution** (`[vr] adaptive_resolution`, off by default): lowers the race's eye
|
||||
resolution down to 70% while frames fall behind, and raises it again once they keep up.
|
||||
Experimental and not yet tried in a race on the Frame. From Wiicompiled_VR-PLUS.
|
||||
- **Updating, from the PC or from inside the game.** `steam-frame-install.sh --update` reuses the
|
||||
options the install was made with and builds nothing when the Frame already has the newest
|
||||
release (and Retro Rewind pack); `--check` only reports. An install built on the Frame itself
|
||||
(`--frame local`) gets an **Updates** tab in the settings panel: it checks for a new release and
|
||||
builds it in the background while the game stays open, showing each step and how far the build
|
||||
is. Close the game meanwhile and the update reopens it when done. Steam notifications were tried
|
||||
for this and do not show on the Frame.
|
||||
- **`Launcher/frame-diagnostics.sh`** gathers the logs, crash files, settings, SteamVR logs and
|
||||
system state from the Frame into one archive, with a summary of how far startup got. Attach it
|
||||
to bug reports.
|
||||
@@ -51,6 +58,10 @@ Run the install command again; you can leave out `--disc`. Dawn's patches did no
|
||||
not rebuilt. Most of the game is, since the translator changed as well as the runtime and renderer,
|
||||
so expect a longer update than usual. The build now also downloads Mbed TLS. Add `--retro-rewind` to get Retro Rewind too.
|
||||
|
||||
frame-beta-2's script has no `--update`, so this one update is the full install command; it saves
|
||||
its options, and from then on `steam-frame-install.sh --update` is enough. To update from inside
|
||||
the game later, make this install on the Frame itself with `--frame local`.
|
||||
|
||||
## Known issues
|
||||
|
||||
Unchanged from frame-beta-2:
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Settings > Updates: whether a newer release of the game exists, and, on a Frame that holds its own
|
||||
// build, updating to it without leaving the headset.
|
||||
//
|
||||
// Both answers come from Launcher/steam-frame-install.sh, which already knows how to compare
|
||||
// versions and how to build: `--check` reports, `--update` installs. The update reports its progress
|
||||
// through a status file rather than Steam notifications, which the Frame's Steam does not show.
|
||||
// The install writes update.conf beside the player's configuration when it put a build on this
|
||||
// machine, so the Update button appears only where it can actually work; installed from a PC, the
|
||||
// tab says to update there.
|
||||
namespace update_check {
|
||||
|
||||
enum class State {
|
||||
// This install cannot update itself: it was built on a PC, which is where updates are run.
|
||||
PcInstall,
|
||||
// It can, and nothing has been asked of it yet this session.
|
||||
Idle,
|
||||
Checking,
|
||||
UpToDate,
|
||||
Available,
|
||||
// An update is running beside the game, as a service of its own: it carries on if the game is
|
||||
// closed, and then opens it again when it is done.
|
||||
Updating,
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Status {
|
||||
State state = State::PcInstall;
|
||||
// The release this build came from, or empty when it was built from a source tree.
|
||||
std::string installedRelease;
|
||||
// One line under the state: which release is available, what failed, or what step is running.
|
||||
std::string detail;
|
||||
// An update has installed a different build since this one started: it takes effect when the
|
||||
// game is started again.
|
||||
bool restartNeeded = false;
|
||||
};
|
||||
|
||||
// What the Updates tab draws. Cheap enough to call every frame: a check or update in flight is
|
||||
// answered from memory, and otherwise the files behind it are re-read at most once a second.
|
||||
Status Current();
|
||||
|
||||
// Whether there is an Updates tab to show at all: this install can update itself, or it at least
|
||||
// knows which release it is running. Neither is true of a build that came from somewhere else.
|
||||
inline bool Relevant(const Status& status) {
|
||||
return status.state != State::PcInstall || !status.installedRelease.empty();
|
||||
}
|
||||
|
||||
// Asks the install script whether a newer release exists, in the background. Returns at once.
|
||||
void StartCheck();
|
||||
|
||||
// Starts the update in the background: its own systemd user service runs the install script, which
|
||||
// reports each step to the file Current() reads, so the game can stay open and show the progress.
|
||||
// On false, `error` says why, for display.
|
||||
bool StartUpdate(std::string& error);
|
||||
|
||||
} // namespace update_check
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "update_check.h"
|
||||
#include "vr/camera_toggle.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
@@ -2094,6 +2095,107 @@ void DrawDiagnosticsSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_updatePromptOpen = false;
|
||||
std::string g_updateError;
|
||||
|
||||
void DrawUpdatePrompt() {
|
||||
constexpr const char* kTitle = "Update";
|
||||
if (g_updatePromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
|
||||
if (!ImGui::BeginPopupModal(kTitle, &g_updatePromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
ImGui::TextUnformatted("The update is built here on the Frame, in the background, which takes a "
|
||||
"while: keep the Frame on its charger. You can keep playing meanwhile, "
|
||||
"though the game may stutter while it compiles. Close the game whenever "
|
||||
"you like: the update carries on and opens it again when it is done.");
|
||||
ImGui::PopTextWrapPos();
|
||||
if (ImGui::Button("Start the update", ImVec2(Scaled(180.0f), 0.0f))) {
|
||||
update_check::StartUpdate(g_updateError);
|
||||
g_updatePromptOpen = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(Scaled(120.0f), 0.0f))) g_updatePromptOpen = false;
|
||||
if (!g_updatePromptOpen) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void DrawUpdateSettings() {
|
||||
const update_check::Status status = update_check::Current();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
if (status.installedRelease.empty()) {
|
||||
ImGui::TextUnformatted("This build came from a source tree rather than a release.");
|
||||
} else {
|
||||
ImGui::Text("Release %s", status.installedRelease.c_str());
|
||||
}
|
||||
|
||||
const bool busy = status.state == update_check::State::Checking ||
|
||||
status.state == update_check::State::Updating;
|
||||
switch (status.state) {
|
||||
case update_check::State::PcInstall:
|
||||
ImGui::TextDisabled("The PC that built this installs its updates. Run the installer there "
|
||||
"again with --update.");
|
||||
break;
|
||||
case update_check::State::Idle:
|
||||
break;
|
||||
case update_check::State::Checking:
|
||||
ImGui::TextDisabled("Looking for a newer release...");
|
||||
break;
|
||||
case update_check::State::UpToDate:
|
||||
ImGui::TextDisabled("%s", status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Available:
|
||||
ImGui::TextUnformatted(status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Updating:
|
||||
ImGui::Text("Updating: %s", status.detail.c_str());
|
||||
ImGui::TextDisabled("Keep playing, or close the game: the update carries on and opens it "
|
||||
"again when it is done.");
|
||||
break;
|
||||
case update_check::State::Failed:
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", status.detail.c_str());
|
||||
break;
|
||||
}
|
||||
if (!g_updateError.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", g_updateError.c_str());
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
// The update replaced the build on disk; this process is still the old one.
|
||||
ImGui::TextUnformatted("The update is installed. Quit and start the game again to use it.");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
if (status.state == update_check::State::PcInstall) {
|
||||
return;
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
if (ImGui::Button("Quit")) g_exitPromptOpen = true;
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::BeginDisabled(busy);
|
||||
if (ImGui::Button(status.state == update_check::State::Idle ? "Check for updates" : "Check again")) {
|
||||
g_updateError.clear();
|
||||
update_check::StartCheck();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Asks github.com for the newest release, and Retro Rewind's server for\n"
|
||||
"its newest pack when this install has one. Nothing is downloaded\n"
|
||||
"until you choose to update.");
|
||||
}
|
||||
if (status.state == update_check::State::Available) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Update now")) {
|
||||
g_updateError.clear();
|
||||
g_updatePromptOpen = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Builds the update here on the Frame, in the background. The game\n"
|
||||
"can stay open and shows the progress here.");
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
// The prompt itself is drawn at the top level, as the exit prompt is: the tab lives both in the
|
||||
// headset's panel and in a desktop menu, and a modal must outlive whichever opened it.
|
||||
}
|
||||
|
||||
void DrawFpsOverlay() {
|
||||
AuroraPresentTiming presentTiming{};
|
||||
aurora_get_present_timing(&presentTiming);
|
||||
@@ -2291,6 +2393,11 @@ void DrawTopBar() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (update_check::Relevant(update_check::Current()) && ImGui::BeginMenu("Updates")) {
|
||||
DrawUpdateSettings();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + style.FramePadding.x * 2.0f;
|
||||
const float exitWidth = ImGui::CalcTextSize("X").x + style.FramePadding.x * 2.0f;
|
||||
@@ -2479,6 +2586,9 @@ void DrawVrSettingsPanelWindow() {
|
||||
tab("Controllers", DrawControllerSettings);
|
||||
tab("Audio", DrawAudioSettings);
|
||||
tab("Diagnostics", DrawDiagnosticsSettings);
|
||||
if (update_check::Relevant(update_check::Current())) {
|
||||
tab("Updates", DrawUpdateSettings);
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
@@ -2687,6 +2797,7 @@ void Draw() noexcept {
|
||||
DrawFpsOverlay();
|
||||
DrawTopBar();
|
||||
DrawExitPrompt();
|
||||
DrawUpdatePrompt();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game.
|
||||
const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "update_check.h"
|
||||
|
||||
#include "runtime_config.h"
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
// Updating itself is the Frame's build, installed by Launcher/steam-frame-install.sh: a desktop
|
||||
// Linux game that holds its own build directory. Everywhere else the tab only reports the release
|
||||
// it is running, which needs none of the machinery below.
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 1
|
||||
#include <sys/wait.h>
|
||||
#else
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 0
|
||||
#endif
|
||||
|
||||
namespace update_check {
|
||||
namespace {
|
||||
|
||||
// Written by Launcher/steam-frame-install.sh when it built on this machine, naming itself and the
|
||||
// service that runs it. Its absence is what tells the tab this install is updated from a PC.
|
||||
constexpr std::string_view kConfigFileName = "update.conf";
|
||||
// An update's progress, and a check's answer. Separate files, so a check never overwrites the
|
||||
// result of the update that opened the game.
|
||||
constexpr std::string_view kStatusFileName = "update-status";
|
||||
constexpr std::string_view kCheckStatusFileName = "update-status-check";
|
||||
// What the Update button leaves for the update to pick up.
|
||||
constexpr std::string_view kRequestFileName = "update-request";
|
||||
// Stamped beside the executable by the install; absent for a build from a source tree.
|
||||
constexpr std::string_view kReleaseTagFileName = ".release-tag";
|
||||
|
||||
// A finished update's status belongs on screen when the game opens right afterwards, which is what
|
||||
// the update does. Older than this it is history, and the tab starts clean.
|
||||
constexpr auto kStatusLifetime = std::chrono::hours(1);
|
||||
// A running update rewrites its status at every step and every percent of its build, so one that
|
||||
// has said nothing for this long was stopped without a chance to say so (the Frame lost power).
|
||||
constexpr auto kRunningStatusLifetime = std::chrono::hours(2);
|
||||
// The tab is drawn every frame; the files behind it are not read anywhere near that often.
|
||||
constexpr auto kReadInterval = std::chrono::seconds(1);
|
||||
|
||||
std::string Trimmed(std::string_view text) {
|
||||
const auto first = text.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
return std::string(text.substr(first, text.find_last_not_of(" \t\r\n") - first + 1));
|
||||
}
|
||||
|
||||
std::string ReadFirstLine(const std::filesystem::path& file) {
|
||||
std::ifstream stream(file);
|
||||
std::string line;
|
||||
if (!stream || !std::getline(stream, line)) {
|
||||
return {};
|
||||
}
|
||||
return Trimmed(line);
|
||||
}
|
||||
|
||||
struct Config {
|
||||
std::filesystem::path script;
|
||||
// The install's work directory, which holds the build the update reuses. The script defaults to
|
||||
// the same place, but an install given --work-dir does not live there.
|
||||
std::filesystem::path workDir;
|
||||
std::string service;
|
||||
|
||||
bool Usable() const { return !script.empty() && !service.empty(); }
|
||||
};
|
||||
|
||||
Config ReadConfig() {
|
||||
Config config;
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
return config;
|
||||
#else
|
||||
std::ifstream stream(RuntimeConfigFile::ApplicationDataDirectory() / kConfigFileName);
|
||||
if (!stream) {
|
||||
return config;
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
const auto separator = line.find('=');
|
||||
if (line.empty() || line.front() == '#' || separator == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string key = Trimmed(std::string_view(line).substr(0, separator));
|
||||
const std::string value = Trimmed(std::string_view(line).substr(separator + 1));
|
||||
if (key == "script") {
|
||||
config.script = value;
|
||||
} else if (key == "work_dir") {
|
||||
config.workDir = value;
|
||||
} else if (key == "service") {
|
||||
config.service = value;
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
if (!config.script.empty() && !std::filesystem::is_regular_file(config.script, ec)) {
|
||||
// The work directory was removed, so there is nothing here to update from any more.
|
||||
config.script.clear();
|
||||
}
|
||||
return config;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string InstalledRelease() {
|
||||
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
|
||||
return ReadFirstLine(*executableDirectory / kReleaseTagFileName);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// What this session has been told, which outranks the files until the game is next started: a check
|
||||
// or update it asked for, and what came back.
|
||||
std::mutex g_mutex;
|
||||
bool g_sessionStateSet = false;
|
||||
State g_sessionState = State::Idle;
|
||||
std::string g_sessionDetail;
|
||||
// What was last read from disk, and when, so drawing the tab does not re-read it every frame.
|
||||
std::chrono::steady_clock::time_point g_lastRead;
|
||||
bool g_haveRead = false;
|
||||
bool g_haveFileState = false;
|
||||
State g_fileState = State::Idle;
|
||||
std::string g_fileDetail;
|
||||
bool g_canUpdate = false;
|
||||
// The release this process was started from, read once: an update replaces the stamp on disk while
|
||||
// the old build is still the one running. The stamp as it is now tells whether that has happened.
|
||||
std::string g_runningRelease;
|
||||
bool g_haveRunningRelease = false;
|
||||
std::string g_releaseOnDisk;
|
||||
|
||||
void SetSessionState(State state, std::string detail) {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_sessionStateSet = true;
|
||||
g_sessionState = state;
|
||||
g_sessionDetail = std::move(detail);
|
||||
}
|
||||
|
||||
std::string ShellQuoted(const std::string& text) {
|
||||
std::string quoted = "'";
|
||||
for (const char character : text) {
|
||||
if (character == '\'') {
|
||||
quoted += "'\\''";
|
||||
} else {
|
||||
quoted += character;
|
||||
}
|
||||
}
|
||||
quoted += '\'';
|
||||
return quoted;
|
||||
}
|
||||
|
||||
// Runs `command`, returning its exit status and keeping the last line of its output for a message.
|
||||
int Run(const std::string& command, std::string& lastLine) {
|
||||
lastLine.clear();
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
(void)command;
|
||||
return -1;
|
||||
#else
|
||||
FILE* pipe = popen(command.c_str(), "r");
|
||||
if (pipe == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
std::array<char, 512> buffer{};
|
||||
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
if (const std::string line = Trimmed(buffer.data()); !line.empty()) {
|
||||
lastLine = line;
|
||||
}
|
||||
}
|
||||
const int status = pclose(pipe);
|
||||
if (status == -1 || !WIFEXITED(status)) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(status);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reads a "<state>\t<unix time>\t<text>" line the script wrote. False when there is none, it is too
|
||||
// old to be about now, or it cannot be understood.
|
||||
bool ReadStatusFile(const std::filesystem::path& file, State& state, std::string& detail) {
|
||||
const std::string line = ReadFirstLine(file);
|
||||
if (line.empty()) {
|
||||
return false;
|
||||
}
|
||||
const auto firstTab = line.find('\t');
|
||||
const auto secondTab = line.find('\t', firstTab + 1);
|
||||
if (firstTab == std::string::npos || secondTab == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
const std::string written = line.substr(0, firstTab);
|
||||
long long seconds = 0;
|
||||
const char* const secondsBegin = line.data() + firstTab + 1;
|
||||
if (std::from_chars(secondsBegin, line.data() + secondTab, seconds).ec != std::errc{}) {
|
||||
return false;
|
||||
}
|
||||
const auto age = std::chrono::system_clock::now() -
|
||||
std::chrono::system_clock::time_point(std::chrono::seconds(seconds));
|
||||
const auto lifetime = written == "running" ? kRunningStatusLifetime : kStatusLifetime;
|
||||
if (age > lifetime || age < -lifetime) {
|
||||
return false;
|
||||
}
|
||||
if (written == "running") {
|
||||
state = State::Updating;
|
||||
} else if (written == "available") {
|
||||
state = State::Available;
|
||||
} else if (written == "uptodate" || written == "done") {
|
||||
state = State::UpToDate;
|
||||
} else if (written == "failed") {
|
||||
state = State::Failed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
detail = line.substr(secondTab + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status Current() {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
// Drawn every frame, so the files behind it are read on a timer instead.
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!g_haveRead || now - g_lastRead > kReadInterval) {
|
||||
g_releaseOnDisk = InstalledRelease();
|
||||
if (!g_haveRunningRelease) {
|
||||
g_runningRelease = g_releaseOnDisk;
|
||||
g_haveRunningRelease = true;
|
||||
}
|
||||
g_lastRead = now;
|
||||
g_haveRead = true;
|
||||
g_canUpdate = ReadConfig().Usable();
|
||||
g_fileState = State::Idle;
|
||||
g_fileDetail.clear();
|
||||
g_haveFileState = ReadStatusFile(RuntimeConfigFile::ApplicationDataDirectory() / kStatusFileName,
|
||||
g_fileState, g_fileDetail);
|
||||
}
|
||||
|
||||
Status status;
|
||||
status.installedRelease = g_runningRelease;
|
||||
status.restartNeeded = g_releaseOnDisk != g_runningRelease;
|
||||
if (!g_canUpdate) {
|
||||
status.state = State::PcInstall;
|
||||
return status;
|
||||
}
|
||||
// An update's own reports outrank what this session last said: one started here (the session
|
||||
// says Updating until the update writes its first step), or one this game was reopened after.
|
||||
const bool followUpdate = !g_sessionStateSet || g_sessionState == State::Updating;
|
||||
if (followUpdate && g_haveFileState) {
|
||||
status.state = g_fileState;
|
||||
status.detail = g_fileDetail;
|
||||
} else if (g_sessionStateSet) {
|
||||
status.state = g_sessionState;
|
||||
status.detail = g_sessionDetail;
|
||||
} else {
|
||||
status.state = State::Idle;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void StartCheck() {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
if (g_sessionStateSet && (g_sessionState == State::Checking || g_sessionState == State::Updating)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
return;
|
||||
}
|
||||
SetSessionState(State::Checking, "Asking github.com for the newest release");
|
||||
|
||||
// Detached: it talks to the network and must not hold up a frame, and there is no shutdown
|
||||
// ordering to manage because it touches nothing the game owns.
|
||||
std::thread([config] {
|
||||
const auto statusFile = RuntimeConfigFile::ApplicationDataDirectory() / kCheckStatusFileName;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
std::string command = "WIICOMPILED_UPDATE_STATUS=" + ShellQuoted(statusFile.string()) + ' ' +
|
||||
ShellQuoted(config.script.string()) + " --check";
|
||||
if (!config.workDir.empty()) {
|
||||
command += " --work-dir " + ShellQuoted(config.workDir.string());
|
||||
}
|
||||
command += " 2>&1";
|
||||
std::string lastLine;
|
||||
const int exitStatus = Run(command, lastLine);
|
||||
|
||||
State state = State::Failed;
|
||||
std::string detail;
|
||||
if (ReadStatusFile(statusFile, state, detail)) {
|
||||
SetSessionState(state, detail);
|
||||
} else if (exitStatus == 0) {
|
||||
// It ran and said nothing this can read, which is still an answer: ask again later.
|
||||
SetSessionState(State::Idle, "The check said nothing this version understands");
|
||||
} else {
|
||||
std::cerr << "[update] the check failed (exit " << exitStatus << "): " << lastLine << std::endl;
|
||||
SetSessionState(State::Failed,
|
||||
lastLine.empty() ? "The check could not be run" : lastLine);
|
||||
}
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
bool StartUpdate(std::string& error) {
|
||||
error.clear();
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
error = "This build was installed from a PC, so the update is run there.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto dataDirectory = RuntimeConfigFile::ApplicationDataDirectory();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dataDirectory, ec);
|
||||
// How the update opens the game again when it is done. Steam names the running title; without
|
||||
// one (a build started from a terminal) the update still runs and the game is started by hand.
|
||||
std::string steamGameId;
|
||||
for (const char* variable : {"SteamGameId", "SteamAppId"}) {
|
||||
if (const char* value = std::getenv(variable); value != nullptr && *value != '\0') {
|
||||
steamGameId = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::ofstream request(dataDirectory / kRequestFileName, std::ios::trunc);
|
||||
if (!request) {
|
||||
error = "Could not write to " + dataDirectory.string();
|
||||
return false;
|
||||
}
|
||||
request << "# Written by the game's Update button; the update removes it.\n";
|
||||
if (!steamGameId.empty()) {
|
||||
request << "steam_game_id=" << steamGameId << '\n';
|
||||
}
|
||||
}
|
||||
// The previous update's result must not be read as this one's before it has written anything.
|
||||
std::filesystem::remove(dataDirectory / kStatusFileName, ec);
|
||||
// The update must outlive the game if the player closes it meanwhile, and Steam ends the game's
|
||||
// own processes when it closes, so it runs as a service of its own. --no-block: started, not
|
||||
// waited for.
|
||||
std::string lastLine;
|
||||
const std::string command =
|
||||
"systemctl --user start --no-block " + ShellQuoted(config.service) + " 2>&1";
|
||||
if (const int exitStatus = Run(command, lastLine); exitStatus != 0) {
|
||||
std::filesystem::remove(dataDirectory / kRequestFileName, ec);
|
||||
std::cerr << "[update] could not start " << config.service << " (exit " << exitStatus
|
||||
<< "): " << lastLine << std::endl;
|
||||
error = lastLine.empty() ? "Could not start the update service" : lastLine;
|
||||
return false;
|
||||
}
|
||||
SetSessionState(State::Updating, "Starting the update");
|
||||
// Read the files again at once, rather than show the removed status for up to a second.
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_haveRead = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace update_check
|
||||
|
||||
#undef MKW_UPDATE_CHECK_SUPPORTED
|
||||
Reference in new issue
Block a user