From f05fd2fc163b84bcc4a02e1e23659569c194ff48 Mon Sep 17 00:00:00 2001 From: Nico Meister Date: Mon, 19 Oct 2026 18:40:00 +0200 Subject: [PATCH] =?UTF-8?q?v2.9.7:=20Shop=20-=20Auswahl=20CIA=20oder=203DS?= =?UTF-8?q?X,=20Katalog-Leser=20ohne=20Puffer=C3=BCberlauf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- Makefile | 2 +- shop/source/main.c | 40 ++++++++++++++-- shop/source/store.c | 108 +++++++++++++++++++++++++++++++++----------- shop/source/store.h | 10 +++- 4 files changed, 126 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 9b58ede..55e72f5 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ SOURCES := source INCLUDES := source # Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer -VERSION := 2.9.6 +VERSION := 2.9.7 APP_TITLE := MBMNET APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk diff --git a/shop/source/main.c b/shop/source/main.c index 6c8dd66..4e20e68 100644 --- a/shop/source/main.c +++ b/shop/source/main.c @@ -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) { diff --git a/shop/source/store.c b/shop/source/store.c index 965bbd4..f057a38 100644 --- a/shop/source/store.c +++ b/shop/source/store.c @@ -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); diff --git a/shop/source/store.h b/shop/source/store.h index e730d07..f135ce6 100644 --- a/shop/source/store.h +++ b/shop/source/store.h @@ -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//) bool banner; // Bannerbild vorhanden (/shop/banner/) 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;