Move Switch OTA launcher sources under ports/switch.

Consolidate the native OTA launcher, bootstrap, and assets into the Switch port tree.
This commit is contained in:
Andrew Quenehen
2026-08-06 10:22:51 -03:00
parent e94d8e32ef
commit 9aa8b4e371
20 changed files with 0 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
#---------------------------------------------------------------------------------
# One-shot OTA bootstrap NRO (copied into the main launcher's romfs).
#---------------------------------------------------------------------------------
.SUFFIXES:
ifeq ($(strip $(DEVKITPRO)),)
.PHONY: all
all:
@echo "DEVKITPRO not set — bootstrap builds with the launcher only."
@false
else
TOPDIR ?= $(CURDIR)
include $(DEVKITPRO)/libnx/switch_rules
TARGET := ota-bootstrap
BUILD := build
SOURCES := .
INCLUDES := include
ROMFS :=
APP_TITLE := gen1recomp OTA
APP_AUTHOR := bryanthaboi, port by andrewqsantos
APP_VERSION := 0.0.0
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
CFLAGS := -g -Wall -O2 -ffunction-sections $(ARCH) $(DEFINES)
CFLAGS += $(INCLUDE) -D__SWITCH__
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lnx
LIBDIRS := $(LIBNX)
ifneq ($(BUILD),$(notdir $(CURDIR)))
export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
export LD := $(CC)
export OFILES := $(CFILES:.c=.o)
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
export APP_ICON := $(LIBNX)/default_icon.jpg
export NROFLAGS += --icon=$(APP_ICON)
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
.PHONY: $(BUILD) clean all
all: $(BUILD)
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf
else
.PHONY: all
DEPENDS := $(OFILES:.o=.d)
all: $(OUTPUT).nro
$(OUTPUT).nro: $(OUTPUT).elf $(OUTPUT).nacp
$(OUTPUT).elf: $(OFILES)
-include $(DEPENDS)
endif
endif
+36
View File
@@ -0,0 +1,36 @@
/*
* Tiny one-shot helper: swap a staged launcher NRO into place, then load the game.
* The main OTA launcher chainloads here because a running NRO cannot replace itself
* on sdmc/FAT.
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#if defined(__SWITCH__)
#include <switch.h>
#endif
#define INSTALL_DIR "sdmc:/switch/gen1recomp"
#define STAGED INSTALL_DIR "/gen1recomp.nro.staged"
#define LAUNCHER INSTALL_DIR "/gen1recomp.nro"
#define GAME INSTALL_DIR "/gen1recomp-game.nro"
int main(int argc, char **argv) {
(void)argc;
(void)argv;
#if defined(__SWITCH__)
remove(LAUNCHER);
if (rename(STAGED, LAUNCHER) != 0) {
return 1;
}
Result rc = envSetNextLoad(GAME, GAME);
if (R_FAILED(rc)) return 1;
return 0;
#else
fprintf(stderr, "ota-bootstrap: host stub (would rename %s -> %s, load %s)\n", STAGED, LAUNCHER,
GAME);
return 0;
#endif
}
+192
View File
@@ -0,0 +1,192 @@
#---------------------------------------------------------------------------------
# gen1recomp Switch native OTA launcher
# Requires: DEVKITPRO + switch-curl, switch-mbedtls, switch-zlib, switch-zziplib
# Host protocol tests: make host-test (no DEVKITPRO needed)
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
# Host-only path when DEVKITPRO is unset
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITPRO)),)
.PHONY: host-test clean-host
host-test:
@mkdir -p build-host
cc -std=c11 -Wall -Wextra -Iinclude -o build-host/test_ota_protocol \
src/ota_protocol.c host/test_ota_protocol.c
./build-host/test_ota_protocol
clean-host:
rm -rf build-host
%:
@echo "DEVKITPRO not set — only 'make host-test' is available."
@echo "Run: bash scripts/switch/install_devkitpro_deps.sh"
@false
else
#---------------------------------------------------------------------------------
TOPDIR ?= $(CURDIR)
include $(DEVKITPRO)/libnx/switch_rules
#---------------------------------------------------------------------------------
TARGET := gen1recomp
BUILD := build
SOURCES := src
DATA := data
INCLUDES := include
ROMFS := romfs
APP_TITLE := gen1recomp
APP_AUTHOR := bryanthaboi, port by andrewqsantos
# Overridable: scripts/switch/build_ota_launcher.sh passes release X.Y.Z
APP_VERSION ?= 0.0.0
# Prefer project Switch icon if present
ICON := $(TOPDIR)/../../assets/switch/icon.jpg
LOGO_RGBA_SRC := $(TOPDIR)/assets/logo.rgba
LOGO_ROMFS := $(CURDIR)/$(ROMFS)/logo.rgba
#---------------------------------------------------------------------------------
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
CFLAGS := -g -Wall -O2 -ffunction-sections $(ARCH) $(DEFINES)
CFLAGS += $(INCLUDE) -D__SWITCH__
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lcurl -lzzip -lmbedtls -lmbedx509 -lmbedcrypto -lz -lnx
LIBDIRS := $(PORTLIBS) $(LIBNX)
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
export LD := $(CC)
else
export LD := $(CXX)
endif
#---------------------------------------------------------------------------------
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SRC)
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
ifeq ($(strip $(ICON)),)
icons := $(wildcard *.jpg)
ifneq (,$(findstring $(TARGET).jpg,$(icons)))
export APP_ICON := $(TOPDIR)/$(TARGET).jpg
else
ifneq (,$(findstring icon.jpg,$(icons)))
export APP_ICON := $(TOPDIR)/icon.jpg
endif
endif
else
ifeq ($(wildcard $(ICON)),)
# fall back to libnx default if project icon missing
export APP_ICON := $(LIBNX)/default_icon.jpg
else
export APP_ICON := $(ICON)
endif
endif
ifeq ($(strip $(NO_ICON)),)
export NROFLAGS += --icon=$(APP_ICON)
endif
ifeq ($(strip $(NO_NACP)),)
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
endif
ifneq ($(ROMFS),)
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
endif
BOOTSTRAP_DIR := $(TOPDIR)/../switch-ota-bootstrap
BOOTSTRAP_ROMFS := $(CURDIR)/$(ROMFS)/ota-bootstrap.nro
.PHONY: $(BUILD) clean all host-test sync-romfs bootstrap-romfs
#---------------------------------------------------------------------------------
all: sync-romfs $(BUILD)
CACERT_URL := https://curl.se/ca/cacert.pem
CACERT_ROMFS := $(CURDIR)/$(ROMFS)/cacert.pem
bootstrap-romfs:
@$(MAKE) --no-print-directory -C $(BOOTSTRAP_DIR) all
@mkdir -p $(CURDIR)/$(ROMFS)
@cp -f $(BOOTSTRAP_DIR)/ota-bootstrap.nro $(BOOTSTRAP_ROMFS)
sync-romfs: bootstrap-romfs
@mkdir -p $(CURDIR)/$(ROMFS)
@[ -f "$(LOGO_RGBA_SRC)" ] || (echo "missing $(LOGO_RGBA_SRC) — run: python3 scripts/switch/bake_ota_logo.py" && exit 1)
@cp -f "$(LOGO_RGBA_SRC)" "$(LOGO_ROMFS)"
@if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \
[ -f $(CACERT_ROMFS) ] || (echo "sync-romfs: failed to fetch cacert.pem" && exit 1); \
fi
$(BUILD): sync-romfs
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst build-host
@rm -f $(CURDIR)/$(ROMFS)/logo.rgba $(CURDIR)/$(ROMFS)/logo.png
host-test:
@mkdir -p build-host
cc -std=c11 -Wall -Wextra -Iinclude -o build-host/test_ota_protocol \
src/ota_protocol.c host/test_ota_protocol.c
./build-host/test_ota_protocol
#---------------------------------------------------------------------------------
else
.PHONY: all
DEPENDS := $(OFILES:.o=.d)
#---------------------------------------------------------------------------------
all: $(OUTPUT).nro
$(OUTPUT).nro: $(OUTPUT).elf $(OUTPUT).nacp
$(OUTPUT).elf: $(OFILES)
-include $(DEPENDS)
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------
endif
+89
View File
@@ -0,0 +1,89 @@
# 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 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 load the game
with `envSetNextLoad`.
The LÖVE self-updater (`src/update/Check.lua`) stays **disabled** on NX.
Wire format (also in Lua): `src/update/SwitchOta.lua`.
NACP icon: `assets/switch/icon.jpg`.
## Layout on microSD
```text
sdmc:/switch/gen1recomp/gen1recomp.nro <- this launcher
sdmc:/switch/gen1recomp/gen1recomp-game.nro <- fused LÖVE game
sdmc:/switch/gen1recomp/version.txt <- installed X.Y.Z
sdmc:/switch/gen1recomp/pokemon-love2d/ <- saves (never touched by OTA)
```
## Host tests (no DEVKITPRO)
```bash
cd native/switch-ota-launcher
make host-test
```
## Switch build (DEVKITPRO)
Needs `DEVKITPRO` with packages roughly:
```bash
(dkp-)pacman -S --needed switch-curl switch-mbedtls switch-zlib switch-zziplib
# or: bash scripts/switch/install_devkitpro_deps.sh
```
```bash
export DEVKITPRO=/opt/devkitpro # typical
cd native/switch-ota-launcher
make
# -> gen1recomp.nro
```
Or from repo root (as part of `--fused`):
```bash
scripts/build_switch.sh --fetch --fused --version X.Y.Z
```
Standalone launcher build:
```bash
scripts/switch/build_ota_launcher.sh
```
Docker fallback uses the same pin as fused builds (`scripts/switch/dkp-docker.image`).
## OTA logo asset
The launcher draws a pre-scaled logo from `romfs:/logo.rgba` (no PNG decoder in
the NRO). The baked blob lives at `assets/logo.rgba` and is copied into romfs at
build time. After changing `assets/logo/logo.png`, regenerate:
```bash
python3 scripts/switch/bake_ota_logo.py
```
Requires Pillow, or on macOS uses `sips` when Pillow is not installed.
## Packaging
`scripts/switch/pack_sd_zip.sh GAME_NRO VERSION OUT_ZIP LAUNCHER_NRO` writes both
NROs into the SD zip. That zip is also what OTA downloads.
Manifest: `scripts/switch/ota_launcher.manifest`.
## Status / known gaps
- Zip extraction uses `switch-zziplib` (`ota_unzip.c`) on device.
- OTA replaces game + launcher from the install zip (NACP versions stay aligned).
The running launcher cannot overwrite its own NRO on sdmc/FAT; a tiny
`ota-bootstrap.nro` (embedded in romfs) chainloads once to swap the staged
launcher, then loads the game.
- HTTPS uses Mozilla CA bundle in romfs (`cacert.pem`, fetched at build time). `ota_net_init()` mounts romfs before the quiet release check.
- Sphaira HOME forwarders cache metadata until reinstalled (see docs/switch-install.md).
- Release runner: `switch-dev` + (`install_devkitpro_deps.sh` **or** Docker)
@@ -0,0 +1,144 @@
/* Host-only unit tests for ota_protocol.c — no libnx. */
#include "ota_protocol.h"
#include <stdio.h>
#include <string.h>
static int g_fail = 0;
static void expect(int cond, const char *msg) {
if (!cond) {
fprintf(stderr, "FAIL: %s\n", msg);
g_fail++;
} else {
printf("PASS: %s\n", msg);
}
}
int main(void) {
expect(ota_compare_semver("1.2.0", "1.1.0") == 1, "semver newer");
expect(ota_compare_semver("1.1.0", "1.1.0") == 0, "semver equal");
expect(ota_compare_semver("1.0.0", "1.1.0") == -1, "semver older");
expect(ota_is_ota_asset_name("gen1recomp-1.5.0-switch.zip"), "ota asset name");
expect(!ota_is_ota_asset_name("gen1recomp-1.5.0-switch-ota.zip"), "reject legacy ota zip name");
expect(!ota_is_ota_asset_name("gen1recomp-1.5.0.love"), "reject love payload");
const char *json =
"{"
"\"tag_name\":\"v1.5.0\","
"\"assets\":["
"{\"name\":\"gen1recomp-1.5.0-switch.zip\","
"\"browser_download_url\":\"https://example/switch.zip\"},"
"{\"name\":\"sha256sums.txt\",\"browser_download_url\":\"https://example/sums\"}"
"]"
"}";
ota_release_t rel;
expect(ota_parse_release(json, &rel) == 1, "parse release");
expect(strcmp(rel.version, "1.5.0") == 0, "release version");
expect(strcmp(rel.asset_name, "gen1recomp-1.5.0-switch.zip") == 0, "release asset");
ota_decision_t d;
ota_decide_update("1.4.0", &rel, &d);
expect(strcmp(d.status, "available") == 0, "decide available");
ota_decide_update("1.5.0", &rel, &d);
expect(strcmp(d.status, "uptodate") == 0, "decide uptodate");
ota_release_t bad;
expect(ota_parse_release("{\"tag_name\":\"v1.5.0\",\"assets\":[]}", &bad) == 0,
"missing ota asset");
expect(strcmp(bad.reason, "missing_ota_asset") == 0, "missing_ota_asset reason");
/* GitHub releases/latest shape: release-level name + fat uploader before browser_download_url. */
const char *github_json =
"{"
"\"tag_name\":\"v0.1.70\","
"\"name\":\"0.1.70\","
"\"assets\":["
"{"
"\"url\":\"https://api.github.com/repos/bryanthaboi/gen1recomp/releases/assets/502823880\","
"\"id\":502823880,"
"\"name\":\"gen1recomp-0.1.70-switch.zip\","
"\"label\":\"\","
"\"uploader\":{"
"\"login\":\"github-actions[bot]\","
"\"id\":41898282,"
"\"node_id\":\"MDM6Qm90NDE4OTgyODI=\","
"\"avatar_url\":\"https://avatars.githubusercontent.com/in/15368?v=4\","
"\"gravatar_id\":\"\","
"\"url\":\"https://api.github.com/users/github-actions%5Bbot%5D\","
"\"html_url\":\"https://github.com/apps/github-actions\","
"\"followers_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/followers\","
"\"following_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}\","
"\"gists_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}\","
"\"starred_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}\","
"\"subscriptions_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/subscriptions\","
"\"organizations_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/orgs\","
"\"repos_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/repos\","
"\"events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}\","
"\"received_events_url\":\"https://api.github.com/users/github-actions%5Bbot%5D/received_events\","
"\"type\":\"Bot\","
"\"user_view_type\":\"public\","
"\"site_admin\":false"
"},"
"\"content_type\":\"application/zip\","
"\"state\":\"uploaded\","
"\"size\":9000573,"
"\"browser_download_url\":\"https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.70/gen1recomp-0.1.70-switch.zip\""
"}"
"]"
"}";
ota_release_t gh;
expect(ota_parse_release(github_json, &gh) == 1, "parse github-shaped release");
expect(strcmp(gh.version, "0.1.70") == 0, "github release version");
expect(strcmp(gh.asset_name, "gen1recomp-0.1.70-switch.zip") == 0, "github release asset");
expect(strcmp(gh.download_url,
"https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.70/gen1recomp-0.1.70-switch.zip") ==
0,
"github release download url");
ota_decide_update("0.1.69", &gh, &d);
expect(strcmp(d.status, "available") == 0, "github release decide 0.1.69->0.1.70");
const char *sums = "abc123 gen1recomp-1.5.0-switch.zip\n";
ota_verify_t v;
ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "abc123", sums, &v);
expect(v.ok == 1, "sha ok");
ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "deadbeef", sums, &v);
expect(v.ok == 0 && strcmp(v.reason, "hash_mismatch") == 0, "sha mismatch");
ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "abc123", "", &v);
expect(v.ok == 0 && strcmp(v.reason, "sum_not_found") == 0, "sha missing sum rejected");
ota_apply_plan_t plan;
ota_plan_atomic_apply("switch/gen1recomp", "/tmp/x", &plan);
expect(strstr(plan.steps[0], "copy_to_part:") != NULL, "plan copy");
expect(strstr(plan.steps[1], "rename:") != NULL, "plan rename");
expect(strstr(plan.steps[2], "copy_to_part:launcher") != NULL, "plan launcher copy");
expect(strstr(plan.steps[3], "rename:") != NULL, "plan launcher rename");
expect(strstr(plan.steps[4], "env_set_next_load:") != NULL, "plan handoff");
expect(strstr(plan.preserve, "pokemon-love2d") != NULL, "preserve saves");
expect(strstr(plan.forbidden_delete, "delete:") != NULL, "forbid delete saves");
expect(strstr(plan.forbidden_direct, "write_direct:") != NULL, "forbid direct write");
ota_offline_t off;
ota_offline_events_t ev = {0};
ev.user_skip = 1;
ota_offline_policy(1.0, &ev, &off);
expect(strcmp(off.action, "play_installed") == 0, "skip plays installed");
ev.user_skip = 0;
ev.network_ok = 0;
ota_offline_policy(1.0, &ev, &off);
expect(strcmp(off.action, "play_installed") == 0, "offline plays installed");
ev.network_ok = 1;
ota_offline_policy(6.0, &ev, &off);
expect(strcmp(off.reason, "timeout") == 0, "timeout 6s");
ota_offline_policy(2.0, &ev, &off);
expect(strcmp(off.action, "keep_checking") == 0, "still checking");
expect(OTA_CHECK_TIMEOUT_SEC == 6, "timeout constant 6");
if (g_fail) {
fprintf(stderr, "%d failure(s)\n", g_fail);
return 1;
}
printf("all ota_protocol host tests passed\n");
return 0;
}
@@ -0,0 +1,38 @@
#ifndef GEN1_OTA_FS_H
#define GEN1_OTA_FS_H
#include "ota_protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Read installed game version from sibling version.txt or NRO nacp-less stamp file.
* Expects switch/gen1recomp/version.txt containing X.Y.Z. Returns 0 on success. */
int ota_fs_read_installed_version(const char *install_dir, char *out, size_t out_len);
/* SHA-256 hex (lowercase) of file. Returns 0 on success. */
int ota_fs_sha256_file(const char *path, char *out_hex, size_t out_len);
/* Apply verified temp payload (extracted game NRO) using atomic plan. Returns 0 on success. */
int ota_fs_atomic_replace_game(const char *install_dir, const char *verified_game_nro,
char *err, size_t err_len);
/* Atomically replace any named NRO under install_dir (copy→.part→rename). */
int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
const char *verified_nro, char *err, size_t err_len);
/* Stage a new launcher + copy romfs bootstrap; chainload bootstrap_out next.
* The running launcher cannot replace its own NRO on sdmc/FAT. */
int ota_fs_stage_launcher_bootstrap(const char *install_dir, const char *verified_launcher,
char *bootstrap_out, size_t bootstrap_out_len, char *err,
size_t err_len);
/* Hand off to game NRO via envSetNextLoad (Switch) or no-op stub (host). */
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,29 @@
#ifndef GEN1_OTA_NET_H
#define GEN1_OTA_NET_H
#include <stddef.h>
#ifdef __cplusplus
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);
/* Mount romfs (CA bundle) and init libcurl. Call before any HTTPS download. */
int ota_net_init(void);
void ota_net_shutdown(void);
/* 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. 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, ota_net_progress_fn progress, void *progress_ud);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,95 @@
#ifndef GEN1_OTA_PROTOCOL_H
#define GEN1_OTA_PROTOCOL_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define OTA_CHECK_TIMEOUT_SEC 6
#define OTA_GAME_NRO_NAME "gen1recomp-game.nro"
#define OTA_LAUNCHER_NRO_NAME "gen1recomp.nro"
#define OTA_SAVE_DIR_NAME "pokemon-love2d"
#define OTA_INSTALL_DIR "switch/gen1recomp"
#define OTA_LAUNCHER_STAGED_SUFFIX ".staged"
#define OTA_BOOTSTRAP_ROMFS "romfs:/ota-bootstrap.nro"
#define OTA_BOOTSTRAP_SD_NAME "ota-bootstrap.nro"
#define OTA_RELEASES_API \
"https://api.github.com/repos/bryanthaboi/gen1recomp/releases/latest"
/* Mirrors src/update/SwitchOta.lua — keep semantics in lockstep. */
int ota_compare_semver(const char *a, const char *b);
int ota_is_ota_asset_name(const char *name);
int ota_version_from_ota_asset(const char *name, char *out, size_t out_len);
typedef struct {
int ok; /* 1 on success */
char reason[64];
char tag[64];
char version[32];
char asset_name[128];
char download_url[512];
} ota_release_t;
int ota_parse_release(const char *json_text, ota_release_t *out);
typedef struct {
char status[32]; /* uptodate | available | error */
char reason[64];
char version[32];
char asset_name[128];
char download_url[512];
} ota_decision_t;
void ota_decide_update(const char *installed_version, const ota_release_t *release,
ota_decision_t *out);
/* sums: newline-separated sha256sums.txt body */
int ota_lookup_sum(const char *sums_text, const char *asset_name, char *out_hex,
size_t out_len);
typedef struct {
int ok;
char reason[64];
} ota_verify_t;
void ota_verify_sha256(const char *asset_name, const char *actual_hex,
const char *sums_text, ota_verify_t *out);
typedef struct {
char steps[5][520]; /* human-readable ops */
char preserve[256];
char forbidden_delete[256];
char forbidden_direct[256];
char part_path[256];
char game_nro[240];
char launcher_nro[240];
char launcher_part[256];
char next_load[240];
} ota_apply_plan_t;
void ota_plan_atomic_apply(const char *install_dir, const char *verified_temp,
ota_apply_plan_t *out);
typedef struct {
char action[32]; /* play_installed | keep_checking */
char reason[64];
char message[128];
} ota_offline_t;
typedef struct {
int user_skip;
int network_ok; /* 0 = offline/fail, 1 = ok, -1 = unknown */
int api_error;
} ota_offline_events_t;
void ota_offline_policy(double elapsed_sec, const ota_offline_events_t *events,
ota_offline_t *out);
#ifdef __cplusplus
}
#endif
#endif /* GEN1_OTA_PROTOCOL_H */
@@ -0,0 +1,37 @@
#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);
/* 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);
/* Tear down framebuffer if active. Safe to call when UI never opened. */
void ota_ui_shutdown(void);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,20 @@
#ifndef GEN1_OTA_UNZIP_H
#define GEN1_OTA_UNZIP_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Extract member_name from zip_path into dest_path.
* Tries exact member_name, then common prefixes (switch/gen1recomp/).
* Returns 0 on success. */
int ota_unzip_extract_file(const char *zip_path, const char *member_name, const char *dest_path,
char *err, size_t err_len);
#ifdef __cplusplus
}
#endif
#endif
+281
View File
@@ -0,0 +1,281 @@
/*
* 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;
}
+177
View File
@@ -0,0 +1,177 @@
#include "ota_fs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__SWITCH__)
#include <switch.h>
#include <mbedtls/sha256.h>
#include <sys/stat.h>
#include <unistd.h>
#else
#include <unistd.h>
#endif
int ota_fs_read_installed_version(const char *install_dir, char *out, size_t out_len) {
if (!out || out_len == 0) return -1;
out[0] = '\0';
char path[256];
snprintf(path, sizeof(path), "%s/version.txt", install_dir ? install_dir : OTA_INSTALL_DIR);
FILE *fp = fopen(path, "rb");
if (!fp) return -1;
if (!fgets(out, (int)out_len, fp)) {
fclose(fp);
return -1;
}
fclose(fp);
/* trim */
size_t n = strlen(out);
while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ')) {
out[--n] = '\0';
}
return n > 0 ? 0 : -1;
}
int ota_fs_sha256_file(const char *path, char *out_hex, size_t out_len) {
if (!path || !out_hex || out_len < 65) return -1;
out_hex[0] = '\0';
FILE *fp = fopen(path, "rb");
if (!fp) return -1;
#if defined(__SWITCH__)
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
unsigned char buf[8192];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
mbedtls_sha256_update(&ctx, buf, n);
}
fclose(fp);
unsigned char digest[32];
mbedtls_sha256_finish(&ctx, digest);
mbedtls_sha256_free(&ctx);
for (int i = 0; i < 32; i++) snprintf(out_hex + i * 2, out_len - (size_t)(i * 2), "%02x", digest[i]);
return 0;
#else
/* Host: shell out to shasum/sha256sum for the host test path if needed.
* Protocol host tests do not call this; return not-implemented. */
fclose(fp);
(void)out_len;
return -1;
#endif
}
static int copy_file(const char *from, const char *to) {
FILE *in = fopen(from, "rb");
if (!in) return -1;
FILE *out = fopen(to, "wb");
if (!out) {
fclose(in);
return -1;
}
char buf[8192];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
if (fwrite(buf, 1, n, out) != n) {
fclose(in);
fclose(out);
return -1;
}
}
fclose(in);
fclose(out);
return 0;
}
int ota_fs_atomic_replace_game(const char *install_dir, const char *verified_game_nro,
char *err, size_t err_len) {
return ota_fs_atomic_replace_nro(install_dir, OTA_GAME_NRO_NAME, verified_game_nro, err,
err_len);
}
int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
const char *verified_nro, char *err, size_t err_len) {
if (!nro_name || !*nro_name || !verified_nro || !*verified_nro) {
if (err && err_len) snprintf(err, err_len, "missing nro paths");
return -1;
}
char dest[240];
char part[256];
snprintf(dest, sizeof(dest), "%s/%s", install_dir ? install_dir : OTA_INSTALL_DIR, nro_name);
snprintf(part, sizeof(part), "%s.part", dest);
if (copy_file(verified_nro, part) != 0) {
if (err && err_len) snprintf(err, err_len, "copy to .part failed (%s)", nro_name);
return -1;
}
/* sdmc/FAT (Switch) and Windows do not replace an existing dest on rename. */
remove(dest);
if (rename(part, dest) != 0) {
if (err && err_len) snprintf(err, err_len, "rename .part -> %s failed", nro_name);
remove(part);
return -1;
}
return 0;
}
int ota_fs_stage_launcher_bootstrap(const char *install_dir, const char *verified_launcher,
char *bootstrap_out, size_t bootstrap_out_len, char *err,
size_t err_len) {
if (!verified_launcher || !*verified_launcher) {
if (err && err_len) snprintf(err, err_len, "missing staged launcher path");
return -1;
}
if (!bootstrap_out || bootstrap_out_len == 0) {
if (err && err_len) snprintf(err, err_len, "missing bootstrap output buffer");
return -1;
}
bootstrap_out[0] = '\0';
const char *base = install_dir ? install_dir : OTA_INSTALL_DIR;
char staged[256];
char bootstrap[256];
char updates[192];
snprintf(staged, sizeof(staged), "%s/%s%s", base, OTA_LAUNCHER_NRO_NAME,
OTA_LAUNCHER_STAGED_SUFFIX);
snprintf(updates, sizeof(updates), "%s/updates", base);
snprintf(bootstrap, sizeof(bootstrap), "%s/updates/%s", base, OTA_BOOTSTRAP_SD_NAME);
remove(staged);
if (copy_file(verified_launcher, staged) != 0) {
if (err && err_len) snprintf(err, err_len, "stage launcher failed");
return -1;
}
#if defined(__SWITCH__)
mkdir(updates, 0755);
#endif
if (copy_file(OTA_BOOTSTRAP_ROMFS, bootstrap) != 0) {
remove(staged);
if (err && err_len) snprintf(err, err_len, "extract bootstrap failed");
return -1;
}
snprintf(bootstrap_out, bootstrap_out_len, "%s", bootstrap);
return 0;
}
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len) {
if (!game_nro_path || !*game_nro_path) {
if (err && err_len) snprintf(err, err_len, "missing game path");
return -1;
}
#if defined(__SWITCH__)
/* argv for next load: empty args string is fine */
Result rc = envSetNextLoad(game_nro_path, game_nro_path);
if (R_FAILED(rc)) {
if (err && err_len) snprintf(err, err_len, "envSetNextLoad failed: 0x%x", rc);
return -1;
}
return 0;
#else
if (err && err_len)
snprintf(err, err_len, "handoff stub (host): would envSetNextLoad %s", game_nro_path);
return 0; /* host stub succeeds for flow tests */
#endif
}
+185
View File
@@ -0,0 +1,185 @@
#include "ota_net.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__SWITCH__)
#include <curl/curl.h>
#include <switch.h>
#endif
#if defined(__SWITCH__)
#define OTA_CA_BUNDLE "romfs:/cacert.pem"
static int g_net_ready = 0;
static int ota_ca_bundle_ready(void) {
FILE *f = fopen(OTA_CA_BUNDLE, "rb");
if (!f) return 0;
fclose(f);
return 1;
}
int ota_net_init(void) {
if (g_net_ready) return 0;
if (R_FAILED(romfsInit())) return -1;
if (!ota_ca_bundle_ready()) return -1;
if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return -1;
g_net_ready = 1;
return 0;
}
void ota_net_shutdown(void) {
if (!g_net_ready) return;
curl_global_cleanup();
romfsExit();
g_net_ready = 0;
}
static void ota_net_configure_tls(CURL *curl) {
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
curl_easy_setopt(curl, CURLOPT_CAINFO, OTA_CA_BUNDLE);
}
struct mem_buf {
char *data;
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;
char *p = (char *)realloc(m->data, m->len + n + 1);
if (!p) return 0;
m->data = p;
memcpy(m->data + m->len, ptr, n);
m->len += n;
m->data[m->len] = '\0';
return n;
}
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;
}
#else
int ota_net_init(void) { return 0; }
void ota_net_shutdown(void) {}
#endif
int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t *out_len,
char *err, size_t err_len) {
if (out) *out = NULL;
if (out_len) *out_len = 0;
if (!url || !out) {
if (err && err_len) snprintf(err, err_len, "bad args");
return -1;
}
#if defined(__SWITCH__)
CURL *curl = curl_easy_init();
if (!curl) {
if (err && err_len) snprintf(err, err_len, "curl_easy_init failed");
return -1;
}
struct mem_buf mem = {0};
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "gen1recomp-switch-ota");
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_mem);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
ota_net_configure_tls(curl);
CURLcode rc = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (rc != CURLE_OK) {
free(mem.data);
if (err && err_len) snprintf(err, err_len, "%s", curl_easy_strerror(rc));
return -1;
}
*out = mem.data;
if (out_len) *out_len = mem.len;
return 0;
#else
(void)timeout_ms;
if (err && err_len)
snprintf(err, err_len, "ota_net_download_buffer only available on __SWITCH__");
return -1;
#endif
}
int ota_net_download_file(const char *url, const char *path, long timeout_ms, char *err,
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;
}
#if defined(__SWITCH__)
FILE *fp = fopen(path, "wb");
if (!fp) {
if (err && err_len) snprintf(err, err_len, "fopen failed: %s", path);
return -1;
}
CURL *curl = curl_easy_init();
if (!curl) {
fclose(fp);
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");
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
ota_net_configure_tls(curl);
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);
if (rc != CURLE_OK) {
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;
#endif
}
@@ -0,0 +1,329 @@
#include "ota_protocol.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int major;
int minor;
int patch;
int ok;
} semver_t;
static void set_reason(char *buf, size_t n, const char *r) {
if (!buf || n == 0) return;
snprintf(buf, n, "%s", r ? r : "");
}
static int parse_semver(const char *s, semver_t *out) {
memset(out, 0, sizeof(*out));
if (!s || !*s) return 0;
if (s[0] == 'v' || s[0] == 'V') s++;
int maj = 0, min = 0, pat = 0;
char trail = 0;
if (sscanf(s, "%d.%d.%d%c", &maj, &min, &pat, &trail) != 3) return 0;
if (maj < 0 || min < 0 || pat < 0) return 0;
out->major = maj;
out->minor = min;
out->patch = pat;
out->ok = 1;
return 1;
}
int ota_compare_semver(const char *a, const char *b) {
semver_t pa, pb;
int oa = parse_semver(a, &pa);
int ob = parse_semver(b, &pb);
if (!oa && !ob) return 0;
if (!oa) return -1;
if (!ob) return 1;
if (pa.major != pb.major) return pa.major < pb.major ? -1 : 1;
if (pa.minor != pb.minor) return pa.minor < pb.minor ? -1 : 1;
if (pa.patch != pb.patch) return pa.patch < pb.patch ? -1 : 1;
return 0;
}
int ota_is_ota_asset_name(const char *name) {
int maj = 0, min = 0, pat = 0;
if (!name) return 0;
/* Unified SD/OTA asset: gen1recomp-X.Y.Z-switch.zip */
size_t len = strlen(name);
const char *suffix = "-switch.zip";
size_t slen = strlen(suffix);
if (len <= slen) return 0;
if (strcmp(name + len - slen, suffix) != 0) return 0;
if (strncmp(name, "gen1recomp-", 11) != 0) return 0;
if (sscanf(name + 11, "%d.%d.%d", &maj, &min, &pat) != 3) return 0;
char rebuilt[128];
snprintf(rebuilt, sizeof(rebuilt), "gen1recomp-%d.%d.%d-switch.zip", maj, min, pat);
return strcmp(name, rebuilt) == 0;
}
int ota_version_from_ota_asset(const char *name, char *out, size_t out_len) {
int maj = 0, min = 0, pat = 0;
if (!out || out_len == 0) return 0;
out[0] = '\0';
if (!ota_is_ota_asset_name(name)) return 0;
if (sscanf(name + 11, "%d.%d.%d", &maj, &min, &pat) != 3) return 0;
snprintf(out, out_len, "%d.%d.%d", maj, min, pat);
return 1;
}
static const char *find_json_string(const char *json, const char *key, char *out, size_t out_len) {
char pattern[128];
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
const char *p = strstr(json, pattern);
if (!p) return NULL;
p = strchr(p + strlen(pattern), ':');
if (!p) return NULL;
p++;
while (*p && isspace((unsigned char)*p)) p++;
if (*p != '"') return NULL;
p++;
size_t i = 0;
while (*p && *p != '"' && i + 1 < out_len) {
if (*p == '\\' && p[1]) {
p++;
out[i++] = *p++;
continue;
}
out[i++] = *p++;
}
out[i] = '\0';
return out;
}
/* Opening { of the JSON object that contains pos (walk backward). */
static const char *find_json_object_start(const char *pos, const char *json_start) {
if (!pos || !json_start || pos < json_start) return NULL;
int depth = 0;
const char *p = pos;
while (p >= json_start) {
if (*p == '}') depth++;
else if (*p == '{') {
if (depth == 0) return p;
depth--;
}
p--;
}
return NULL;
}
/* Pointer just past the closing } of the object that starts at object_start. */
static const char *find_json_object_end(const char *object_start) {
if (!object_start || *object_start != '{') return NULL;
int depth = 1;
const char *p = object_start + 1;
while (*p) {
if (*p == '{') depth++;
else if (*p == '}') {
depth--;
if (depth == 0) return p + 1;
}
p++;
}
return NULL;
}
int ota_parse_release(const char *json_text, ota_release_t *out) {
memset(out, 0, sizeof(*out));
if (!json_text || !*json_text) {
set_reason(out->reason, sizeof(out->reason), "empty_json");
return 0;
}
if (!find_json_string(json_text, "tag_name", out->tag, sizeof(out->tag))) {
set_reason(out->reason, sizeof(out->reason), "missing_tag");
return 0;
}
semver_t sv;
if (!parse_semver(out->tag, &sv)) {
set_reason(out->reason, sizeof(out->reason), "bad_tag");
return 0;
}
snprintf(out->version, sizeof(out->version), "%d.%d.%d", sv.major, sv.minor, sv.patch);
/* Scan for OTA asset name then browser_download_url inside the same asset object. */
const char *cursor = json_text;
while ((cursor = strstr(cursor, "\"name\"")) != NULL) {
char name[128];
if (!find_json_string(cursor, "name", name, sizeof(name))) {
cursor += 6;
continue;
}
if (!ota_is_ota_asset_name(name)) {
cursor += 6;
continue;
}
const char *asset_start = find_json_object_start(cursor, json_text);
const char *asset_end =
asset_start ? find_json_object_end(asset_start) : NULL;
if (!asset_start || !asset_end || asset_end <= cursor) {
cursor += 6;
continue;
}
char url[512];
const char *u = NULL;
const char *scan = cursor;
while (scan < asset_end &&
(scan = strstr(scan, "\"browser_download_url\"")) != NULL && scan < asset_end) {
if (find_json_string(scan, "browser_download_url", url, sizeof(url))) {
u = url;
break;
}
scan += 21;
}
if (!u || !*u) {
cursor += 6;
continue;
}
snprintf(out->asset_name, sizeof(out->asset_name), "%s", name);
snprintf(out->download_url, sizeof(out->download_url), "%s", u);
out->ok = 1;
return 1;
}
set_reason(out->reason, sizeof(out->reason), "missing_ota_asset");
return 0;
}
void ota_decide_update(const char *installed_version, const ota_release_t *release,
ota_decision_t *out) {
memset(out, 0, sizeof(*out));
semver_t inst;
if (!parse_semver(installed_version, &inst)) {
snprintf(out->status, sizeof(out->status), "error");
set_reason(out->reason, sizeof(out->reason), "bad_installed_version");
return;
}
if (!release || !release->ok) {
snprintf(out->status, sizeof(out->status), "error");
set_reason(out->reason, sizeof(out->reason), "bad_release");
return;
}
if (ota_compare_semver(release->version, installed_version) <= 0) {
snprintf(out->status, sizeof(out->status), "uptodate");
snprintf(out->version, sizeof(out->version), "%s", installed_version);
return;
}
snprintf(out->status, sizeof(out->status), "available");
snprintf(out->version, sizeof(out->version), "%s", release->version);
snprintf(out->asset_name, sizeof(out->asset_name), "%s", release->asset_name);
snprintf(out->download_url, sizeof(out->download_url), "%s", release->download_url);
}
int ota_lookup_sum(const char *sums_text, const char *asset_name, char *out_hex, size_t out_len) {
if (out_hex && out_len) out_hex[0] = '\0';
if (!sums_text || !asset_name || !out_hex || out_len < 65) return 0;
const char *p = sums_text;
while (*p) {
char hex[96];
char name[256];
int n = 0;
if (sscanf(p, "%95s %255s%n", hex, name, &n) >= 2 && n > 0) {
const char *nm = name;
if (nm[0] == '*') nm++;
if (strncmp(nm, "./", 2) == 0) nm += 2;
if (strcmp(nm, asset_name) == 0) {
/* normalize hex to lowercase */
size_t i;
for (i = 0; hex[i] && i + 1 < out_len; i++)
out_hex[i] = (char)tolower((unsigned char)hex[i]);
out_hex[i] = '\0';
return 1;
}
p += n;
while (*p && *p != '\n') p++;
if (*p == '\n') p++;
continue;
}
while (*p && *p != '\n') p++;
if (*p == '\n') p++;
}
return 0;
}
void ota_verify_sha256(const char *asset_name, const char *actual_hex, const char *sums_text,
ota_verify_t *out) {
memset(out, 0, sizeof(*out));
if (!asset_name || !*asset_name) {
set_reason(out->reason, sizeof(out->reason), "bad_asset_name");
return;
}
if (!sums_text) {
set_reason(out->reason, sizeof(out->reason), "missing_sums");
return;
}
char expected[96];
if (!ota_lookup_sum(sums_text, asset_name, expected, sizeof(expected))) {
set_reason(out->reason, sizeof(out->reason), "sum_not_found");
return;
}
if (!actual_hex || !*actual_hex) {
set_reason(out->reason, sizeof(out->reason), "missing_actual_hash");
return;
}
char actual[96];
size_t i;
for (i = 0; actual_hex[i] && i + 1 < sizeof(actual); i++)
actual[i] = (char)tolower((unsigned char)actual_hex[i]);
actual[i] = '\0';
if (strcmp(actual, expected) != 0) {
set_reason(out->reason, sizeof(out->reason), "hash_mismatch");
return;
}
out->ok = 1;
}
void ota_plan_atomic_apply(const char *install_dir, const char *verified_temp,
ota_apply_plan_t *out) {
memset(out, 0, sizeof(*out));
if (!install_dir || !*install_dir) install_dir = OTA_INSTALL_DIR;
if (!verified_temp) verified_temp = "";
snprintf(out->game_nro, sizeof(out->game_nro), "%s/%s", install_dir, OTA_GAME_NRO_NAME);
snprintf(out->part_path, sizeof(out->part_path), "%s.part", out->game_nro);
snprintf(out->launcher_nro, sizeof(out->launcher_nro), "%s/%s", install_dir,
OTA_LAUNCHER_NRO_NAME);
snprintf(out->launcher_part, sizeof(out->launcher_part), "%s.part", out->launcher_nro);
snprintf(out->next_load, sizeof(out->next_load), "%s", out->game_nro);
/* verified_temp is the game NRO; launcher replace uses a sibling verified path */
snprintf(out->steps[0], sizeof(out->steps[0]), "copy_to_part:%s->%s", verified_temp,
out->part_path);
snprintf(out->steps[1], sizeof(out->steps[1]), "rename:%s->%s", out->part_path, out->game_nro);
snprintf(out->steps[2], sizeof(out->steps[2]), "copy_to_part:launcher->%s", out->launcher_part);
snprintf(out->steps[3], sizeof(out->steps[3]), "rename:%s->%s", out->launcher_part,
out->launcher_nro);
snprintf(out->steps[4], sizeof(out->steps[4]), "env_set_next_load:%s", out->game_nro);
snprintf(out->preserve, sizeof(out->preserve), "%s/%s", install_dir, OTA_SAVE_DIR_NAME);
snprintf(out->forbidden_delete, sizeof(out->forbidden_delete), "delete:%s/%s", install_dir,
OTA_SAVE_DIR_NAME);
snprintf(out->forbidden_direct, sizeof(out->forbidden_direct), "write_direct:%s", out->game_nro);
}
void ota_offline_policy(double elapsed_sec, const ota_offline_events_t *events, ota_offline_t *out) {
memset(out, 0, sizeof(*out));
ota_offline_events_t ev = {0, -1, 0};
if (events) ev = *events;
if (ev.user_skip) {
snprintf(out->action, sizeof(out->action), "play_installed");
set_reason(out->reason, sizeof(out->reason), "user_skip");
snprintf(out->message, sizeof(out->message), "update skipped");
return;
}
if (ev.api_error || ev.network_ok == 0) {
snprintf(out->action, sizeof(out->action), "play_installed");
set_reason(out->reason, sizeof(out->reason), "offline_or_error");
snprintf(out->message, sizeof(out->message),
"offline or update check failed — play installed version");
return;
}
if (elapsed_sec >= (double)OTA_CHECK_TIMEOUT_SEC) {
snprintf(out->action, sizeof(out->action), "play_installed");
set_reason(out->reason, sizeof(out->reason), "timeout");
snprintf(out->message, sizeof(out->message), "update check timed out after %ds",
OTA_CHECK_TIMEOUT_SEC);
return;
}
snprintf(out->action, sizeof(out->action), "keep_checking");
set_reason(out->reason, sizeof(out->reason), "in_flight");
}
+595
View File
@@ -0,0 +1,595 @@
/*
* 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>
#include "../third_party/font8x8_basic.h"
#define OTA_LOGO_ROMFS "romfs:/logo.rgba"
#define FB_W 1280
#define FB_H 720
#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)
#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;
}
}
/* 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) return;
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;
char buf[512];
ota_ui_sanitize_ascii(s, buf, sizeof(buf));
int cx = x;
for (const char *p = buf; *p; p++) {
if (*p == '\n') {
cx = x;
y += 8 * scale + 4;
continue;
}
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) {
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) {
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;
FILE *fp = fopen(OTA_LOGO_ROMFS, "rb");
if (!fp) return;
unsigned char header[8];
if (fread(header, 1, sizeof(header), fp) != sizeof(header)) {
fclose(fp);
return;
}
int w = (int)(header[0] | (header[1] << 8) | (header[2] << 16) | (header[3] << 24));
int h = (int)(header[4] | (header[5] << 8) | (header[6] << 16) | (header[7] << 24));
if (w <= 0 || h <= 0 || w > 2048 || h > 2048) {
fclose(fp);
return;
}
size_t nbytes = (size_t)w * (size_t)h * 4u;
unsigned char *data = (unsigned char *)malloc(nbytes);
if (!data) {
fclose(fp);
return;
}
if (fread(data, 1, nbytes, fp) != nbytes) {
free(data);
fclose(fp);
return;
}
fclose(fp);
g_logo = (u32 *)malloc((size_t)w * (size_t)h * sizeof(u32));
if (!g_logo) {
free(data);
return;
}
for (int i = 0; i < w * h; i++) {
unsigned char *p = data + (size_t)i * 4u;
g_logo[i] = RGBA8(p[0], p[1], p[2], p[3]);
}
free(data);
g_logo_w = w;
g_logo_h = h;
}
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;
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) {
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;
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, lbl, 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 = TITLE_Y - 20;
draw_text_centered(fb, stride_px, y, "Update available", COL_INK, 3);
y += LINE_TITLE;
char line[96];
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 += LINE_TITLE + 8;
draw_text_centered(fb, stride_px, y, "Your saves stay on this console.", COL_MUTED, 2);
y += LINE_BODY + 28;
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 update",
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_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 = TITLE_Y;
draw_text_centered(fb, stride_px, y, s->title ? s->title : "", COL_INK, 3);
y += LINE_TITLE;
if (s->detail && s->detail[0]) {
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) {
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;
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 = TITLE_Y;
draw_text_centered(fb, stride_px, y, a->title ? a->title : "", COL_YELLOW, 3);
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);
}
void ota_ui_alert(const char *title, const char *line1, const char *line2) {
if (!ui_ensure()) return;
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);
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 = TITLE_Y - 10;
draw_text_centered(fb, stride_px, y, "Game files missing", COL_YELLOW, 3);
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);
}
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_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) {}
#endif
+89
View File
@@ -0,0 +1,89 @@
#include "ota_unzip.h"
#include <stdio.h>
#include <string.h>
#if defined(__SWITCH__)
#include <fcntl.h>
#include <zzip/zzip.h>
#endif
#if defined(__SWITCH__)
static int extract_member(ZZIP_DIR *dir, const char *member, const char *dest_path, char *err,
size_t err_len) {
ZZIP_FILE *zf = zzip_file_open(dir, member, O_RDONLY);
if (!zf) return 1; /* not found / cannot open */
FILE *out = fopen(dest_path, "wb");
if (!out) {
zzip_file_close(zf);
if (err && err_len) snprintf(err, err_len, "fopen dest failed: %s", dest_path);
return -1;
}
char buf[8192];
zzip_ssize_t n;
while ((n = zzip_file_read(zf, buf, sizeof(buf))) > 0) {
if ((zzip_ssize_t)fwrite(buf, 1, (size_t)n, out) != n) {
fclose(out);
zzip_file_close(zf);
if (err && err_len) snprintf(err, err_len, "fwrite failed");
return -1;
}
}
fclose(out);
zzip_file_close(zf);
if (n < 0) {
if (err && err_len) snprintf(err, err_len, "zzip_file_read failed");
return -1;
}
return 0;
}
#endif
int ota_unzip_extract_file(const char *zip_path, const char *member_name, const char *dest_path,
char *err, size_t err_len) {
if (!zip_path || !member_name || !dest_path) {
if (err && err_len) snprintf(err, err_len, "bad args");
return -1;
}
#if defined(__SWITCH__)
zzip_error_t zerr = ZZIP_NO_ERROR;
ZZIP_DIR *dir = zzip_dir_open(zip_path, &zerr);
if (!dir) {
if (err && err_len) snprintf(err, err_len, "zzip_dir_open failed (%d): %s", (int)zerr, zip_path);
return -1;
}
char alt1[192];
char alt2[192];
snprintf(alt1, sizeof(alt1), "switch/gen1recomp/%s", member_name);
snprintf(alt2, sizeof(alt2), "./%s", member_name);
const char *candidates[] = {member_name, alt1, alt2, NULL};
int rc = -1;
if (err && err_len) err[0] = '\0';
for (int i = 0; candidates[i]; i++) {
int t = extract_member(dir, candidates[i], dest_path, err, err_len);
if (t == 0) {
rc = 0;
break;
}
if (t < 0) {
rc = -1;
break;
}
}
if (rc != 0 && err && err_len && !err[0]) {
snprintf(err, err_len, "member not found in zip: %s", member_name);
}
zzip_dir_close(dir);
return rc;
#else
(void)zip_path;
(void)member_name;
(void)dest_path;
if (err && err_len) snprintf(err, err_len, "ota_unzip only available on __SWITCH__");
return -1;
#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
};