Compare commits

..
2 Commits
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 8662777f11 Merge feature/frame-updater: the Steam Frame's launcher updates the game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:53:11 +02:00
Stefan MaierandClaude Opus 5.5 e24df8ead9 The Steam Frame's launcher updates the game
The game's own updater (the Quest's) cannot on the Steam Frame: Lepton
installs the APK in the game's folder there at every start. So the
launcher does it: while the game runs it asks GitHub for the latest
release, fetches a newer one's Steam Frame bundle into update/, checks
it (its build.txt is the release's number), and puts the APK and the
launcher in place as the next start begins, before Lepton runs the APK.
update.auto = false in config.toml stops it, as on the Quest; a build
with no number (build.txt, which the bundle now carries and the
installer copies; 0 for local builds) never looks; without the internet,
or when anything fails, nothing changes. The in-game updater no longer
starts there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:40:26 +02:00
7 changed files with 107 additions and 9 deletions

No files matched your search

+3 -2
View File
@@ -116,8 +116,9 @@ No computer and no keyboard needed.
library and offers to switch back to Gaming Mode.
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
To update, open the same file in Desktop Mode again; your settings,
saves and game data stay. More about the Steam Frame:
The game updates itself: when a new version is out, it is downloaded
while you play and used from the next start on. Your settings, saves and
game data stay. More about the Steam Frame:
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
## Playing
+5 -2
View File
@@ -16,8 +16,11 @@ On the Frame, in Desktop Mode (Steam menu > Power > Switch to Desktop):
copies the game data out of it, adds "Ringfall: Mjolnir VR" to the Steam library
and switches back to Gaming Mode. Start it from the library there.
Opening the same file again updates the game (settings, saves and game data
stay).
The game updates itself: while it runs, its launcher looks for a newer
release, fetches it and puts it in place as the next start begins
(`steam_frame_launch.sh`; `update.auto = false` in `config.toml` stops it;
`launcher.log` says what it did). Opening the same file again updates it
too (settings, saves and game data stay).
The desktop file (`steam_frame_installer.desktop`) downloads the latest
release's `ringfall-mjolnir-vr-steam-frame.zip` (`tools/steam_frame_bundle.py`, made by
@@ -35,7 +35,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
static final String SHARED_DATA_ROOT = "/home/steamos/Documents/RingfallMjolnirVR";
private File dataRoot;
private TextView status;
@@ -36,7 +36,8 @@ import java.util.zip.ZipInputStream;
* the latest release when it starts, on a thread of its own, and if it is
* newer asks the player whether to update:
*
* - Yes: the release's app (halo-android-release.zip or -debug.zip) is
* - Yes: the release's app (ringfall-mjolnir-vr-android-release.zip or
* -debug.zip) is
* downloaded and handed to Android's package installer, which replaces the
* game (closing it) and offers to open the new version.
* - No: nothing, until the next start.
@@ -45,6 +46,9 @@ import java.util.zip.ZipInputStream;
*
* Every build of main is signed with the same key (the workflow's), which an
* app must keep for Android to install a new version over it.
*
* Not on the Steam Frame: Lepton installs the APK in the game's folder there
* at every start, so its launcher updates that (steam_frame_launch.sh).
*/
final class Updater {
/* the VR port's own releases: the original's, made without VR, must
@@ -63,7 +67,8 @@ final class Updater {
static void start(Activity activity) {
File config = configFile(activity);
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config))
if (BuildConfig.HALO_BUILD_NUMBER <= 0 || config == null || !autoUpdate(config) ||
new File(LauncherActivity.SHARED_DATA_ROOT).isDirectory())
return;
new Thread(() -> {
int latest = latestRelease();
+4
View File
@@ -237,6 +237,10 @@ cp "$BUNDLE/$LAUNCHER" "$GAME_DIR/$LAUNCHER.tmp" && chmod 755 "$GAME_DIR/$LAUNCH
cp "$BUNDLE/$APK_NAME" "$GAME_DIR/$APK_NAME.tmp" && chmod 666 "$GAME_DIR/$APK_NAME.tmp" &&
mv -f "$GAME_DIR/$APK_NAME.tmp" "$GAME_DIR/$APK_NAME" && touch "$GAME_DIR/$APK_NAME" ||
fail "Could not write $GAME_DIR/$APK_NAME (is the storage full?)."
# (the build's number: the launcher updates from it, steam_frame_launch.sh;
# an update fetched for an older build is dropped)
cp "$BUNDLE/build.txt" "$GAME_DIR/build.txt" 2>/dev/null || rm -f "$GAME_DIR/build.txt"
rm -rf "$GAME_DIR/update"
# --- the game data
+72
View File
@@ -70,6 +70,75 @@ run()
child=0
}
# Updates (update.auto in config.toml; the game's own updater, the Quest's,
# does nothing here: Lepton installs the APK in this folder each start).
# While the game runs, the latest release is looked for; a newer one's
# bundle is fetched into update/, checked, and put in place as the next
# start begins, before Lepton runs the APK (it installs the newer one).
# A build that knows no number of its own (build.txt: a local one) never
# looks. Nothing changes without the internet, or when anything fails.
REPOSITORY="@REPOSITORY@"
UPDATE="$DATA/update"
BUILD_FILE="$DATA/build.txt"
SELF="$DATA/ringfall-mjolnir-vr.sh"
update_apply()
{
local new="$UPDATE/bundle" build
[ -f "$UPDATE/ready" ] || { rm -rf "$UPDATE"; return 0; }
build=$(cat "$new/build.txt" 2>/dev/null)
# (the launcher renamed over this one: bash goes on reading its own copy)
if cp "$new/ringfall-mjolnir-vr.sh" "$SELF.tmp" && chmod 755 "$SELF.tmp" &&
cp "$new/RingfallMjolnirVR.apk" "$APK.tmp" && chmod 666 "$APK.tmp" &&
mv -f "$SELF.tmp" "$SELF" && mv -f "$APK.tmp" "$APK" && touch "$APK" &&
cp "$new/build.txt" "$BUILD_FILE"
then
log "update: build $build put in place"
else
log "update: build $build could not be put in place"
rm -f "$SELF.tmp" "$APK.tmp"
fi
rm -rf "$UPDATE"
}
update_wanted()
{
# (update.auto = false in config.toml's [update]: never)
! awk '/^\[/ { section = $0 } section == "[update]" && /^[ \t]*auto[ \t]*=/ { print $3 }' \
"$DATA/config.toml" 2>/dev/null | grep -q false
}
update_check()
{
local installed latest
installed=$(cat "$BUILD_FILE" 2>/dev/null)
[[ "$installed" =~ ^[0-9]+$ ]] && (( installed > 0 )) || return 0
update_wanted || return 0
latest=$(curl -fsSL --max-time 20 -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/$REPOSITORY/releases/latest" |
python3 -I -c 'import json, sys; print(json.load(sys.stdin)["tag_name"].rpartition("-")[2])' 2>/dev/null)
[[ "$latest" =~ ^[0-9]+$ ]] && (( latest > installed )) || return 0
log "update: build $latest found (this is $installed), fetching it"
rm -rf "$UPDATE" && mkdir -p "$UPDATE" || return 0
if curl -fsSL --retry 2 --max-time 900 -o "$UPDATE/bundle.zip" \
"https://github.com/$REPOSITORY/releases/download/build-$latest/ringfall-mjolnir-vr-steam-frame.zip" &&
python3 -I -m zipfile -e "$UPDATE/bundle.zip" "$UPDATE/bundle" &&
[ "$(cat "$UPDATE/bundle/build.txt" 2>/dev/null)" = "$latest" ] &&
[ -s "$UPDATE/bundle/RingfallMjolnirVR.apk" ] && [ -s "$UPDATE/bundle/ringfall-mjolnir-vr.sh" ]
then
rm -f "$UPDATE/bundle.zip"
touch "$UPDATE/ready"
log "update: build $latest fetched, put in place at the next start"
else
log "update: build $latest could not be fetched"
rm -rf "$UPDATE"
fi
}
update_apply
update_check </dev/null >/dev/null 2>&1 &
updater=$!
# (a new APK is only installed on its first start, not started; and a start
# right after a game that was ended hard can fail in SteamVR, which then
# needs a moment: again, up to three times, while it ends within 30 s)
@@ -83,4 +152,7 @@ for attempt in 1 2 3; do
sleep 5
done
rm -f "$QUIT"
# (a fetch still under way is given up: the next start looks again)
pkill -P "$updater" 2>/dev/null
kill "$updater" 2>/dev/null
log "end"
+15 -2
View File
@@ -7,13 +7,17 @@ The Steam Frame's installer as release assets (port/android/STEAM_FRAME.md):
writes into the output folder
- ringfall-mjolnir-vr-steam-frame.zip: the installer (port/android/steam_frame_install.sh,
as install.sh), the APK (RingfallMjolnirVR.apk), the launcher
(steam_frame_launch.sh, as ringfall-mjolnir-vr.sh), the disc image reader (steam_frame_xiso.py, with
(steam_frame_launch.sh, as ringfall-mjolnir-vr.sh, which updates the game
from the repository named), the build's number (build.txt: the workflow's
run number, HALO_BUILD_NUMBER or GITHUB_RUN_NUMBER; 0, which never
updates, without one), the disc image reader (steam_frame_xiso.py, with
extract-xiso's license) and the library pictures (grid/);
- Install-Ringfall-Mjolnir-VR.desktop (port/android/steam_frame_installer.desktop): what
the player downloads and opens in Desktop Mode; it fetches the latest
release's zip from the repository named.
"""
import os
import sys
import zipfile
from pathlib import Path
@@ -38,13 +42,22 @@ def main():
]
files += [(picture, "grid/" + picture.name, 0o644)
for picture in sorted((ANDROID / "steam_frame_grid").glob("*.png"))]
build = os.environ.get("HALO_BUILD_NUMBER") or os.environ.get("GITHUB_RUN_NUMBER") or "0"
if not build.isdigit():
build = "0"
bundle = output / "ringfall-mjolnir-vr-steam-frame.zip"
with zipfile.ZipFile(bundle, "w", zipfile.ZIP_DEFLATED) as archive:
for path, name, mode in files:
info = zipfile.ZipInfo.from_file(path, name)
info.external_attr = (0o100000 | mode) << 16
info.compress_type = zipfile.ZIP_STORED if name.endswith((".apk", ".png")) else zipfile.ZIP_DEFLATED
archive.writestr(info, path.read_bytes())
data = path.read_bytes()
if name == "ringfall-mjolnir-vr.sh":
data = data.replace(b"@REPOSITORY@", repository.encode())
archive.writestr(info, data)
info = zipfile.ZipInfo("build.txt", date_time=(2026, 1, 1, 0, 0, 0))
info.external_attr = (0o100000 | 0o644) << 16
archive.writestr(info, build + "\n")
print(f"{bundle.relative_to(Path.cwd()) if bundle.is_relative_to(Path.cwd()) else bundle}")
desktop = (ANDROID / "steam_frame_installer.desktop").read_text().replace("@REPOSITORY@", repository)