mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Improve Switch OTA flow with download progress and clearer error screens.
This commit is contained in:
@@ -7,13 +7,16 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* fraction is 0..1 when Content-Length is known, else -1 for indeterminate. */
|
||||
typedef void (*ota_net_progress_fn)(void *userdata, double fraction);
|
||||
|
||||
/* Download URL into memory buffer (caller frees *out). Returns 0 on success. */
|
||||
int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t *out_len,
|
||||
char *err, size_t err_len);
|
||||
|
||||
/* Download URL to a filesystem path. Returns 0 on success. */
|
||||
/* Download URL to a filesystem path. progress may be NULL. Returns 0 on success. */
|
||||
int ota_net_download_file(const char *url, const char *path, long timeout_ms, char *err,
|
||||
size_t err_len);
|
||||
size_t err_len, ota_net_progress_fn progress, void *progress_ud);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ void ota_ui_show_progress(const char *title, const char *detail, float progress0
|
||||
/* Error / info + wait for B. */
|
||||
void ota_ui_alert(const char *title, const char *line1, const char *line2);
|
||||
|
||||
/* User-friendly error with optional technical detail and installed-version footer. */
|
||||
void ota_ui_alert_error(const char *title, const char *friendly, const char *technical,
|
||||
const char *installed_version);
|
||||
|
||||
/* Missing game install screen; wait for +. */
|
||||
void ota_ui_missing_game(void);
|
||||
|
||||
|
||||
@@ -32,6 +32,27 @@
|
||||
#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));
|
||||
@@ -75,27 +96,32 @@ static int run_update_flow(const char *install_dir) {
|
||||
mkdir(updates_dir, 0755);
|
||||
#endif
|
||||
|
||||
ota_ui_show_progress("Downloading…", "This may take a minute.", 0.15f);
|
||||
if (ota_net_download_file(dec.download_url, zip_path, 180000L, err, sizeof(err)) != 0) {
|
||||
ota_ui_alert("Download failed", "Playing the installed version.", NULL);
|
||||
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("Checking update…", "Verifying file integrity.", 0.55f);
|
||||
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)) != 0) {
|
||||
if (ota_net_download_file(sums_url, sums_path, CHECK_TIMEOUT_MS, err, sizeof(err), NULL,
|
||||
NULL) != 0) {
|
||||
remove(zip_path);
|
||||
ota_ui_alert("Could not verify", "Playing the installed version.", NULL);
|
||||
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);
|
||||
ota_ui_shutdown();
|
||||
show_update_error("Could not verify", "Could not read checksum file.", "fopen sums failed",
|
||||
installed);
|
||||
return 0;
|
||||
}
|
||||
fseek(sf, 0, SEEK_END);
|
||||
@@ -104,18 +130,22 @@ static int run_update_flow(const char *install_dir) {
|
||||
char *sums = (char *)malloc((size_t)slen + 1);
|
||||
if (!sums) {
|
||||
fclose(sf);
|
||||
ota_ui_shutdown();
|
||||
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);
|
||||
ota_ui_shutdown();
|
||||
show_update_error("Could not verify", "Could not read downloaded update file.",
|
||||
"sha256 file read failed", installed);
|
||||
return 0;
|
||||
}
|
||||
ota_verify_t ver;
|
||||
@@ -123,7 +153,8 @@ static int run_update_flow(const char *install_dir) {
|
||||
free(sums);
|
||||
if (!ver.ok) {
|
||||
remove(zip_path);
|
||||
ota_ui_alert("Update check failed", "Playing the installed version.", NULL);
|
||||
show_update_error("Update check failed",
|
||||
"Downloaded file did not match expected checksum.", ver.reason, installed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -134,11 +165,12 @@ static int run_update_flow(const char *install_dir) {
|
||||
install_dir);
|
||||
|
||||
#if defined(__SWITCH__)
|
||||
ota_ui_show_progress("Installing…", "Keeping your saves safe.", 0.8f);
|
||||
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);
|
||||
ota_ui_alert("Install failed", "Playing the installed version.", NULL);
|
||||
show_update_error("Could not extract",
|
||||
"Update zip is missing game files or is corrupted.", err, installed);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +178,9 @@ static int run_update_flow(const char *install_dir) {
|
||||
if (ota_fs_atomic_replace_game(install_dir, extracted, err, sizeof(err)) != 0) {
|
||||
remove(extracted);
|
||||
remove(zip_path);
|
||||
ota_ui_alert("Install failed", "Playing the installed version.", NULL);
|
||||
show_update_error("Could not install",
|
||||
"Could not replace game on microSD. Free up space and try again.", err,
|
||||
installed);
|
||||
return 0;
|
||||
}
|
||||
remove(extracted);
|
||||
@@ -168,7 +202,7 @@ static int run_update_flow(const char *install_dir) {
|
||||
fclose(vf);
|
||||
}
|
||||
|
||||
ota_ui_show_progress("Ready", "Starting the game…", 1.0f);
|
||||
ota_ui_show_progress("Ready", "Starting the game...", 1.0f);
|
||||
svcSleepThread(600000000ULL);
|
||||
#else
|
||||
(void)extracted;
|
||||
|
||||
@@ -15,6 +15,11 @@ struct mem_buf {
|
||||
size_t len;
|
||||
};
|
||||
|
||||
struct file_progress {
|
||||
ota_net_progress_fn fn;
|
||||
void *userdata;
|
||||
};
|
||||
|
||||
static size_t write_mem(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
struct mem_buf *m = (struct mem_buf *)userdata;
|
||||
size_t n = size * nmemb;
|
||||
@@ -30,6 +35,23 @@ static size_t write_mem(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
static size_t write_file(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||
return fwrite(ptr, size, nmemb, (FILE *)userdata);
|
||||
}
|
||||
|
||||
static int xfer_progress(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
|
||||
curl_off_t ulnow) {
|
||||
(void)ultotal;
|
||||
(void)ulnow;
|
||||
struct file_progress *fp = (struct file_progress *)clientp;
|
||||
if (!fp || !fp->fn) return 0;
|
||||
if (dltotal > 0) {
|
||||
double frac = (double)dlnow / (double)dltotal;
|
||||
if (frac < 0.0) frac = 0.0;
|
||||
if (frac > 1.0) frac = 1.0;
|
||||
fp->fn(fp->userdata, frac);
|
||||
} else if (dlnow > 0) {
|
||||
fp->fn(fp->userdata, -1.0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t *out_len,
|
||||
@@ -54,7 +76,6 @@ int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_mem);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
|
||||
/* Atmosphere / homebrew CA store is limited; match common homebrew practice. */
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
CURLcode rc = curl_easy_perform(curl);
|
||||
@@ -76,7 +97,7 @@ int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t
|
||||
}
|
||||
|
||||
int ota_net_download_file(const char *url, const char *path, long timeout_ms, char *err,
|
||||
size_t err_len) {
|
||||
size_t err_len, ota_net_progress_fn progress, void *progress_ud) {
|
||||
if (!url || !path) {
|
||||
if (err && err_len) snprintf(err, err_len, "bad args");
|
||||
return -1;
|
||||
@@ -93,6 +114,7 @@ int ota_net_download_file(const char *url, const char *path, long timeout_ms, ch
|
||||
if (err && err_len) snprintf(err, err_len, "curl_easy_init failed");
|
||||
return -1;
|
||||
}
|
||||
struct file_progress fp_cb = {progress, progress_ud};
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "gen1recomp-switch-ota");
|
||||
@@ -102,6 +124,11 @@ int ota_net_download_file(const char *url, const char *path, long timeout_ms, ch
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
if (progress) {
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xfer_progress);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &fp_cb);
|
||||
}
|
||||
CURLcode rc = curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
fclose(fp);
|
||||
@@ -109,9 +136,12 @@ int ota_net_download_file(const char *url, const char *path, long timeout_ms, ch
|
||||
if (err && err_len) snprintf(err, err_len, "%s", curl_easy_strerror(rc));
|
||||
return -1;
|
||||
}
|
||||
if (progress) progress(progress_ud, 1.0);
|
||||
return 0;
|
||||
#else
|
||||
(void)timeout_ms;
|
||||
(void)progress;
|
||||
(void)progress_ud;
|
||||
if (err && err_len)
|
||||
snprintf(err, err_len, "ota_net_download_file only available on __SWITCH__");
|
||||
return -1;
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
#define FB_W 1280
|
||||
#define FB_H 720
|
||||
|
||||
/* Theme.PAL (0–255) → RGBA8 */
|
||||
#define TITLE_Y 200
|
||||
#define LINE_TITLE 44
|
||||
#define LINE_BODY 36
|
||||
#define LINE_SMALL 28
|
||||
#define WRAP_MAX_PX 720
|
||||
|
||||
/* Theme.PAL (0-255) -> RGBA8 */
|
||||
#define COL_BG RGBA8_MAXALPHA(0, 0, 0)
|
||||
#define COL_INK RGBA8_MAXALPHA(255, 255, 255)
|
||||
#define COL_DETAIL RGBA8_MAXALPHA(200, 200, 200)
|
||||
@@ -60,9 +66,54 @@ static void fill_rect(u32 *fb, u32 stride_px, int x, int y, int w, int h, u32 co
|
||||
}
|
||||
}
|
||||
|
||||
/* Normalize UTF-8 punctuation to ASCII; drop other non-ASCII bytes. */
|
||||
static size_t ota_ui_sanitize_ascii(const char *src, char *dst, size_t dst_len) {
|
||||
if (!dst || dst_len == 0) return 0;
|
||||
dst[0] = '\0';
|
||||
if (!src) return 0;
|
||||
size_t w = 0;
|
||||
for (size_t i = 0; src[i] && w + 1 < dst_len;) {
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
if (c < 0x80) {
|
||||
dst[w++] = (char)c;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == 0xE2 && src[i + 1] && src[i + 2]) {
|
||||
unsigned char b2 = (unsigned char)src[i + 1];
|
||||
unsigned char b3 = (unsigned char)src[i + 2];
|
||||
if (b2 == 0x80 && b3 == 0xA6) {
|
||||
if (w + 3 < dst_len) {
|
||||
dst[w++] = '.';
|
||||
dst[w++] = '.';
|
||||
dst[w++] = '.';
|
||||
}
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
if (b2 == 0x80 && (b3 == 0x94 || b3 == 0x93)) {
|
||||
dst[w++] = '-';
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
if (b2 == 0x80 && (b3 == 0x98 || b3 == 0x99)) {
|
||||
dst[w++] = '\'';
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((c & 0xE0) == 0xC0) i += 2;
|
||||
else if ((c & 0xF0) == 0xE0) i += 3;
|
||||
else if ((c & 0xF8) == 0xF0) i += 4;
|
||||
else i++;
|
||||
}
|
||||
dst[w] = '\0';
|
||||
return w;
|
||||
}
|
||||
|
||||
static void draw_glyph(u32 *fb, u32 stride_px, int x, int y, char ch, u32 color, int scale) {
|
||||
unsigned char c = (unsigned char)ch;
|
||||
if (c > 127) c = '?';
|
||||
if (c > 127) return;
|
||||
const char *bits = font8x8_basic[c];
|
||||
for (int row = 0; row < 8; row++) {
|
||||
unsigned char line = (unsigned char)bits[row];
|
||||
@@ -81,21 +132,100 @@ static int text_width(const char *s, int scale) {
|
||||
|
||||
static void draw_text(u32 *fb, u32 stride_px, int x, int y, const char *s, u32 color, int scale) {
|
||||
if (!s) return;
|
||||
char buf[512];
|
||||
ota_ui_sanitize_ascii(s, buf, sizeof(buf));
|
||||
int cx = x;
|
||||
for (; *s; s++) {
|
||||
if (*s == '\n') {
|
||||
for (const char *p = buf; *p; p++) {
|
||||
if (*p == '\n') {
|
||||
cx = x;
|
||||
y += 8 * scale + 4;
|
||||
continue;
|
||||
}
|
||||
draw_glyph(fb, stride_px, cx, y, *s, color, scale);
|
||||
draw_glyph(fb, stride_px, cx, y, *p, color, scale);
|
||||
cx += 8 * scale;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_text_centered(u32 *fb, u32 stride_px, int y, const char *s, u32 color, int scale) {
|
||||
int w = text_width(s, scale);
|
||||
draw_text(fb, stride_px, (FB_W - w) / 2, y, s, color, scale);
|
||||
char buf[512];
|
||||
ota_ui_sanitize_ascii(s, buf, sizeof(buf));
|
||||
int w = text_width(buf, scale);
|
||||
draw_text(fb, stride_px, (FB_W - w) / 2, y, buf, color, scale);
|
||||
}
|
||||
|
||||
static int draw_text_wrapped_centered(u32 *fb, u32 stride_px, int y, const char *s, u32 color,
|
||||
int scale, int max_px) {
|
||||
if (!s || !*s) return y;
|
||||
char buf[512];
|
||||
ota_ui_sanitize_ascii(s, buf, sizeof(buf));
|
||||
int char_w = 8 * scale;
|
||||
int max_chars = max_px / char_w;
|
||||
if (max_chars < 8) max_chars = 8;
|
||||
|
||||
char line[128];
|
||||
int line_len = 0;
|
||||
const char *word = buf;
|
||||
int lines = 0;
|
||||
|
||||
while (*word) {
|
||||
while (*word == ' ') word++;
|
||||
if (!*word) break;
|
||||
|
||||
const char *end = word;
|
||||
while (*end && *end != ' ' && *end != '\n') end++;
|
||||
int wlen = (int)(end - word);
|
||||
|
||||
if (line_len > 0 && line_len + 1 + wlen > max_chars) {
|
||||
line[line_len] = '\0';
|
||||
draw_text_centered(fb, stride_px, y, line, color, scale);
|
||||
y += 8 * scale + 4;
|
||||
lines++;
|
||||
line_len = 0;
|
||||
}
|
||||
|
||||
if (wlen > max_chars) {
|
||||
if (line_len > 0) {
|
||||
line[line_len] = '\0';
|
||||
draw_text_centered(fb, stride_px, y, line, color, scale);
|
||||
y += 8 * scale + 4;
|
||||
lines++;
|
||||
line_len = 0;
|
||||
}
|
||||
while (wlen > 0) {
|
||||
int chunk = wlen > max_chars ? max_chars : wlen;
|
||||
memcpy(line, word, (size_t)chunk);
|
||||
line[chunk] = '\0';
|
||||
draw_text_centered(fb, stride_px, y, line, color, scale);
|
||||
y += 8 * scale + 4;
|
||||
lines++;
|
||||
word += chunk;
|
||||
wlen -= chunk;
|
||||
}
|
||||
word = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line_len > 0) line[line_len++] = ' ';
|
||||
memcpy(line + line_len, word, (size_t)wlen);
|
||||
line_len += wlen;
|
||||
line[line_len] = '\0';
|
||||
word = (*end == '\n') ? end + 1 : end;
|
||||
if (*(end - 1) == '\n' || *end == '\n') {
|
||||
draw_text_centered(fb, stride_px, y, line, color, scale);
|
||||
y += 8 * scale + 4;
|
||||
lines++;
|
||||
line_len = 0;
|
||||
if (*end == '\n') word = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (line_len > 0) {
|
||||
draw_text_centered(fb, stride_px, y, line, color, scale);
|
||||
y += 8 * scale + 4;
|
||||
lines++;
|
||||
}
|
||||
if (lines == 0) return y;
|
||||
return y;
|
||||
}
|
||||
|
||||
static void draw_rail(u32 *fb, u32 stride_px) {
|
||||
@@ -145,7 +275,6 @@ static void load_logo(void) {
|
||||
}
|
||||
for (int i = 0; i < w * h; i++) {
|
||||
unsigned char *p = data + i * 4;
|
||||
/* Premultiply-ish: keep RGB, store A in high byte for skip */
|
||||
g_logo[i] = RGBA8(p[0], p[1], p[2], p[3]);
|
||||
}
|
||||
g_logo_w = w;
|
||||
@@ -178,7 +307,6 @@ static void draw_button(u32 *fb, u32 stride_px, int x, int y, int w, int h, u32
|
||||
const char *label, int focused) {
|
||||
fill_rect(fb, stride_px, x, y, w, h, fill);
|
||||
if (focused) {
|
||||
/* 2px white focus ring (launcher focus outline) */
|
||||
fill_rect(fb, stride_px, x - 3, y - 3, w + 6, 2, COL_INK);
|
||||
fill_rect(fb, stride_px, x - 3, y + h + 1, w + 6, 2, COL_INK);
|
||||
fill_rect(fb, stride_px, x - 3, y - 3, 2, h + 6, COL_INK);
|
||||
@@ -190,10 +318,17 @@ static void draw_button(u32 *fb, u32 stride_px, int x, int y, int w, int h, u32
|
||||
fill_rect(fb, stride_px, x + w - 1, y, 1, h, COL_LINE);
|
||||
}
|
||||
int scale = 3;
|
||||
int tw = text_width(label, scale);
|
||||
char lbl[128];
|
||||
ota_ui_sanitize_ascii(label, lbl, sizeof(lbl));
|
||||
int max_tw = w - 24;
|
||||
int tw = text_width(lbl, scale);
|
||||
while (scale > 2 && tw > max_tw) {
|
||||
scale--;
|
||||
tw = text_width(lbl, scale);
|
||||
}
|
||||
int tx = x + (w - tw) / 2;
|
||||
int ty = y + (h - 8 * scale) / 2;
|
||||
draw_text(fb, stride_px, tx, ty, label, ink, scale);
|
||||
draw_text(fb, stride_px, tx, ty, lbl, ink, scale);
|
||||
}
|
||||
|
||||
static void draw_chrome(u32 *fb, u32 stride_px) {
|
||||
@@ -231,25 +366,25 @@ static void paint_prompt(u32 *fb, u32 stride_px, void *ctx) {
|
||||
prompt_ctx_t *p = (prompt_ctx_t *)ctx;
|
||||
draw_chrome(fb, stride_px);
|
||||
|
||||
int y = 180;
|
||||
int y = TITLE_Y - 20;
|
||||
draw_text_centered(fb, stride_px, y, "Update available", COL_INK, 3);
|
||||
y += 48;
|
||||
y += LINE_TITLE;
|
||||
|
||||
char line[96];
|
||||
snprintf(line, sizeof(line), "v%s -> v%s", p->installed ? p->installed : "?",
|
||||
snprintf(line, sizeof(line), "v%s to v%s", p->installed ? p->installed : "?",
|
||||
p->latest ? p->latest : "?");
|
||||
draw_text_centered(fb, stride_px, y, line, COL_YELLOW, 3);
|
||||
y += 56;
|
||||
y += LINE_TITLE + 8;
|
||||
|
||||
draw_text_centered(fb, stride_px, y, "Your saves stay on this console.", COL_MUTED, 2);
|
||||
y += 64;
|
||||
y += LINE_BODY + 28;
|
||||
|
||||
int bw = 420;
|
||||
int bw = 600;
|
||||
int bh = 64;
|
||||
int bx = (FB_W - bw) / 2;
|
||||
draw_button(fb, stride_px, bx, y, bw, bh, COL_YELLOW, COL_INVERSE, "A Update", p->focus == 0);
|
||||
y += bh + 24;
|
||||
draw_button(fb, stride_px, bx, y, bw, bh, COL_INK, COL_INVERSE, "B Play without updating",
|
||||
draw_button(fb, stride_px, bx, y, bw, bh, COL_INK, COL_INVERSE, "B Play without update",
|
||||
p->focus == 1);
|
||||
}
|
||||
|
||||
@@ -285,31 +420,32 @@ typedef struct {
|
||||
int show_bar;
|
||||
} status_ctx_t;
|
||||
|
||||
static void paint_progress_bar(u32 *fb, u32 stride_px, int y, float t) {
|
||||
int bw = 520;
|
||||
int bh = 18;
|
||||
int bx = (FB_W - bw) / 2;
|
||||
if (t < 0.f) t = 0.f;
|
||||
if (t > 1.f) t = 1.f;
|
||||
fill_rect(fb, stride_px, bx, y, bw, bh, COL_RAISED);
|
||||
fill_rect(fb, stride_px, bx, y, bw, 1, COL_LINE);
|
||||
fill_rect(fb, stride_px, bx, y + bh - 1, bw, 1, COL_LINE);
|
||||
fill_rect(fb, stride_px, bx, y, 1, bh, COL_LINE);
|
||||
fill_rect(fb, stride_px, bx + bw - 1, y, 1, bh, COL_LINE);
|
||||
int fw = (int)(bw * t);
|
||||
if (fw > 0) fill_rect(fb, stride_px, bx, y, fw, bh, COL_GREEN);
|
||||
}
|
||||
|
||||
static void paint_status(u32 *fb, u32 stride_px, void *ctx) {
|
||||
status_ctx_t *s = (status_ctx_t *)ctx;
|
||||
draw_chrome(fb, stride_px);
|
||||
int y = 200;
|
||||
int y = TITLE_Y;
|
||||
draw_text_centered(fb, stride_px, y, s->title ? s->title : "", COL_INK, 3);
|
||||
y += 48;
|
||||
y += LINE_TITLE;
|
||||
if (s->detail && s->detail[0]) {
|
||||
draw_text_centered(fb, stride_px, y, s->detail, COL_DETAIL, 2);
|
||||
y += 40;
|
||||
}
|
||||
if (s->show_bar) {
|
||||
int bw = 520;
|
||||
int bh = 18;
|
||||
int bx = (FB_W - bw) / 2;
|
||||
float t = s->progress;
|
||||
if (t < 0.f) t = 0.f;
|
||||
if (t > 1.f) t = 1.f;
|
||||
fill_rect(fb, stride_px, bx, y, bw, bh, COL_RAISED);
|
||||
fill_rect(fb, stride_px, bx, y, bw, 1, COL_LINE);
|
||||
fill_rect(fb, stride_px, bx, y + bh - 1, bw, 1, COL_LINE);
|
||||
fill_rect(fb, stride_px, bx, y, 1, bh, COL_LINE);
|
||||
fill_rect(fb, stride_px, bx + bw - 1, y, 1, bh, COL_LINE);
|
||||
int fw = (int)(bw * t);
|
||||
if (fw > 0) fill_rect(fb, stride_px, bx, y, fw, bh, COL_GREEN);
|
||||
y = draw_text_wrapped_centered(fb, stride_px, y, s->detail, COL_DETAIL, 2, WRAP_MAX_PX);
|
||||
y += LINE_SMALL;
|
||||
}
|
||||
if (s->show_bar) paint_progress_bar(fb, stride_px, y, s->progress);
|
||||
}
|
||||
|
||||
void ota_ui_show_status(const char *title, const char *detail) {
|
||||
@@ -326,18 +462,24 @@ typedef struct {
|
||||
const char *title;
|
||||
const char *line1;
|
||||
const char *line2;
|
||||
const char *line3;
|
||||
} alert_ctx_t;
|
||||
|
||||
static void paint_alert(u32 *fb, u32 stride_px, void *ctx) {
|
||||
alert_ctx_t *a = (alert_ctx_t *)ctx;
|
||||
draw_chrome(fb, stride_px);
|
||||
int y = 200;
|
||||
int y = TITLE_Y;
|
||||
draw_text_centered(fb, stride_px, y, a->title ? a->title : "", COL_YELLOW, 3);
|
||||
y += 52;
|
||||
if (a->line1) draw_text_centered(fb, stride_px, y, a->line1, COL_DETAIL, 2);
|
||||
y += 36;
|
||||
if (a->line2) draw_text_centered(fb, stride_px, y, a->line2, COL_MUTED, 2);
|
||||
y += 64;
|
||||
y += LINE_TITLE;
|
||||
if (a->line1 && a->line1[0])
|
||||
y = draw_text_wrapped_centered(fb, stride_px, y, a->line1, COL_DETAIL, 2, WRAP_MAX_PX);
|
||||
y += LINE_SMALL;
|
||||
if (a->line2 && a->line2[0])
|
||||
y = draw_text_wrapped_centered(fb, stride_px, y, a->line2, COL_MUTED, 2, WRAP_MAX_PX);
|
||||
y += LINE_SMALL;
|
||||
if (a->line3 && a->line3[0])
|
||||
y = draw_text_wrapped_centered(fb, stride_px, y, a->line3, COL_MUTED, 2, WRAP_MAX_PX);
|
||||
y += LINE_BODY + 16;
|
||||
int bw = 360;
|
||||
int bh = 56;
|
||||
draw_button(fb, stride_px, (FB_W - bw) / 2, y, bw, bh, COL_INK, COL_INVERSE, "B Continue", 1);
|
||||
@@ -345,7 +487,30 @@ static void paint_alert(u32 *fb, u32 stride_px, void *ctx) {
|
||||
|
||||
void ota_ui_alert(const char *title, const char *line1, const char *line2) {
|
||||
if (!ui_ensure()) return;
|
||||
alert_ctx_t a = {title, line1, line2};
|
||||
alert_ctx_t a = {title, line1, line2, NULL};
|
||||
while (appletMainLoop()) {
|
||||
present(paint_alert, &a);
|
||||
padUpdate(&g_pad);
|
||||
if (padGetButtonsDown(&g_pad) & HidNpadButton_B) break;
|
||||
svcSleepThread(16000000ULL);
|
||||
}
|
||||
ota_ui_shutdown();
|
||||
}
|
||||
|
||||
void ota_ui_alert_error(const char *title, const char *friendly, const char *technical,
|
||||
const char *installed_version) {
|
||||
if (!ui_ensure()) return;
|
||||
char footer[96];
|
||||
snprintf(footer, sizeof(footer), "Playing installed version (v%s).",
|
||||
installed_version && installed_version[0] ? installed_version : "?");
|
||||
char tech[256];
|
||||
if (technical && technical[0]) {
|
||||
ota_ui_sanitize_ascii(technical, tech, sizeof(tech));
|
||||
if (strlen(tech) > 80) tech[80] = '\0';
|
||||
} else {
|
||||
tech[0] = '\0';
|
||||
}
|
||||
alert_ctx_t a = {title, friendly, tech[0] ? tech : NULL, footer};
|
||||
while (appletMainLoop()) {
|
||||
present(paint_alert, &a);
|
||||
padUpdate(&g_pad);
|
||||
@@ -358,13 +523,13 @@ void ota_ui_alert(const char *title, const char *line1, const char *line2) {
|
||||
static void paint_missing(u32 *fb, u32 stride_px, void *ctx) {
|
||||
(void)ctx;
|
||||
draw_chrome(fb, stride_px);
|
||||
int y = 190;
|
||||
int y = TITLE_Y - 10;
|
||||
draw_text_centered(fb, stride_px, y, "Game files missing", COL_YELLOW, 3);
|
||||
y += 52;
|
||||
draw_text_centered(fb, stride_px, y, "Copy the Switch zip onto your microSD,", COL_DETAIL, 2);
|
||||
y += 32;
|
||||
draw_text_centered(fb, stride_px, y, "then open gen1recomp again.", COL_DETAIL, 2);
|
||||
y += 64;
|
||||
y += LINE_TITLE;
|
||||
y = draw_text_wrapped_centered(fb, stride_px, y,
|
||||
"Copy the Switch zip onto your microSD, then open gen1recomp again.",
|
||||
COL_DETAIL, 2, WRAP_MAX_PX);
|
||||
y += LINE_BODY + 16;
|
||||
int bw = 320;
|
||||
int bh = 56;
|
||||
draw_button(fb, stride_px, (FB_W - bw) / 2, y, bw, bh, COL_INK, COL_INVERSE, "+ Exit", 1);
|
||||
@@ -399,6 +564,12 @@ void ota_ui_alert(const char *title, const char *line1, const char *line2) {
|
||||
fprintf(stderr, "[ota_ui] alert: %s / %s / %s\n", title ? title : "", line1 ? line1 : "",
|
||||
line2 ? line2 : "");
|
||||
}
|
||||
void ota_ui_alert_error(const char *title, const char *friendly, const char *technical,
|
||||
const char *installed_version) {
|
||||
fprintf(stderr, "[ota_ui] error: %s / %s / %s / v%s\n", title ? title : "",
|
||||
friendly ? friendly : "", technical ? technical : "",
|
||||
installed_version ? installed_version : "?");
|
||||
}
|
||||
void ota_ui_missing_game(void) { fprintf(stderr, "[ota_ui] missing game\n"); }
|
||||
void ota_ui_shutdown(void) {}
|
||||
|
||||
|
||||
@@ -500,10 +500,19 @@ test('AC-010: Fonte do launcher e Makefile DEVKITPRO existem @spec:AC-010', () =
|
||||
assert.match(main, /gen1recomp-game\.nro|OTA_GAME_NRO_NAME/);
|
||||
assert.match(main, /Quiet by default|stays quiet|LÖVE self-updater stays off|self-updater stays off/i);
|
||||
assert.match(main, /ota_ui_prompt_update|ota_ui_show_progress/);
|
||||
assert.match(main, /ota_ui_alert_error/);
|
||||
assert.match(main, /Step 1\/3: Downloading/);
|
||||
assert.doesNotMatch(main, /\u2026/, 'OTA UI strings must be ASCII (no Unicode ellipsis)');
|
||||
assert.doesNotMatch(main, /ota_ui_alert\([^)]*Install failed/, 'generic install alert removed');
|
||||
assert.doesNotMatch(main, /consoleInit/, 'no terminal console UI');
|
||||
assert.ok(fs.existsSync(path.join(root, 'native/switch-ota-launcher/src/ota_ui.c')));
|
||||
assert.match(read('native/switch-ota-launcher/src/ota_ui.c'), /COL_RAIL_R|rail|FFD600|255,\s*214/);
|
||||
assert.match(read('native/switch-ota-launcher/src/ota_ui.c'), /logo\.png|stb_image/);
|
||||
const otaUi = read('native/switch-ota-launcher/src/ota_ui.c');
|
||||
assert.match(otaUi, /COL_RAIL_R|rail|FFD600|255,\s*214/);
|
||||
assert.match(otaUi, /logo\.png|stb_image/);
|
||||
assert.match(otaUi, /ota_ui_sanitize_ascii/);
|
||||
assert.match(otaUi, /draw_text_wrapped_centered/);
|
||||
assert.match(otaUi, /ota_ui_alert_error/);
|
||||
assert.match(read('native/switch-ota-launcher/src/ota_net.c'), /XFERINFOFUNCTION|xfer_progress/);
|
||||
assert.match(read('native/switch-ota-launcher/Makefile'), /^ROMFS\s*:=/m);
|
||||
|
||||
const mk = read('native/switch-ota-launcher/Makefile');
|
||||
|
||||
Reference in New Issue
Block a user