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:
co-authored by
Claude Opus 5.5
parent
93316ec2c9
commit
f05fd2fc16
@@ -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
|
||||
|
||||
+35
-5
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user