mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
9aa8b4e371
Consolidate the native OTA launcher, bootstrap, and assets into the Switch port tree.
282 lines
8.5 KiB
C
282 lines
8.5 KiB
C
/*
|
|
* gen1recomp Switch OTA launcher
|
|
*
|
|
* Quiet by default: checks GitHub Releases with no UI. Only shows the
|
|
* branded launcher-style screen when an update is available. Downloads the
|
|
* same SD zip (gen1recomp-*-switch.zip), verifies SHA-256, replaces game +
|
|
* launcher NROs, then envSetNextLoad. Never touches pokemon-love2d/.
|
|
* LÖVE self-updater stays off on NX.
|
|
*/
|
|
|
|
#include "ota_fs.h"
|
|
#include "ota_net.h"
|
|
#include "ota_protocol.h"
|
|
#include "ota_ui.h"
|
|
#include "ota_unzip.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#if defined(__SWITCH__)
|
|
#include <sys/stat.h>
|
|
#include <switch.h>
|
|
#include <unistd.h>
|
|
#else
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#define SD_INSTALL_DIR "sdmc:/switch/gen1recomp"
|
|
#define CHECK_TIMEOUT_MS (OTA_CHECK_TIMEOUT_SEC * 1000L)
|
|
#define GAME_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_GAME_NRO_NAME
|
|
#define LAUNCHER_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_LAUNCHER_NRO_NAME
|
|
|
|
typedef struct {
|
|
const char *detail;
|
|
float base;
|
|
float span;
|
|
} download_progress_ctx_t;
|
|
|
|
static void on_download_progress(void *userdata, double fraction) {
|
|
download_progress_ctx_t *ctx = (download_progress_ctx_t *)userdata;
|
|
if (!ctx) return;
|
|
float p = ctx->base;
|
|
if (fraction >= 0.0) p += ctx->span * (float)fraction;
|
|
else p += ctx->span * 0.1f;
|
|
if (p > ctx->base + ctx->span) p = ctx->base + ctx->span;
|
|
ota_ui_show_progress("Step 1/3: Downloading...", ctx->detail, p);
|
|
}
|
|
|
|
static void show_update_error(const char *title, const char *friendly, const char *technical,
|
|
const char *installed) {
|
|
ota_ui_alert_error(title, friendly, technical, installed);
|
|
}
|
|
|
|
static int run_update_flow(const char *install_dir) {
|
|
char installed[64] = "0.0.0";
|
|
(void)ota_fs_read_installed_version(install_dir, installed, sizeof(installed));
|
|
|
|
char *json = NULL;
|
|
size_t json_len = 0;
|
|
char err[256];
|
|
err[0] = '\0';
|
|
|
|
if (ota_net_download_buffer(OTA_RELEASES_API, CHECK_TIMEOUT_MS, &json, &json_len, err,
|
|
sizeof(err)) != 0) {
|
|
free(json);
|
|
return 0; /* offline / timeout — play installed, no UI */
|
|
}
|
|
|
|
ota_release_t rel;
|
|
if (!ota_parse_release(json, &rel)) {
|
|
free(json);
|
|
return 0;
|
|
}
|
|
free(json);
|
|
|
|
ota_decision_t dec;
|
|
ota_decide_update(installed, &rel, &dec);
|
|
if (strcmp(dec.status, "uptodate") == 0 || strcmp(dec.status, "error") == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (!ota_ui_prompt_update(installed, dec.version)) {
|
|
return 0;
|
|
}
|
|
|
|
char updates_dir[192];
|
|
char zip_path[256];
|
|
char sums_path[256];
|
|
snprintf(updates_dir, sizeof(updates_dir), "%s/updates", install_dir);
|
|
snprintf(zip_path, sizeof(zip_path), "%s/updates/%s", install_dir, dec.asset_name);
|
|
snprintf(sums_path, sizeof(sums_path), "%s/updates/sha256sums.txt", install_dir);
|
|
|
|
#if defined(__SWITCH__)
|
|
mkdir(updates_dir, 0755);
|
|
#endif
|
|
|
|
download_progress_ctx_t dl_ctx = {"This may take a minute.", 0.f, 0.5f};
|
|
ota_ui_show_progress("Step 1/3: Downloading...", dl_ctx.detail, dl_ctx.base);
|
|
if (ota_net_download_file(dec.download_url, zip_path, 180000L, err, sizeof(err),
|
|
on_download_progress, &dl_ctx) != 0) {
|
|
show_update_error("Download failed",
|
|
"Could not download the update. Check Wi-Fi and try again.", err, installed);
|
|
return 0;
|
|
}
|
|
|
|
ota_ui_show_progress("Step 2/3: Verifying...", "Checking file integrity.", 0.55f);
|
|
char sums_url[512];
|
|
snprintf(sums_url, sizeof(sums_url),
|
|
"https://github.com/bryanthaboi/gen1recomp/releases/download/%s/sha256sums.txt",
|
|
rel.tag);
|
|
if (ota_net_download_file(sums_url, sums_path, CHECK_TIMEOUT_MS, err, sizeof(err), NULL,
|
|
NULL) != 0) {
|
|
remove(zip_path);
|
|
show_update_error("Could not verify", "Downloaded file could not be checked.", err, installed);
|
|
return 0;
|
|
}
|
|
|
|
FILE *sf = fopen(sums_path, "rb");
|
|
if (!sf) {
|
|
remove(zip_path);
|
|
show_update_error("Could not verify", "Could not read checksum file.", "fopen sums failed",
|
|
installed);
|
|
return 0;
|
|
}
|
|
fseek(sf, 0, SEEK_END);
|
|
long slen = ftell(sf);
|
|
fseek(sf, 0, SEEK_SET);
|
|
char *sums = (char *)malloc((size_t)slen + 1);
|
|
if (!sums) {
|
|
fclose(sf);
|
|
remove(zip_path);
|
|
show_update_error("Could not verify", "Not enough memory to verify update.", "malloc failed",
|
|
installed);
|
|
return 0;
|
|
}
|
|
fread(sums, 1, (size_t)slen, sf);
|
|
sums[slen] = '\0';
|
|
fclose(sf);
|
|
|
|
ota_ui_show_progress("Step 2/3: Verifying...", "Computing checksum...", 0.65f);
|
|
char hex[96];
|
|
if (ota_fs_sha256_file(zip_path, hex, sizeof(hex)) != 0) {
|
|
free(sums);
|
|
remove(zip_path);
|
|
show_update_error("Could not verify", "Could not read downloaded update file.",
|
|
"sha256 file read failed", installed);
|
|
return 0;
|
|
}
|
|
ota_verify_t ver;
|
|
ota_verify_sha256(dec.asset_name, hex, sums, &ver);
|
|
free(sums);
|
|
if (!ver.ok) {
|
|
remove(zip_path);
|
|
show_update_error("Update check failed",
|
|
"Downloaded file did not match expected checksum.", ver.reason, installed);
|
|
return 0;
|
|
}
|
|
|
|
char extracted[256];
|
|
char extracted_launcher[256];
|
|
snprintf(extracted, sizeof(extracted), "%s/updates/gen1recomp-game.nro.verified", install_dir);
|
|
snprintf(extracted_launcher, sizeof(extracted_launcher), "%s/updates/gen1recomp.nro.verified",
|
|
install_dir);
|
|
|
|
#if defined(__SWITCH__)
|
|
ota_ui_show_progress("Step 3/3: Installing...", "Keeping your saves safe.", 0.8f);
|
|
if (ota_unzip_extract_file(zip_path, GAME_MEMBER_IN_ZIP, extracted, err, sizeof(err)) != 0) {
|
|
if (ota_unzip_extract_file(zip_path, OTA_GAME_NRO_NAME, extracted, err, sizeof(err)) != 0) {
|
|
remove(zip_path);
|
|
show_update_error("Could not extract",
|
|
"Update zip is missing game files or is corrupted.", err, installed);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (ota_fs_atomic_replace_game(install_dir, extracted, err, sizeof(err)) != 0) {
|
|
remove(extracted);
|
|
remove(zip_path);
|
|
show_update_error("Could not install",
|
|
"Could not replace game on microSD. Free up space and try again.", err,
|
|
installed);
|
|
return 0;
|
|
}
|
|
remove(extracted);
|
|
|
|
int have_launcher = 0;
|
|
if (ota_unzip_extract_file(zip_path, LAUNCHER_MEMBER_IN_ZIP, extracted_launcher, err,
|
|
sizeof(err)) == 0 ||
|
|
ota_unzip_extract_file(zip_path, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err,
|
|
sizeof(err)) == 0) {
|
|
have_launcher = 1;
|
|
}
|
|
if (!have_launcher) {
|
|
remove(zip_path);
|
|
show_update_error("Could not extract",
|
|
"Update zip is missing launcher files.", err, installed);
|
|
return 0;
|
|
}
|
|
|
|
char bootstrap_path[256];
|
|
bootstrap_path[0] = '\0';
|
|
if (ota_fs_stage_launcher_bootstrap(install_dir, extracted_launcher, bootstrap_path,
|
|
sizeof(bootstrap_path), err, sizeof(err)) != 0) {
|
|
remove(extracted_launcher);
|
|
remove(zip_path);
|
|
show_update_error("Could not install",
|
|
"Game updated but launcher could not be staged. Reinstall from the SD zip.",
|
|
err, installed);
|
|
return 0;
|
|
}
|
|
remove(extracted_launcher);
|
|
|
|
char vpath[192];
|
|
snprintf(vpath, sizeof(vpath), "%s/version.txt", install_dir);
|
|
FILE *vf = fopen(vpath, "wb");
|
|
if (vf) {
|
|
fprintf(vf, "%s\n", dec.version);
|
|
fclose(vf);
|
|
}
|
|
|
|
ota_ui_show_progress("Ready", "Finishing update...", 1.0f);
|
|
svcSleepThread(600000000ULL);
|
|
remove(zip_path);
|
|
ota_ui_shutdown();
|
|
|
|
if (ota_fs_handoff_to_game(bootstrap_path, err, sizeof(err)) != 0) {
|
|
show_update_error("Could not install", "Launcher bootstrap failed.", err, installed);
|
|
return 0;
|
|
}
|
|
return 2; /* chainload bootstrap; main must not hand off to game */
|
|
#else
|
|
(void)extracted;
|
|
(void)extracted_launcher;
|
|
#endif
|
|
remove(zip_path);
|
|
ota_ui_shutdown();
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
|
|
const char *install = SD_INSTALL_DIR;
|
|
int update_rc = 0;
|
|
|
|
#if defined(__SWITCH__)
|
|
socketInitializeDefault();
|
|
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
|
|
if (ota_net_init() == 0) {
|
|
update_rc = run_update_flow(install);
|
|
ota_net_shutdown();
|
|
}
|
|
#else
|
|
update_rc = run_update_flow(install);
|
|
#endif
|
|
|
|
if (update_rc == 2) {
|
|
#if defined(__SWITCH__)
|
|
socketExit();
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
char game[192];
|
|
snprintf(game, sizeof(game), "%s/%s", install, OTA_GAME_NRO_NAME);
|
|
char err[128];
|
|
err[0] = '\0';
|
|
if (ota_fs_handoff_to_game(game, err, sizeof(err)) != 0) {
|
|
ota_ui_missing_game();
|
|
}
|
|
|
|
ota_ui_shutdown();
|
|
|
|
#if defined(__SWITCH__)
|
|
socketExit();
|
|
#endif
|
|
return 0;
|
|
}
|