v2.9.7: Shop - Auswahl CIA oder 3DSX, Katalog-Leser ohne Pufferüberlauf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-10-19 18:40:00 +02:00
co-authored by Claude Opus 5.5
parent 93316ec2c9
commit f05fd2fc16
4 changed files with 126 additions and 34 deletions
+35 -5
View File
@@ -57,6 +57,10 @@ static const char *SORT_NAMES[] = { "nach Name", "beliebteste zuerst", "neueste
typedef struct { float x, y, w, h; } Box;
static const Box backBox = { 6, 8, 30, 46 };
static const Box actionBox = { 12, 70, 296, 40 }, actionBoxNarrow = { 12, 70, 208, 40 }, removeBox = { 226, 70, 82, 40 };
static const Box kindBox = { 206, 116, 102, 26 }; // Umschalter CIA | 3DSX (links / rechts)
// Ist die gewählte Variante (CIA/3DSX) eine andere als die installierte?
static bool other_variant(const App *a) { return a->installed && strcmp(a->kind, a->instKind) != 0; }
static C2D_TextBuf textBuf, measureBuf; // measureBuf nur zum Messen (wird jedes Mal geleert)
static C2D_SpriteSheet sheets[8];
@@ -359,7 +363,8 @@ static void draw_top_detail(const App *a, int shot)
{
draw_hero(a, a->shots ? shot : -1);
char right[48];
snprintf(right, sizeof(right), a->shots > 1 ? "L / R: Bilder B: Zurück" : "B: Zurück");
snprintf(right, sizeof(right), "%s%sB: Zurück", a->shots > 1 ? "L/R: Bilder " : "",
app_has_both(a) ? "X: CIA/3DSX " : "");
draw_top_bar(a->title, right);
// Punkte für die Screenshots
if (a->shots > 1) {
@@ -446,8 +451,11 @@ static void draw_bottom_detail(const App *a, int armed, int descScroll)
AppStatus s = app_status(a);
bool running = job_state() == JOB_RUNNING;
Box b = s == APP_NEW ? actionBox : actionBoxNarrow;
const char *label = s == APP_NEW ? "Kostenlos herunterladen" : s == APP_UPDATE ? "Aktualisieren"
: strcmp(a->kind, "cia") == 0 ? "Öffnen" : "Neu laden";
bool switchKind = other_variant(a);
const char *label = s == APP_NEW ? "Kostenlos herunterladen"
: switchKind ? (strcmp(a->kind, "cia") == 0 ? "Als CIA installieren" : "Als 3DSX installieren")
: s == APP_UPDATE ? "Aktualisieren" : strcmp(a->kind, "cia") == 0 ? "Öffnen" : "Neu laden";
if (switchKind) s = APP_UPDATE; // wie ein Update: roter Knopf, Bestätigung
u32 col = s == APP_INSTALLED ? COL_GREEN : COL_RED;
if (armed == 0) { label = s == APP_NEW ? "Nochmal tippen: Laden" : "Nochmal tippen"; col = COL_AMBER; }
char progress[48];
@@ -473,13 +481,25 @@ static void draw_bottom_detail(const App *a, int armed, int descScroll)
// Infos
char size[16];
size_text(size, sizeof(size), a->size);
snprintf(line, sizeof(line), "%s · %s · %s", a->version[0] ? a->version : "ohne Version", size, category_name(a->category));
char info1[96];
snprintf(info1, sizeof(info1), "%s · %s · %s", a->version[0] ? a->version : "ohne Version", size, category_name(a->category));
bool both = app_has_both(a);
fit(line, sizeof(line), info1, 0.42f, both ? kindBox.x - 22 : BOT_W - 28);
text(line, 14, 118, 0.42f, COL_WHITE, 0);
// Umschalter CIA | 3DSX (nur wenn es beide gibt)
if (both) {
Box k = kindBox;
bool cia = strcmp(a->kind, "cia") == 0;
round_rect(k.x, k.y, k.w, k.h, k.h / 2, COL_CARD);
round_rect(cia ? k.x + 2 : k.x + k.w / 2, k.y + 2, k.w / 2 - 2, k.h - 4, (k.h - 4) / 2, COL_RED);
text("CIA", k.x + k.w / 4, k.y + 5, 0.42f, cia ? COL_WHITE : COL_MUTED, C2D_AlignCenter);
text("3DSX", k.x + k.w * 3 / 4, k.y + 5, 0.42f, cia ? COL_MUTED : COL_WHITE, C2D_AlignCenter);
}
char info2[128];
snprintf(info2, sizeof(info2), "%s%s%s%s", strcmp(a->kind, "cia") == 0 ? "Im HOME-Menü" : "Im Homebrew Launcher",
a->updated[0] ? " · aktualisiert " : "", a->updated, a->license[0] ? " · " : "");
if (a->license[0]) snprintf(info2 + strlen(info2), sizeof(info2) - strlen(info2), "%s", a->license);
fit(line, sizeof(line), info2, 0.38f, BOT_W - 28);
fit(line, sizeof(line), info2, 0.38f, both ? kindBox.x - 22 : BOT_W - 28);
text(line, 14, 134, 0.38f, COL_MUTED, 0);
C2D_DrawRectSolid(12, 150, 0.5f, BOT_W - 24, 1, COL_CARD_HI);
@@ -654,11 +674,17 @@ int main(int argc, char **argv)
} else if (ready && a) {
// --- App-Seite ---
AppStatus s = app_status(a);
bool switchKind = other_variant(a);
if (switchKind) s = APP_UPDATE; // andere Variante gewählt: Knopf installiert sie
bool hit = false, hitRemove = false;
if (down & KEY_TOUCH) {
hidTouchRead(&tp);
touchStartY = tp.py; touchStartScroll = descScroll; dragged = false;
if (inside(backBox, tp)) down |= KEY_B;
else if (app_has_both(a) && !busy && inside(kindBox, tp)) {
app_choose(a, tp.px < kindBox.x + kindBox.w / 2 ? "cia" : "3dsx", true);
armed = -1;
}
else if (inside(s == APP_NEW ? actionBox : actionBoxNarrow, tp)) hit = true;
else if (s != APP_NEW && inside(removeBox, tp)) hitRemove = true;
} else if (held & KEY_TOUCH && touchStartY >= DESC_Y - 10) {
@@ -674,6 +700,10 @@ int main(int argc, char **argv)
if (a->shots > 1 && down & KEY_R) shot = (shot + 1) % a->shots;
if (a->shots > 1 && down & KEY_L) shot = (shot + a->shots - 1) % a->shots;
if (down & KEY_X && app_has_both(a) && !busy) {
app_choose(a, strcmp(a->kind, "cia") == 0 ? "3dsx" : "cia", true);
armed = -1;
}
if (down & KEY_B && !busy) { detail = false; armed = -1; }
else if (!busy && (down & KEY_A || hit)) {
if (s == APP_INSTALLED && strcmp(a->kind, "cia") == 0) {
+81 -27
View File
@@ -13,6 +13,7 @@
#include "net.h"
#include "store.h"
#define COPY(dst, src) snprintf(dst, sizeof(dst), "%s", src)
#define LIST_FILE SHOP_DIR "/list.txt"
#define INSTALLED_FILE SHOP_DIR "/installed.txt"
#define MAX_SHEETS 8
@@ -37,6 +38,35 @@ App *catalog_app(int i) { return &apps[i]; }
int catalog_sheet_count(void) { return sheetCount; }
const char *catalog_sheet_path(int n) { return n >= 0 && n < sheetCount ? sheetPath[n] : ""; }
// Bevorzugte Variante, wenn es beide gibt (zuletzt gewählt, Standard CIA)
#define KIND_FILE SHOP_DIR "/format.txt"
static char preferredKind[6] = "cia";
static void load_preferred_kind(void)
{
FILE *f = fopen(KIND_FILE, "r");
if (!f) return;
char k[8] = "";
if (fgets(k, sizeof(k), f) && !strncmp(k, "3dsx", 4)) strcpy(preferredKind, "3dsx");
fclose(f);
}
bool app_has_both(const App *a) { return a->ciaFile[0] && a->dsxFile[0]; }
void app_choose(App *a, const char *kind, bool remember)
{
bool cia = !strcmp(kind, "cia");
if (cia ? !a->ciaFile[0] : !a->dsxFile[0]) return;
COPY(a->kind, cia ? "cia" : "3dsx");
COPY(a->file, cia ? a->ciaFile : a->dsxFile);
a->size = cia ? a->ciaSize : a->dsxSize;
if (remember) {
COPY(preferredKind, a->kind);
FILE *f = fopen(KIND_FILE, "w");
if (f) { fputs(preferredKind, f); fclose(f); }
}
}
AppStatus app_status(const App *a)
{
if (!a->installed) return APP_NEW;
@@ -90,15 +120,14 @@ static int split(char *line, char **fields, int max)
return n;
}
#define COPY(dst, src) snprintf(dst, sizeof(dst), "%s", src)
static bool parse_list(char *text, int *revision)
{
char *save = NULL, *line = strtok_r(text, "\n", &save);
char *f[16];
if (!line || split(line, f, 20) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false;
char *f[24];
if (!line || split(line, f, 24) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false;
int format = atoi(f[1]);
int fields = format >= 2 ? 17 : 13;
int fields = format >= 3 ? 21 : format >= 2 ? 17 : 13;
*revision = atoi(f[2]);
int count = atoi(f[3]);
sheetCount = atoi(f[4]);
@@ -109,7 +138,7 @@ static bool parse_list(char *text, int *revision)
appCount = 0;
while ((line = strtok_r(NULL, "\n", &save)) && appCount < count) {
if (split(line, f, 20) < fields) continue;
if (split(line, f, 24) < fields) continue;
App *a = &apps[appCount++];
COPY(a->slug, f[0]);
COPY(a->title, f[1]);
@@ -129,7 +158,20 @@ static bool parse_list(char *text, int *revision)
a->colorBg = f[14][0] == '#' ? strtoul(f[14] + 1, NULL, 16) : 0;
COPY(a->license, f[15]);
}
if (format >= 3) {
COPY(a->ciaFile, f[16]);
a->ciaSize = strtoul(f[17], NULL, 10);
COPY(a->dsxFile, f[18]);
a->dsxSize = strtoul(f[19], NULL, 10);
} else if (!strcmp(a->kind, "cia")) {
COPY(a->ciaFile, a->file);
a->ciaSize = a->size;
} else {
COPY(a->dsxFile, a->file);
a->dsxSize = a->size;
}
a->description = strdup(f[fields - 1]);
if (app_has_both(a)) app_choose(a, preferredKind, false);
}
return appCount > 0;
}
@@ -167,8 +209,10 @@ static void load_installed(void)
}
a->installed = true;
COPY(a->installedVersion, f[1]);
COPY(a->instKind, f[2]);
a->titleId = strtoull(f[3], NULL, 16);
if (f[4][0]) COPY(a->file, f[4]);
COPY(a->instFile, f[4]);
app_choose(a, a->instKind, false); // die Seite zeigt die installierte Variante
}
free(text);
}
@@ -180,7 +224,7 @@ static void save_installed(void)
for (int i = 0; i < appCount; i++) {
App *a = &apps[i];
if (a->installed)
fprintf(f, "%s\t%s\t%s\t%016llX\t%s\n", a->slug, a->installedVersion, a->kind, a->titleId, a->file);
fprintf(f, "%s\t%s\t%s\t%016llX\t%s\n", a->slug, a->installedVersion, a->instKind, a->titleId, a->instFile);
}
if (extraInstalled) fputs(extraInstalled, f);
fclose(f);
@@ -215,7 +259,8 @@ static void catalog_worker(void *arg)
mkdir(SHOP_DIR, 0777);
snprintf(catMsg, sizeof(catMsg), "Lade den Katalog ...");
snprintf(url, sizeof(url), "http://%s/shop/list2", SHOP_SERVER);
snprintf(url, sizeof(url), "http://%s/shop/list3", SHOP_SERVER);
load_preferred_kind();
u32 status = net_get(url, &data, &size, 4 * 1024 * 1024, &rc);
char *text = NULL;
if (status == 200 && size > 10) {
@@ -372,13 +417,32 @@ static bool install_sink(void *p, const u8 *data, u32 len, u32 total)
return write_part(in, data, len);
}
// Installierte Variante entfernen (CIA über AM, .3dsx von der SD). Schon von Hand gelöscht zählt als Erfolg.
static Result remove_files(const App *a)
{
if (strcmp(a->instKind, "cia") == 0) {
if (!a->titleId) return 0;
Result rc = AM_DeleteTitle(MEDIATYPE_SD, a->titleId);
AM_TitleEntry info;
u64 id = a->titleId;
if (R_FAILED(rc) && R_FAILED(AM_GetTitleInfo(MEDIATYPE_SD, 1, &id, &info))) rc = 0;
return rc;
}
if (a->instFile[0]) {
char path[128];
snprintf(path, sizeof(path), "sdmc:/3ds/%s", a->instFile);
remove(path);
}
return 0;
}
static void install_worker(App *a)
{
char url[192];
Install in = { .cia = strcmp(a->kind, "cia") == 0 };
in.head = malloc(HEAD_SIZE);
if (!in.head) { snprintf(jobMsg, sizeof(jobMsg), "Zu wenig Speicher."); jobState = JOB_FAILED; return; }
snprintf(url, sizeof(url), "http://%s/shop/get/%s", SHOP_SERVER, a->slug);
snprintf(url, sizeof(url), "http://%s/shop/get/%s/%s", SHOP_SERVER, a->slug, a->kind);
Result rc = 0;
u32 status = net_stream(url, install_sink, &in, &rc);
if (status == 200 && !in.started && !in.error) in.error = "Die Datei ist keine gültige App.";
@@ -403,9 +467,13 @@ static void install_worker(App *a)
if (ok) {
bool update = a->installed;
// Vorher die andere Variante installiert? Die alte wegräumen (nur eine von beiden behalten)
if (a->installed && strcmp(a->instKind, a->kind) != 0) remove_files(a);
a->installed = true;
COPY(a->installedVersion, a->version);
if (in.cia) a->titleId = cia_title_id(in.head, in.headLen);
COPY(a->instKind, a->kind);
COPY(a->instFile, a->file);
a->titleId = in.cia ? cia_title_id(in.head, in.headLen) : 0;
save_installed();
snprintf(jobMsg, sizeof(jobMsg), in.cia ? "%s %s!\nDu findest die App jetzt im HOME-Menü."
: "%s %s!\nStarte sie über den Homebrew Launcher (/3ds/%s).",
@@ -423,25 +491,11 @@ static void install_worker(App *a)
static void remove_worker(App *a)
{
bool ok = true;
Result rc = 0;
if (strcmp(a->kind, "cia") == 0) {
if (a->titleId) {
rc = AM_DeleteTitle(MEDIATYPE_SD, a->titleId);
// schon von Hand gelöscht? Dann ist es auch gut.
AM_TitleEntry info;
u64 id = a->titleId;
if (R_FAILED(rc) && R_FAILED(AM_GetTitleInfo(MEDIATYPE_SD, 1, &id, &info))) rc = 0;
ok = R_SUCCEEDED(rc);
}
} else {
char path[128];
snprintf(path, sizeof(path), "sdmc:/3ds/%s", a->file);
remove(path);
}
Result rc = remove_files(a);
bool ok = R_SUCCEEDED(rc);
if (ok) {
a->installed = false;
a->installedVersion[0] = 0;
a->installedVersion[0] = a->instKind[0] = a->instFile[0] = 0;
a->titleId = 0;
save_installed();
snprintf(jobMsg, sizeof(jobMsg), "%s entfernt.", a->title);
+9 -1
View File
@@ -9,9 +9,13 @@
typedef enum { APP_NEW, APP_INSTALLED, APP_UPDATE } AppStatus;
typedef struct {
char slug[72], title[64], author[44], version[28], category[16], kind[6], file[72], updated[12];
char slug[72], title[64], author[44], version[28], category[16], updated[12];
// Gewählte Variante (das lädt der Knopf): "cia" oder "3dsx", Dateiname, Größe
char kind[6], file[72];
int sheet, icon; // Symbol: Blatt und Nummer darin (-1 = keins)
u32 size, stars;
char ciaFile[72], dsxFile[72]; // beide Varianten (leer = gibt es nicht)
u32 ciaSize, dsxSize;
int shots; // Anzahl Screenshots (/shop/shot/<slug>/<n>)
bool banner; // Bannerbild vorhanden (/shop/banner/<slug>)
u32 colorBg; // Hintergrundfarbe der App (0xRRGGBB), 0 = keine
@@ -20,6 +24,7 @@ typedef struct {
// Installiert (aus SHOP_DIR/installed.txt)
bool installed;
char installedVersion[28];
char instKind[6], instFile[72]; // installierte Variante
u64 titleId; // bei CIAs, 0 sonst
} App;
@@ -34,6 +39,9 @@ App *catalog_app(int i);
int catalog_sheet_count(void);
const char *catalog_sheet_path(int n); // Symbol-Blatt auf der SD (leer, wenn nicht da)
AppStatus app_status(const App *a);
bool app_has_both(const App *a);
// Variante wählen ("cia" / "3dsx"); remember = als Standard für andere Apps merken
void app_choose(App *a, const char *kind, bool remember);
// --- Installieren / Entfernen (läuft im Hintergrund) ---
typedef enum { JOB_IDLE, JOB_RUNNING, JOB_DONE, JOB_FAILED } JobState;