Add launcher-styled framebuffer UI for Switch OTA prompts.

Replace the console flash with a quiet branded screen (RGB rail, logo, A/B buttons) that only appears when an update needs confirm.
This commit is contained in:
Andrew Quenehen
2026-08-05 02:03:51 -03:00
parent cafa6f3d61
commit 4dd4c5c463
12 changed files with 8644 additions and 168 deletions
+14 -3
View File
@@ -37,6 +37,7 @@ BUILD := build
SOURCES := src
DATA := data
INCLUDES := include
ROMFS := romfs
APP_TITLE := gen1recomp
APP_AUTHOR := bryanthaboi, port by andrewqsantos
@@ -45,6 +46,7 @@ APP_VERSION ?= 0.0.0
# Prefer project Switch icon if present
ICON := $(TOPDIR)/../../assets/switch/icon.jpg
LOGO_SRC := $(TOPDIR)/../../assets/logo/logo.png
#---------------------------------------------------------------------------------
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
@@ -124,12 +126,20 @@ ifeq ($(strip $(NO_NACP)),)
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
endif
.PHONY: $(BUILD) clean all host-test
ifneq ($(ROMFS),)
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
endif
.PHONY: $(BUILD) clean all host-test sync-romfs
#---------------------------------------------------------------------------------
all: $(BUILD)
all: sync-romfs $(BUILD)
$(BUILD):
sync-romfs:
@mkdir -p $(CURDIR)/$(ROMFS)
@cp -f $(LOGO_SRC) $(CURDIR)/$(ROMFS)/logo.png
$(BUILD): sync-romfs
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
@@ -137,6 +147,7 @@ $(BUILD):
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst build-host
@rm -f $(CURDIR)/$(ROMFS)/logo.png
host-test:
@mkdir -p build-host
+6 -5
View File
@@ -1,11 +1,12 @@
# Native Switch OTA launcher
In-console updates for Gen1Recomp on Nintendo Switch. This NRO is the **hbmenu
entry** (`gen1recomp.nro`). It checks GitHub Releases quietly (no console flash
when you are already up to date or offline). Only if a newer release exists
does it prompt to download the same `gen1recomp-*-switch.zip` used for install,
verify SHA-256 from `sha256sums.txt`, replace **both** `gen1recomp-game.nro`
and `gen1recomp.nro` (matching NACP version for hbmenu/Sphaira), then hand off
entry** (`gen1recomp.nro`). It checks GitHub Releases quietly (no UI when you
are already up to date or offline). Only if a newer release exists does it show
a **launcher-style screen** (black + RGB rail + project logo + flat A/B
buttons), download the same `gen1recomp-*-switch.zip` used for install, verify
SHA-256 from `sha256sums.txt`, replace **both** `gen1recomp-game.nro` and
`gen1recomp.nro` (matching NACP version for hbmenu/Sphaira), then hand off
with `envSetNextLoad`.
The LÖVE self-updater (`src/update/Check.lua`) stays **disabled** on NX.
@@ -0,0 +1,33 @@
#ifndef GEN1_OTA_UI_H
#define GEN1_OTA_UI_H
#ifdef __cplusplus
extern "C" {
#endif
/* Framebuffer UI matching the in-game launcher look (black + RGB rail +
* flat semantic buttons). Silent by default — only call when user-facing. */
/* Returns 1 = update, 0 = skip / play installed. */
int ota_ui_prompt_update(const char *installed, const char *latest);
/* Status screen (download / install). Redraws each call. */
void ota_ui_show_status(const char *title, const char *detail);
/* Progress 0..1; detail optional. */
void ota_ui_show_progress(const char *title, const char *detail, float progress01);
/* Error / info + wait for B. */
void ota_ui_alert(const char *title, const char *line1, const char *line2);
/* Missing game install screen; wait for +. */
void ota_ui_missing_game(void);
/* Tear down framebuffer if active. Safe to call when UI never opened. */
void ota_ui_shutdown(void);
#ifdef __cplusplus
}
#endif
#endif
+30 -152
View File
@@ -1,16 +1,17 @@
/*
* gen1recomp Switch OTA launcher
*
* Quiet by default: checks GitHub Releases with no console flash. Only shows
* a short prompt when an update is available. Downloads the same SD zip
* published for install (gen1recomp-*-switch.zip), verifies SHA-256, extracts
* switch/gen1recomp/gen1recomp-game.nro, then envSetNextLoad to the game.
* Never touches pokemon-love2d/. LÖVE self-updater stays off on NX.
* 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>
@@ -31,61 +32,6 @@
#define GAME_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_GAME_NRO_NAME
#define LAUNCHER_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_LAUNCHER_NRO_NAME
#if defined(__SWITCH__)
static int g_ui = 0;
static void ui_begin(void) {
if (g_ui) return;
consoleInit(NULL);
consoleClear();
g_ui = 1;
}
static void ui_end(void) {
if (!g_ui) return;
consoleExit(NULL);
g_ui = 0;
}
static void ui_line(const char *msg) {
ui_begin();
printf("%s\n", msg);
consoleUpdate(NULL);
}
static void ui_blank(void) {
if (!g_ui) return;
consoleClear();
consoleUpdate(NULL);
}
#else
static void ui_begin(void) {}
static void ui_end(void) {}
static void ui_line(const char *msg) { fprintf(stderr, "%s\n", msg); }
static void ui_blank(void) {}
#endif
/* Returns 1 = update, 0 = skip. Only called when UI is already needed. */
static int wait_confirm_or_skip(void) {
#if defined(__SWITCH__)
PadState pad;
padInitializeDefault(&pad);
ui_line("");
ui_line(" A — Update");
ui_line(" B — Play without updating");
while (appletMainLoop()) {
padUpdate(&pad);
u64 k = padGetButtonsDown(&pad);
if (k & HidNpadButton_A) return 1;
if (k & HidNpadButton_B) return 0;
consoleUpdate(NULL);
}
return 0;
#else
return 0;
#endif
}
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));
@@ -114,19 +60,7 @@ static int run_update_flow(const char *install_dir) {
return 0;
}
/* Update available — only now show UI */
{
char msg[160];
ui_blank();
ui_line("");
ui_line(" gen1recomp");
ui_line("");
snprintf(msg, sizeof(msg), " Update available: %s → %s", installed, dec.version);
ui_line(msg);
}
if (!wait_confirm_or_skip()) {
ui_end();
if (!ota_ui_prompt_update(installed, dec.version)) {
return 0;
}
@@ -141,50 +75,27 @@ static int run_update_flow(const char *install_dir) {
mkdir(updates_dir, 0755);
#endif
ui_blank();
ui_line("");
ui_line(" Downloading…");
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) {
ui_line("");
ui_line(" Download failed.");
ui_line(" Playing the installed version.");
#if defined(__SWITCH__)
{
PadState pad;
padInitializeDefault(&pad);
ui_line("");
ui_line(" Press B to continue");
while (appletMainLoop()) {
padUpdate(&pad);
if (padGetButtonsDown(&pad) & HidNpadButton_B) break;
consoleUpdate(NULL);
}
}
#endif
ui_end();
ota_ui_alert("Download failed", "Playing the installed version.", NULL);
return 0;
}
ota_ui_show_progress("Checking update…", "Verifying 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) {
ui_line("");
ui_line(" Could not verify the update.");
ui_line(" Playing the installed version.");
remove(zip_path);
ui_end();
#if defined(__SWITCH__)
svcSleepThread(1200000000ULL);
#endif
ota_ui_alert("Could not verify", "Playing the installed version.", NULL);
return 0;
}
FILE *sf = fopen(sums_path, "rb");
if (!sf) {
remove(zip_path);
ui_end();
ota_ui_shutdown();
return 0;
}
fseek(sf, 0, SEEK_END);
@@ -193,7 +104,7 @@ static int run_update_flow(const char *install_dir) {
char *sums = (char *)malloc((size_t)slen + 1);
if (!sums) {
fclose(sf);
ui_end();
ota_ui_shutdown();
return 0;
}
fread(sums, 1, (size_t)slen, sf);
@@ -204,70 +115,49 @@ static int run_update_flow(const char *install_dir) {
if (ota_fs_sha256_file(zip_path, hex, sizeof(hex)) != 0) {
free(sums);
remove(zip_path);
ui_end();
ota_ui_shutdown();
return 0;
}
ota_verify_t ver;
ota_verify_sha256(dec.asset_name, hex, sums, &ver);
free(sums);
if (!ver.ok) {
ui_line("");
ui_line(" Update file failed checks.");
ui_line(" Playing the installed version.");
remove(zip_path);
ui_end();
#if defined(__SWITCH__)
svcSleepThread(1200000000ULL);
#endif
ota_ui_alert("Update check failed", "Playing the installed version.", NULL);
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);
snprintf(extracted_launcher, sizeof(extracted_launcher), "%s/updates/gen1recomp.nro.verified",
install_dir);
#if defined(__SWITCH__)
ui_line("");
ui_line(" Installing…");
/* Prefer path inside SD zip; fall back to bare name. */
ota_ui_show_progress("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) {
ui_line("");
ui_line(" Install failed.");
ui_line(" Playing the installed version.");
remove(zip_path);
ui_end();
svcSleepThread(1200000000ULL);
ota_ui_alert("Install failed", "Playing the installed version.", NULL);
return 0;
}
}
if (ota_fs_atomic_replace_game(install_dir, extracted, err, sizeof(err)) != 0) {
ui_line("");
ui_line(" Install failed.");
ui_line(" Playing the installed version.");
remove(extracted);
remove(zip_path);
ui_end();
svcSleepThread(1200000000ULL);
ota_ui_alert("Install failed", "Playing the installed version.", NULL);
return 0;
}
remove(extracted);
/* Also replace launcher so hbmenu/Sphaira see the matching NACP version.
* Best-effort: game update already succeeded if this fails. */
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) {
if (ota_fs_atomic_replace_nro(install_dir, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err,
sizeof(err)) == 0) {
remove(extracted_launcher);
} else {
remove(extracted_launcher);
}
(void)ota_fs_atomic_replace_nro(install_dir, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err,
sizeof(err));
remove(extracted_launcher);
}
char vpath[192];
@@ -277,12 +167,15 @@ static int run_update_flow(const char *install_dir) {
fprintf(vf, "%s\n", dec.version);
fclose(vf);
}
ota_ui_show_progress("Ready", "Starting the game…", 1.0f);
svcSleepThread(600000000ULL);
#else
(void)extracted;
(void)extracted_launcher;
#endif
remove(zip_path);
ui_end();
ota_ui_shutdown();
return 0;
}
@@ -303,26 +196,11 @@ int main(int argc, char **argv) {
char err[128];
err[0] = '\0';
if (ota_fs_handoff_to_game(game, err, sizeof(err)) != 0) {
ui_line("");
ui_line(" gen1recomp");
ui_line("");
ui_line(" Game files are missing.");
ui_line(" Copy the Switch zip onto your microSD,");
ui_line(" then open gen1recomp again.");
#if defined(__SWITCH__)
ui_line("");
ui_line(" Press + to exit");
PadState pad;
padInitializeDefault(&pad);
while (appletMainLoop()) {
padUpdate(&pad);
if (padGetButtonsDown(&pad) & HidNpadButton_Plus) break;
consoleUpdate(NULL);
}
#endif
ui_end();
ota_ui_missing_game();
}
ota_ui_shutdown();
#if defined(__SWITCH__)
socketExit();
#endif
+405
View File
@@ -0,0 +1,405 @@
/*
* Switch OTA UI — matches in-game launcher language (Theme.lua):
* black field, RGB version rail, flat yellow/white buttons, white ink.
* Quiet until called. Uses framebuffer + 8x8 font + optional romfs logo.
*/
#include "ota_ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__SWITCH__)
#include <switch.h>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_PNG
#define STBI_NO_THREAD_LOCALS
#include "../third_party/stb_image.h"
#include "../third_party/font8x8_basic.h"
#define FB_W 1280
#define FB_H 720
/* Theme.PAL (0255) → 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)
#define COL_MUTED RGBA8_MAXALPHA(150, 150, 150)
#define COL_INVERSE RGBA8_MAXALPHA(0, 0, 0)
#define COL_YELLOW RGBA8_MAXALPHA(255, 214, 0)
#define COL_GREEN RGBA8_MAXALPHA(0, 255, 140)
#define COL_RAIL_R RGBA8_MAXALPHA(255, 60, 72)
#define COL_RAIL_B RGBA8_MAXALPHA(70, 150, 255)
#define COL_RAIL_G RGBA8_MAXALPHA(255, 203, 5)
#define COL_LINE RGBA8_MAXALPHA(90, 90, 90)
#define COL_RAISED RGBA8_MAXALPHA(20, 20, 20)
static int g_ready = 0;
static Framebuffer g_fb;
static PadState g_pad;
static u32 *g_logo = NULL;
static int g_logo_w = 0, g_logo_h = 0;
static void fill_rect(u32 *fb, u32 stride_px, int x, int y, int w, int h, u32 color) {
if (w <= 0 || h <= 0) return;
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (x + w > FB_W) w = FB_W - x;
if (y + h > FB_H) h = FB_H - y;
if (w <= 0 || h <= 0) return;
for (int row = 0; row < h; row++) {
u32 *line = fb + (y + row) * stride_px + x;
for (int col = 0; col < w; col++) line[col] = color;
}
}
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 = '?';
const char *bits = font8x8_basic[c];
for (int row = 0; row < 8; row++) {
unsigned char line = (unsigned char)bits[row];
for (int col = 0; col < 8; col++) {
if (line & (1 << col)) {
fill_rect(fb, stride_px, x + col * scale, y + row * scale, scale, scale, color);
}
}
}
}
static int text_width(const char *s, int scale) {
if (!s) return 0;
return (int)strlen(s) * 8 * scale;
}
static void draw_text(u32 *fb, u32 stride_px, int x, int y, const char *s, u32 color, int scale) {
if (!s) return;
int cx = x;
for (; *s; s++) {
if (*s == '\n') {
cx = x;
y += 8 * scale + 4;
continue;
}
draw_glyph(fb, stride_px, cx, y, *s, 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);
}
static void draw_rail(u32 *fb, u32 stride_px) {
int h = 6;
int third = FB_W / 3;
fill_rect(fb, stride_px, 0, 0, third, h, COL_RAIL_R);
fill_rect(fb, stride_px, third, 0, third, h, COL_RAIL_B);
fill_rect(fb, stride_px, third * 2, 0, FB_W - third * 2, h, COL_RAIL_G);
}
static void blit_logo(u32 *fb, u32 stride_px, int dst_x, int dst_y, int max_w) {
if (!g_logo || g_logo_w <= 0 || g_logo_h <= 0) return;
int dw = g_logo_w;
int dh = g_logo_h;
if (dw > max_w) {
dh = dh * max_w / dw;
dw = max_w;
}
for (int y = 0; y < dh; y++) {
int sy = y * g_logo_h / dh;
for (int x = 0; x < dw; x++) {
int sx = x * g_logo_w / dw;
u32 px = g_logo[sy * g_logo_w + sx];
u8 a = (px >> 24) & 0xff;
if (a < 16) continue;
int dx = dst_x + x;
int dy = dst_y + y;
if (dx < 0 || dy < 0 || dx >= FB_W || dy >= FB_H) continue;
fb[dy * stride_px + dx] = px | 0xff000000u;
}
}
}
static void load_logo(void) {
if (g_logo) return;
romfsInit();
int w = 0, h = 0, n = 0;
unsigned char *data = stbi_load("romfs:/logo.png", &w, &h, &n, 4);
if (!data || w <= 0 || h <= 0) {
if (data) stbi_image_free(data);
return;
}
g_logo = (u32 *)malloc((size_t)w * (size_t)h * sizeof(u32));
if (!g_logo) {
stbi_image_free(data);
return;
}
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;
g_logo_h = h;
stbi_image_free(data);
}
static int ui_ensure(void) {
if (g_ready) return 1;
NWindow *win = nwindowGetDefault();
if (R_FAILED(framebufferCreate(&g_fb, win, FB_W, FB_H, PIXEL_FORMAT_RGBA_8888, 2))) return 0;
framebufferMakeLinear(&g_fb);
padInitializeDefault(&g_pad);
load_logo();
g_ready = 1;
return 1;
}
void ota_ui_shutdown(void) {
if (!g_ready) return;
framebufferClose(&g_fb);
free(g_logo);
g_logo = NULL;
g_logo_w = g_logo_h = 0;
romfsExit();
g_ready = 0;
}
static void draw_button(u32 *fb, u32 stride_px, int x, int y, int w, int h, u32 fill, u32 ink,
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);
fill_rect(fb, stride_px, x + w + 1, y - 3, 2, h + 6, COL_INK);
} else {
fill_rect(fb, stride_px, x, y, w, 1, COL_LINE);
fill_rect(fb, stride_px, x, y + h - 1, w, 1, COL_LINE);
fill_rect(fb, stride_px, x, y, 1, h, COL_LINE);
fill_rect(fb, stride_px, x + w - 1, y, 1, h, COL_LINE);
}
int scale = 3;
int tw = text_width(label, scale);
int tx = x + (w - tw) / 2;
int ty = y + (h - 8 * scale) / 2;
draw_text(fb, stride_px, tx, ty, label, ink, scale);
}
static void draw_chrome(u32 *fb, u32 stride_px) {
fill_rect(fb, stride_px, 0, 0, FB_W, FB_H, COL_BG);
draw_rail(fb, stride_px);
int logo_max = 320;
int logo_h = g_logo ? (g_logo_h * logo_max / g_logo_w) : 40;
if (logo_h > 90) logo_h = 90;
int logo_y = 48;
if (g_logo) {
int logo_w = logo_max;
if (g_logo_w * logo_h / g_logo_h < logo_max) logo_w = g_logo_w * logo_h / g_logo_h;
blit_logo(fb, stride_px, (FB_W - logo_w) / 2, logo_y, logo_max);
} else {
draw_text_centered(fb, stride_px, logo_y + 16, "gen1recomp", COL_INK, 4);
}
}
static void present(void (*paint)(u32 *fb, u32 stride_px, void *ctx), void *ctx) {
if (!ui_ensure()) return;
u32 stride = 0;
u32 *fb = (u32 *)framebufferBegin(&g_fb, &stride);
u32 stride_px = stride / sizeof(u32);
paint(fb, stride_px, ctx);
framebufferEnd(&g_fb);
}
typedef struct {
const char *installed;
const char *latest;
int focus; /* 0 = Update, 1 = Play */
} prompt_ctx_t;
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;
draw_text_centered(fb, stride_px, y, "Update available", COL_INK, 3);
y += 48;
char line[96];
snprintf(line, sizeof(line), "v%s -> v%s", p->installed ? p->installed : "?",
p->latest ? p->latest : "?");
draw_text_centered(fb, stride_px, y, line, COL_YELLOW, 3);
y += 56;
draw_text_centered(fb, stride_px, y, "Your saves stay on this console.", COL_MUTED, 2);
y += 64;
int bw = 420;
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",
p->focus == 1);
}
int ota_ui_prompt_update(const char *installed, const char *latest) {
if (!ui_ensure()) return 0;
prompt_ctx_t ctx = {installed, latest, 0};
while (appletMainLoop()) {
present(paint_prompt, &ctx);
padUpdate(&g_pad);
u64 k = padGetButtonsDown(&g_pad);
if (k & HidNpadButton_A) {
int do_update = (ctx.focus == 0);
if (!do_update) ota_ui_shutdown();
return do_update;
}
if (k & HidNpadButton_B) {
ota_ui_shutdown();
return 0;
}
if (k & (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_Left | HidNpadButton_Right)) {
ctx.focus = 1 - ctx.focus;
}
svcSleepThread(16000000ULL);
}
ota_ui_shutdown();
return 0;
}
typedef struct {
const char *title;
const char *detail;
float progress;
int show_bar;
} status_ctx_t;
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;
draw_text_centered(fb, stride_px, y, s->title ? s->title : "", COL_INK, 3);
y += 48;
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);
}
}
void ota_ui_show_status(const char *title, const char *detail) {
status_ctx_t s = {title, detail, 0.f, 0};
present(paint_status, &s);
}
void ota_ui_show_progress(const char *title, const char *detail, float progress01) {
status_ctx_t s = {title, detail, progress01, 1};
present(paint_status, &s);
}
typedef struct {
const char *title;
const char *line1;
const char *line2;
} 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;
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;
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);
}
void ota_ui_alert(const char *title, const char *line1, const char *line2) {
if (!ui_ensure()) return;
alert_ctx_t a = {title, line1, line2};
while (appletMainLoop()) {
present(paint_alert, &a);
padUpdate(&g_pad);
if (padGetButtonsDown(&g_pad) & HidNpadButton_B) break;
svcSleepThread(16000000ULL);
}
ota_ui_shutdown();
}
static void paint_missing(u32 *fb, u32 stride_px, void *ctx) {
(void)ctx;
draw_chrome(fb, stride_px);
int y = 190;
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;
int bw = 320;
int bh = 56;
draw_button(fb, stride_px, (FB_W - bw) / 2, y, bw, bh, COL_INK, COL_INVERSE, "+ Exit", 1);
}
void ota_ui_missing_game(void) {
if (!ui_ensure()) return;
while (appletMainLoop()) {
present(paint_missing, NULL);
padUpdate(&g_pad);
if (padGetButtonsDown(&g_pad) & HidNpadButton_Plus) break;
svcSleepThread(16000000ULL);
}
ota_ui_shutdown();
}
#else /* host stub */
int ota_ui_prompt_update(const char *installed, const char *latest) {
fprintf(stderr, "[ota_ui] update %s -> %s (host stub: skip)\n", installed ? installed : "?",
latest ? latest : "?");
return 0;
}
void ota_ui_show_status(const char *title, const char *detail) {
fprintf(stderr, "[ota_ui] %s %s\n", title ? title : "", detail ? detail : "");
}
void ota_ui_show_progress(const char *title, const char *detail, float progress01) {
fprintf(stderr, "[ota_ui] %s %s (%.0f%%)\n", title ? title : "", detail ? detail : "",
progress01 * 100.f);
}
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_missing_game(void) { fprintf(stderr, "[ota_ui] missing game\n"); }
void ota_ui_shutdown(void) {}
#endif
+152
View File
@@ -0,0 +1,152 @@
/**
* 8x8 monochrome bitmap fonts for rendering
* Author: Daniel Hepper <daniel@hepper.net>
*
* License: Public Domain
*
* Based on:
* // Summary: font8x8.h
* // 8x8 monochrome bitmap fonts for rendering
* //
* // Author:
* // Marcel Sondaar
* // International Business Machines (public domain VGA fonts)
* //
* // License:
* // Public Domain
*
* Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm
**/
// Constant: font8x8_basic
// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin)
char font8x8_basic[128][8] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space)
{ 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!)
{ 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (")
{ 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#)
{ 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($)
{ 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%)
{ 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&)
{ 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (')
{ 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (()
{ 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ())
{ 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*)
{ 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,)
{ 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.)
{ 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/)
{ 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0)
{ 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1)
{ 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2)
{ 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3)
{ 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4)
{ 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5)
{ 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6)
{ 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7)
{ 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8)
{ 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9)
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:)
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;)
{ 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<)
{ 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=)
{ 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>)
{ 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?)
{ 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@)
{ 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A)
{ 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B)
{ 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C)
{ 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D)
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E)
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F)
{ 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G)
{ 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H)
{ 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I)
{ 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J)
{ 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K)
{ 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L)
{ 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M)
{ 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N)
{ 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O)
{ 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P)
{ 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q)
{ 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R)
{ 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S)
{ 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T)
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U)
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V)
{ 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W)
{ 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X)
{ 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y)
{ 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z)
{ 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([)
{ 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\)
{ 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (])
{ 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_)
{ 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`)
{ 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a)
{ 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b)
{ 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c)
{ 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d)
{ 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e)
{ 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f)
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g)
{ 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h)
{ 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i)
{ 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j)
{ 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k)
{ 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l)
{ 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m)
{ 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n)
{ 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o)
{ 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p)
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q)
{ 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r)
{ 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s)
{ 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t)
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u)
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v)
{ 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w)
{ 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x)
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y)
{ 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z)
{ 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({)
{ 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|)
{ 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (})
{ 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~)
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F
};
File diff suppressed because it is too large Load Diff