Compare commits

...
26 Commits
Author SHA1 Message Date
Nico MeisterandClaude Opus 5.5 2f4a2ff8f8 v3.1.9: Logs - Live-Knopf (und Y) springt ans Ende und läuft wieder mit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:52:59 +01:00
Nico MeisterandClaude Opus 5.5 9ac6167953 v3.1.8: Logs - Schiebepad für Zeilen, Steuerkreuz für Quellen, Hochblättern sprang nicht mehr zurück
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:50:41 +01:00
Nico MeisterandClaude Opus 5.5 e9a7bfb712 v3.1.7: Log-Passwort per HTTPS-Kopfzeile (POST-Inhalt kam nicht an)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:46:53 +01:00
Nico MeisterandClaude Opus 5.5 7d49f1f342 v3.1.6: Admin-Freischaltung per Geräte-Schlüssel und Code (ohne Konto-Dienst), Y löst gezielt den MBMNET-Platz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:31:47 +01:00
Nico MeisterandClaude Opus 5.5 39ed683c95 v3.1.5: ACT_Save nach SetDefaultAccount (Soft-Reboot verlor die Einstellung), Admin-Prüfung stellt act auf den MBMNET-Platz und zeigt beide IDs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:26:23 +01:00
Nico MeisterandClaude Opus 5.5 6375cd5b00 v3.1.4: AcquireAccountTokenEx zuerst mit gespeichertem Passwort (useNullPassword=false)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:22:43 +01:00
Nico MeisterandClaude Opus 5.5 d1fba48c6f v3.1.3: Admin-Prüfung zuerst (unabhängig von den Patches), SELECT zeigt den Auffrischen-Schritt (welches Modul)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:20:41 +01:00
Nico MeisterandClaude Opus 5.5 36f6e5e027 v3.1.2: Absturz bei der Admin-Prüfung (ACTA_LoadConsoleAccount/AcquireAccountTokenEx mit NULL-Passwort) behoben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:05:54 +01:00
Nico MeisterandClaude Opus 5.5 44c9b7e60c v3.1.1: Admin-Prüfung meldet jeden Schritt (Fehlersuche)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:03:44 +01:00
Nico MeisterandClaude Opus 5.5 59e082e7d5 v3.1.0: Banner-Töne als kleine Melodien (App und Shop) statt Piepen; HOME-Menü-Design wieder entfernt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 21:02:11 +01:00
Nico MeisterandClaude Opus 5.5 17c4c7a074 MBMNET-Design fürs HOME-Menü: eigene Töne (CWAV), Aufbau wie Usagi, in release.sh und im SD-Paket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:57:36 +01:00
Nico MeisterandClaude Opus 5.5 f4ff27b375 v3.0.9: Admin-Prüfung lädt und loggt das Konto bei Bedarf ein (ACTA_LoadConsoleAccount, AcquireAccountTokenEx), zeigt alle Zwischenergebnisse
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:51:56 +01:00
Nico MeisterandClaude Opus 5.5 68ee20f35c v3.0.8: Admin-Prüfung mit den Tokens aus dem Konto-Dienst (lokal gelesen, per HTTPS vom Server geprüft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:47:17 +01:00
Nico MeisterandClaude Opus 5.5 4cc3329dab v3.0.7: ACT_Initialize mit SDK-Version und Speicherblock (sonst lehnt act Server-Anfragen ab)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:41:31 +01:00
Nico MeisterandClaude Opus 5.5 f7590a939e v3.0.6: Admin-Prüfung im Auffrischen nach dem Konto-Abgleich, Wiederholung bei ungültigem Zustand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:37:39 +01:00
Nico MeisterandClaude Opus 5.5 072b19d227 v3.0.5: Anmelde-Nachweis für die Admin-Prüfung geteilt anfordern, Ergebnis von act prüfen, Zwischenspeicher als Notlösung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:34:31 +01:00
Nico MeisterandClaude Opus 5.5 d1f5f5add3 v3.0.4: eigene Werkzeug-Seite, Logs der MBMNET-Server (HTTPS mit MBMNET-CA), Admin nur mit signiertem Nachweis vom Server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:28:29 +01:00
Nico MeisterandClaude Opus 5.5 718c605f6d v3.0.3: Y löst die verknüpfte ID (für alle, mit Bestätigung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:12:19 +01:00
Nico MeisterandClaude Opus 5.5 4b07ddf3ae v3.0.2: BOSS-Patch für die Policy-Liste (SpotPass im Hintergrund auch nach einem Neustart)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:09:54 +01:00
Nico MeisterandClaude Opus 5.5 29f2c556c2 v3.0.1: SpotPass-Test-Aufgabe aufräumen (Start/Ende, 24-h-Abstand), Werkzeuge nur für vom Server bestätigte Admins (MBMNET-Platz, Antwort an die PID gebunden)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:07:57 +01:00
Nico MeisterandClaude Opus 5.5 f5fc0861c7 v3.0.0: Umschalten wartet nicht mehr aufs Auffrischen (abbrechbar, CA-Abruf zuletzt), schnellere Prüfintervalle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:13:44 +02:00
Nico MeisterandClaude Opus 5.5 c384c20558 v2.9.9: D8A0C4E5 beim Wechsel zu Nintendo (FRD_Logout vor dem Abmelden, MBMNET-ID erst danach lösen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:06:56 +02:00
Nico MeisterandClaude Opus 5.5 23bf1abca8 v2.9.8: Soft-Reboot über ns:s (wie Nimbus) statt Hardware-Reset beim Umschalten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:00:18 +02:00
Nico MeisterandClaude Opus 5.5 f05fd2fc16 v2.9.7: Shop - Auswahl CIA oder 3DSX, Katalog-Leser ohne Pufferüberlauf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:40:00 +02:00
Nico MeisterandClaude Opus 5.5 93316ec2c9 v2.9.6: SpotPass alle 15 Minuten statt alle 2 (Akku)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:37:28 +02:00
Nico MeisterandClaude Opus 5.5 93efce6f58 v2.9.5: schnelleres Umschalten (Miiverse-CA aus dem SD-Zwischenspeicher, kurzes Warten beim Beenden)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:36:57 +02:00
25 changed files with 1298 additions and 191 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ SOURCES := source
INCLUDES := source INCLUDES := source
# Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer # Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer
VERSION := 2.9.4 VERSION := 3.1.9
APP_TITLE := MBMNET APP_TITLE := MBMNET
APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk
BIN
View File
Binary file not shown.
+2 -2
View File
@@ -198,7 +198,7 @@ AccessControlInfo:
QueryProcessMemory: 125 QueryProcessMemory: 125
# Dienste: u. a. am:net (Updates installieren), http:C (Updates laden), fs:USER, ptm:sysm (Neustart), # Dienste: u. a. am:net (Updates installieren), http:C (Updates laden), fs:USER, ptm:sysm (Neustart),
# frd:a (Freundekonto umschalten), pm:app (FTP-Modul starten/beenden) # frd:a (Freundekonto umschalten), pm:app (FTP-Modul starten/beenden), ns:s (schneller Neustart)
ServiceAccessControl: ServiceAccessControl:
- APT:U - APT:U
- ac:u - ac:u
@@ -222,7 +222,7 @@ AccessControlInfo:
- ir:rst - ir:rst
- ir:u - ir:u
- ir:USER - ir:USER
- mic:u - ns:s
- ndm:u - ndm:u
- news:s - news:s
- nwm::EXT - nwm::EXT
Binary file not shown.
+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; typedef struct { float x, y, w, h; } Box;
static const Box backBox = { 6, 8, 30, 46 }; 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 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_TextBuf textBuf, measureBuf; // measureBuf nur zum Messen (wird jedes Mal geleert)
static C2D_SpriteSheet sheets[8]; 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); draw_hero(a, a->shots ? shot : -1);
char right[48]; 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); draw_top_bar(a->title, right);
// Punkte für die Screenshots // Punkte für die Screenshots
if (a->shots > 1) { 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); AppStatus s = app_status(a);
bool running = job_state() == JOB_RUNNING; bool running = job_state() == JOB_RUNNING;
Box b = s == APP_NEW ? actionBox : actionBoxNarrow; Box b = s == APP_NEW ? actionBox : actionBoxNarrow;
const char *label = s == APP_NEW ? "Kostenlos herunterladen" : s == APP_UPDATE ? "Aktualisieren" bool switchKind = other_variant(a);
: strcmp(a->kind, "cia") == 0 ? "Öffnen" : "Neu laden"; 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; u32 col = s == APP_INSTALLED ? COL_GREEN : COL_RED;
if (armed == 0) { label = s == APP_NEW ? "Nochmal tippen: Laden" : "Nochmal tippen"; col = COL_AMBER; } if (armed == 0) { label = s == APP_NEW ? "Nochmal tippen: Laden" : "Nochmal tippen"; col = COL_AMBER; }
char progress[48]; char progress[48];
@@ -473,13 +481,25 @@ static void draw_bottom_detail(const App *a, int armed, int descScroll)
// Infos // Infos
char size[16]; char size[16];
size_text(size, sizeof(size), a->size); 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); 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]; char info2[128];
snprintf(info2, sizeof(info2), "%s%s%s%s", strcmp(a->kind, "cia") == 0 ? "Im HOME-Menü" : "Im Homebrew Launcher", 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] ? " · " : ""); a->updated[0] ? " · aktualisiert " : "", a->updated, a->license[0] ? " · " : "");
if (a->license[0]) snprintf(info2 + strlen(info2), sizeof(info2) - strlen(info2), "%s", a->license); 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); text(line, 14, 134, 0.38f, COL_MUTED, 0);
C2D_DrawRectSolid(12, 150, 0.5f, BOT_W - 24, 1, COL_CARD_HI); 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) { } else if (ready && a) {
// --- App-Seite --- // --- App-Seite ---
AppStatus s = app_status(a); 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; bool hit = false, hitRemove = false;
if (down & KEY_TOUCH) { if (down & KEY_TOUCH) {
hidTouchRead(&tp); hidTouchRead(&tp);
touchStartY = tp.py; touchStartScroll = descScroll; dragged = false; touchStartY = tp.py; touchStartScroll = descScroll; dragged = false;
if (inside(backBox, tp)) down |= KEY_B; 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 (inside(s == APP_NEW ? actionBox : actionBoxNarrow, tp)) hit = true;
else if (s != APP_NEW && inside(removeBox, tp)) hitRemove = true; else if (s != APP_NEW && inside(removeBox, tp)) hitRemove = true;
} else if (held & KEY_TOUCH && touchStartY >= DESC_Y - 10) { } 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_R) shot = (shot + 1) % a->shots;
if (a->shots > 1 && down & KEY_L) shot = (shot + a->shots - 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; } if (down & KEY_B && !busy) { detail = false; armed = -1; }
else if (!busy && (down & KEY_A || hit)) { else if (!busy && (down & KEY_A || hit)) {
if (s == APP_INSTALLED && strcmp(a->kind, "cia") == 0) { if (s == APP_INSTALLED && strcmp(a->kind, "cia") == 0) {
+81 -27
View File
@@ -13,6 +13,7 @@
#include "net.h" #include "net.h"
#include "store.h" #include "store.h"
#define COPY(dst, src) snprintf(dst, sizeof(dst), "%s", src)
#define LIST_FILE SHOP_DIR "/list.txt" #define LIST_FILE SHOP_DIR "/list.txt"
#define INSTALLED_FILE SHOP_DIR "/installed.txt" #define INSTALLED_FILE SHOP_DIR "/installed.txt"
#define MAX_SHEETS 8 #define MAX_SHEETS 8
@@ -37,6 +38,35 @@ App *catalog_app(int i) { return &apps[i]; }
int catalog_sheet_count(void) { return sheetCount; } int catalog_sheet_count(void) { return sheetCount; }
const char *catalog_sheet_path(int n) { return n >= 0 && n < sheetCount ? sheetPath[n] : ""; } 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) AppStatus app_status(const App *a)
{ {
if (!a->installed) return APP_NEW; if (!a->installed) return APP_NEW;
@@ -90,15 +120,14 @@ static int split(char *line, char **fields, int max)
return n; return n;
} }
#define COPY(dst, src) snprintf(dst, sizeof(dst), "%s", src)
static bool parse_list(char *text, int *revision) static bool parse_list(char *text, int *revision)
{ {
char *save = NULL, *line = strtok_r(text, "\n", &save); char *save = NULL, *line = strtok_r(text, "\n", &save);
char *f[16]; char *f[24];
if (!line || split(line, f, 20) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false; if (!line || split(line, f, 24) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false;
int format = atoi(f[1]); int format = atoi(f[1]);
int fields = format >= 2 ? 17 : 13; int fields = format >= 3 ? 21 : format >= 2 ? 17 : 13;
*revision = atoi(f[2]); *revision = atoi(f[2]);
int count = atoi(f[3]); int count = atoi(f[3]);
sheetCount = atoi(f[4]); sheetCount = atoi(f[4]);
@@ -109,7 +138,7 @@ static bool parse_list(char *text, int *revision)
appCount = 0; appCount = 0;
while ((line = strtok_r(NULL, "\n", &save)) && appCount < count) { 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++]; App *a = &apps[appCount++];
COPY(a->slug, f[0]); COPY(a->slug, f[0]);
COPY(a->title, f[1]); 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; a->colorBg = f[14][0] == '#' ? strtoul(f[14] + 1, NULL, 16) : 0;
COPY(a->license, f[15]); 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]); a->description = strdup(f[fields - 1]);
if (app_has_both(a)) app_choose(a, preferredKind, false);
} }
return appCount > 0; return appCount > 0;
} }
@@ -167,8 +209,10 @@ static void load_installed(void)
} }
a->installed = true; a->installed = true;
COPY(a->installedVersion, f[1]); COPY(a->installedVersion, f[1]);
COPY(a->instKind, f[2]);
a->titleId = strtoull(f[3], NULL, 16); 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); free(text);
} }
@@ -180,7 +224,7 @@ static void save_installed(void)
for (int i = 0; i < appCount; i++) { for (int i = 0; i < appCount; i++) {
App *a = &apps[i]; App *a = &apps[i];
if (a->installed) 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); if (extraInstalled) fputs(extraInstalled, f);
fclose(f); fclose(f);
@@ -215,7 +259,8 @@ static void catalog_worker(void *arg)
mkdir(SHOP_DIR, 0777); mkdir(SHOP_DIR, 0777);
snprintf(catMsg, sizeof(catMsg), "Lade den Katalog ..."); 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); u32 status = net_get(url, &data, &size, 4 * 1024 * 1024, &rc);
char *text = NULL; char *text = NULL;
if (status == 200 && size > 10) { 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); 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) static void install_worker(App *a)
{ {
char url[192]; char url[192];
Install in = { .cia = strcmp(a->kind, "cia") == 0 }; Install in = { .cia = strcmp(a->kind, "cia") == 0 };
in.head = malloc(HEAD_SIZE); in.head = malloc(HEAD_SIZE);
if (!in.head) { snprintf(jobMsg, sizeof(jobMsg), "Zu wenig Speicher."); jobState = JOB_FAILED; return; } 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; Result rc = 0;
u32 status = net_stream(url, install_sink, &in, &rc); 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."; 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) { if (ok) {
bool update = a->installed; 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; a->installed = true;
COPY(a->installedVersion, a->version); 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(); save_installed();
snprintf(jobMsg, sizeof(jobMsg), in.cia ? "%s %s!\nDu findest die App jetzt im HOME-Menü." 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).", : "%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) static void remove_worker(App *a)
{ {
bool ok = true; Result rc = remove_files(a);
Result rc = 0; bool ok = R_SUCCEEDED(rc);
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);
}
if (ok) { if (ok) {
a->installed = false; a->installed = false;
a->installedVersion[0] = 0; a->installedVersion[0] = a->instKind[0] = a->instFile[0] = 0;
a->titleId = 0; a->titleId = 0;
save_installed(); save_installed();
snprintf(jobMsg, sizeof(jobMsg), "%s entfernt.", a->title); 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 enum { APP_NEW, APP_INSTALLED, APP_UPDATE } AppStatus;
typedef struct { 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) int sheet, icon; // Symbol: Blatt und Nummer darin (-1 = keins)
u32 size, stars; 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>) int shots; // Anzahl Screenshots (/shop/shot/<slug>/<n>)
bool banner; // Bannerbild vorhanden (/shop/banner/<slug>) bool banner; // Bannerbild vorhanden (/shop/banner/<slug>)
u32 colorBg; // Hintergrundfarbe der App (0xRRGGBB), 0 = keine u32 colorBg; // Hintergrundfarbe der App (0xRRGGBB), 0 = keine
@@ -20,6 +24,7 @@ typedef struct {
// Installiert (aus SHOP_DIR/installed.txt) // Installiert (aus SHOP_DIR/installed.txt)
bool installed; bool installed;
char installedVersion[28]; char installedVersion[28];
char instKind[6], instFile[72]; // installierte Variante
u64 titleId; // bei CIAs, 0 sonst u64 titleId; // bei CIAs, 0 sonst
} App; } App;
@@ -34,6 +39,9 @@ App *catalog_app(int i);
int catalog_sheet_count(void); int catalog_sheet_count(void);
const char *catalog_sheet_path(int n); // Symbol-Blatt auf der SD (leer, wenn nicht da) const char *catalog_sheet_path(int n); // Symbol-Blatt auf der SD (leer, wenn nicht da)
AppStatus app_status(const App *a); 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) --- // --- Installieren / Entfernen (läuft im Hintergrund) ---
typedef enum { JOB_IDLE, JOB_RUNNING, JOB_DONE, JOB_FAILED } JobState; typedef enum { JOB_IDLE, JOB_RUNNING, JOB_DONE, JOB_FAILED } JobState;
+97 -29
View File
@@ -1,58 +1,126 @@
// Entwickler-Werkzeuge nur für Admins: Die App fragt bei MBMNET, ob die verknüpfte NNID einem Admin // Entwickler-Werkzeuge nur für Admins - und nur, wenn der Server es gerade bestätigt hat.
// gehört (GET /mbmnet/app-info?pid=...). Die Antwort wird auf der SD gemerkt, damit die Werkzeuge //
// auch im Nintendo-Modus und ohne Verbindung da sind. // Die Konsole hat einen geheimen Geräte-Schlüssel (32 Zufallsbytes, /3ds/mbmnet/geraete-schluessel). Die App
// schickt ihn per HTTPS (MBMNET-CA) an /mbmnet/admin-check:
// - freigeschaltete Konsole eines Admins -> admin=1
// - unbekannt -> pair=123456: Diesen Code gibt ein Admin auf der Webseite unter Admin > Konsolen ein.
// Kein Passwort am 3DS, kein Konto-Dienst nötig. Ohne Antwort vom Server gibt es keine Werkzeuge.
// (Früher über die NNID-Anmeldung im Konto-Dienst - die war im MBMNET-Platz durcheinander und ließ sich
// nicht zuverlässig nutzen.)
#include <3ds.h> #include <3ds.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include "admin.h" #include "admin.h"
#include "https.h"
#define CACHE_FILE "sdmc:/3ds/mbmnet/admin" #define KEY_FILE "sdmc:/3ds/mbmnet/geraete-schluessel"
#define OLD_CACHE_FILE "sdmc:/3ds/mbmnet/admin" // gemerkte Antwort älterer Versionen (wird gelöscht)
static volatile bool isAdmin; static volatile bool isAdmin, running;
static Thread thread; static Thread thread;
static char url[128]; static char server[64];
static char why[160] = "Werkzeuge: Server prüft noch ...";
static char key[65];
static void remember(bool on) // Geräte-Schlüssel laden oder einmal erzeugen (Zufall vom SSL-Dienst des 3DS)
static bool load_key(void)
{ {
FILE *f = fopen(CACHE_FILE, "w"); FILE *f = fopen(KEY_FILE, "r");
if (f) { fputs(on ? "1" : "0", f); fclose(f); } if (f) {
if (fgets(key, sizeof(key), f)) key[strcspn(key, "\r\n")] = 0;
fclose(f);
if (strlen(key) == 64) return true;
}
u8 raw[32];
bool ok = false;
if (R_SUCCEEDED(sslcInit(0))) {
ok = R_SUCCEEDED(sslcGenerateRandomData(raw, sizeof(raw)));
sslcExit();
}
if (!ok) { key[0] = 0; return false; }
for (int i = 0; i < 32; i++) snprintf(key + i * 2, 3, "%02x", raw[i]);
mkdir("sdmc:/3ds/mbmnet", 0777);
f = fopen(KEY_FILE, "w");
if (!f) { key[0] = 0; return false; }
fputs(key, f);
fclose(f);
return true;
} }
static void worker(void *arg) static void check(void *arg)
{ {
(void)arg; (void)arg;
httpcContext ctx; if (!key[0] && !load_key()) {
if (R_FAILED(httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1))) return; snprintf(why, sizeof(why), "Werkzeuge: Geräte-Schlüssel nicht anlegbar (SD-Karte?).");
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION); running = false;
u32 status = 0, got = 0; return;
char body[32] = { 0 }; }
Result rc = httpcBeginRequest(&ctx); // Geräte-ID nur zur Anzeige auf der Webseite (welche Konsole)
if (R_SUCCEEDED(rc)) rc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 8ULL * 1000 * 1000 * 1000); char devid[16] = "";
if (R_SUCCEEDED(rc) && status == 200) httpcDownloadData(&ctx, (u8 *)body, sizeof(body) - 1, &got); u32 id = 0;
httpcCloseContext(&ctx); s32 internal = 0;
if (status != 200 || strncmp(body, "admin=", 6) != 0) return; // keine Antwort: gemerkte gilt weiter if (R_SUCCEEDED(amInit())) {
isAdmin = body[6] == '1'; if (R_SUCCEEDED(AM_GetDeviceId(&internal, &id))) snprintf(devid, sizeof(devid), "%lu", (unsigned long)id);
remember(isAdmin); amExit();
}
char body[48];
u32 len = 0;
Result rc = 0;
u32 code = https_request(server, "/mbmnet/admin-check", NULL, "X-Admin-Device", key, body, sizeof(body), &len, NULL, 0, &rc);
(void)devid;
if (code == 200 && !strncmp(body, "admin=1", 7)) {
isAdmin = true;
why[0] = 0;
} else if (code == 200 && !strncmp(body, "pair=", 5) && body[5]) {
char c[8];
snprintf(c, sizeof(c), "%.6s", body + 5);
snprintf(why, sizeof(why), "Werkzeuge freischalten: Code %.3s %.3s\nauf mbmnet.mbm.wtf unter Admin > Konsolen eingeben,\n"
"dann nochmal SELECT drücken (10 Minuten gültig).", c, c + 3);
} else if (code == 200 && !strncmp(body, "pair=", 5)) {
snprintf(why, sizeof(why), "Werkzeuge: zu viele offene Freischaltungen, später nochmal.");
} else if (code == 200) {
snprintf(why, sizeof(why), "Werkzeuge: Diese Konsole ist nicht (mehr) freigeschaltet.");
} else if (code) {
snprintf(why, sizeof(why), "Werkzeuge: Server antwortet HTTP %lu.", (unsigned long)code);
} else {
snprintf(why, sizeof(why), "Werkzeuge: keine HTTPS-Verbindung zum Server (%08lX).", (unsigned long)rc);
}
running = false;
} }
void admin_start(const char *server, u32 pid) void admin_prepare(const char *srv, bool unused1, u8 unused2)
{ {
FILE *f = fopen(CACHE_FILE, "r"); (void)unused1; (void)unused2;
if (f) { isAdmin = fgetc(f) == '1'; fclose(f); } isAdmin = false;
if (!pid) return; remove(OLD_CACHE_FILE);
snprintf(url, sizeof(url), "http://%s/mbmnet/app-info?pid=%lu", server, (unsigned long)pid); snprintf(server, sizeof(server), "%s", srv);
snprintf(why, sizeof(why), "Werkzeuge: Server prüft noch ...");
}
// Prüfen im Hintergrund (beim Start und bei SELECT, solange noch kein Admin). Läuft schon eine: nichts tun.
void admin_check_async(void)
{
if (running || isAdmin) return;
if (thread) { threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; }
running = true;
s32 prio = 0x30; s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
thread = threadCreate(worker, NULL, 16 * 1024, prio + 1, -2, false); thread = threadCreate(check, NULL, 16 * 1024, prio + 1, -2, false);
if (!thread) running = false;
} }
void admin_check(void) { admin_check_async(); }
bool admin_is(void) { return isAdmin; } bool admin_is(void) { return isAdmin; }
const char *admin_why(void) { return why; }
void admin_progress(const char *step) { (void)step; }
void admin_skipped(void) {}
bool admin_finish(void) bool admin_finish(void)
{ {
if (!thread) return true; if (!thread) return true;
if (R_FAILED(threadJoin(thread, 15ULL * 1000 * 1000 * 1000))) return false; // läuft noch if (R_FAILED(threadJoin(thread, 100ULL * 1000 * 1000))) return false; // läuft noch
threadFree(thread); threadFree(thread);
thread = NULL; thread = NULL;
return true; return true;
+17 -5
View File
@@ -1,9 +1,21 @@
#pragma once #pragma once
#include <3ds/types.h> #include <3ds/types.h>
#include <stdbool.h>
// Fragt im Hintergrund bei MBMNET, ob die verknüpfte NNID (pid) einem Admin gehört. // Prüft, ob das angemeldete MBMNET-Konto Admin ist (signierter Anmelde-Nachweis, per HTTPS vom Server bestätigt,
// pid = 0: nicht fragen, nur die gemerkte Antwort vom letzten Mal benutzen. // siehe admin.c). admin_prepare beim Start; mbmnetLoggedIn = false: keine Werkzeuge.
void admin_start(const char *server, u32 pid); void admin_prepare(const char *server, bool mbmnetLoggedIn, u8 mbmnetSlot);
// Admin? (gemerkte Antwort, bis der Server geantwortet hat) // Die eigentliche Prüfung (blockiert, im Hintergrund-Thread aufrufen, nicht gleichzeitig mit anderen act-Anfragen)
void admin_check(void);
// Prüfung fiel aus (es wurde umgeschaltet)
void admin_skipped(void);
// Was das Auffrischen gerade macht (vor der Prüfung), für die Anzeige bei SELECT
void admin_progress(const char *step);
// Admin? Nur nach Bestätigung durch den Server in dieser Sitzung.
bool admin_is(void); bool admin_is(void);
bool admin_finish(void); // wartet höchstens 15 s, false = läuft noch // Warum es (noch) keine Werkzeuge gibt (leer = Admin)
const char *admin_why(void);
bool admin_finish(void); // wartet höchstens 0,1 s, false = läuft noch
// Prüfung im Hintergrund starten (beim Start und bei SELECT, solange noch keine Freigabe)
void admin_check_async(void);
+12 -6
View File
@@ -88,24 +88,26 @@ bool frdswitch_slot_linked(u8 friendAccount, char *id, size_t len)
static Result unload(void) static Result unload(void)
{ {
note("Konto (act) abmelden", ACTA_UnloadConsoleAccount()); // ohne geladenes Konto: "ungültiger Zustand" note("Konto (act) abmelden", ACTA_UnloadConsoleAccount()); // ohne geladenes Konto: "ungültiger Zustand"
note("Freunde-Dienst abmelden", FRD_Logout()); // laufende Anmeldung beenden (sonst "ungültiger Zustand")
Result rc = step("Offline gehen (exklusiv)", NDMU_EnterExclusiveState(NDM_EXCLUSIVE_STATE_LOCAL_COMMUNICATIONS)); Result rc = step("Offline gehen (exklusiv)", NDMU_EnterExclusiveState(NDM_EXCLUSIVE_STATE_LOCAL_COMMUNICATIONS));
if (R_FAILED(rc)) return rc; if (R_FAILED(rc)) return rc;
for (int i = 0; i < 150; i++) { // höchstens 15 Sekunden for (int i = 0; i < 1500; i++) { // höchstens 15 Sekunden, alle 10 ms nachsehen
bool online = true; bool online = true;
if (R_FAILED(FRD_IsOnline(&online)) || !online) break; if (R_FAILED(FRD_IsOnline(&online)) || !online) break;
svcSleepThread(100 * 1000 * 1000); svcSleepThread(10 * 1000 * 1000);
} }
// Hängt der Freunde-Dienst noch an einer Anfrage, kurz wiederholen // Hängt der Freunde-Dienst noch an einer Anfrage, kurz wiederholen
for (int attempt = 1; attempt <= 20; attempt++) { for (int attempt = 1; attempt <= 200; attempt++) { // alle 100 ms, höchstens 20 s
if (attempt == 100) note("Freunde-Dienst erneut abmelden", FRD_Logout());
rc = FRDA_UnloadLocalAccount(); rc = FRDA_UnloadLocalAccount();
if (R_SUCCEEDED(rc)) { if (R_SUCCEEDED(rc)) {
if (logf) fprintf(logf, "%-36s %08lX (Versuch %d)\n", "Freundekonto abmelden", (unsigned long)rc, attempt); if (logf) fprintf(logf, "%-36s %08lX (Versuch %d)\n", "Freundekonto abmelden", (unsigned long)rc, attempt);
return rc; return rc;
} }
svcSleepThread(500 * 1000 * 1000); svcSleepThread(100 * 1000 * 1000);
} }
return step("Freundekonto abmelden (10 s)", rc); return step("Freundekonto abmelden (20 s)", rc);
} }
// Freundekonto laden. "Schon geladen" (z. B. direkt nach dem Anlegen) zählt als Erfolg. // Freundekonto laden. "Schon geladen" (z. B. direkt nach dem Anlegen) zählt als Erfolg.
@@ -137,7 +139,11 @@ static Result use_act_slot(u8 friendAccount, u32 *slotOut, bool *created)
} }
if (logf) fprintf(logf, " -> Konto-Platz %lu\n", (unsigned long)slot); if (logf) fprintf(logf, " -> Konto-Platz %lu\n", (unsigned long)slot);
*slotOut = slot; *slotOut = slot;
return step("Standard-Konto setzen", ACTA_SetDefaultAccount(slot)); rc = step("Standard-Konto setzen", ACTA_SetDefaultAccount(slot));
// Ausdrücklich speichern: Beim Soft-Reboot (seit v2.9.8) ging die Einstellung sonst verloren, dann stand
// act nach dem Neustart noch auf dem alten Platz (z. B. Pretendo) und meldete sich damit bei MBMNET an.
if (R_SUCCEEDED(rc)) note("Konto-Einstellungen speichern", ACT_Save());
return rc;
} }
bool frdswitch_to(u8 target, bool *created, char *msg, size_t msglen) bool frdswitch_to(u8 target, bool *created, char *msg, size_t msglen)
+64
View File
@@ -0,0 +1,64 @@
// HTTPS mit der MBMNET-CA, siehe https.h
#include <3ds.h>
#include <stdio.h>
#include <string.h>
#include "https.h"
#include "olvpatch.h"
static u8 caDer[1024];
static u32 caLen;
static LightLock caLock = 1; // 1 = entsperrt (LightLock_Init)
u32 https_request(const char *server, const char *path, const char *form, const char *header, const char *value,
char *out, u32 cap, u32 *outLen, char *date, size_t dateLen, Result *rcOut)
{
static u32 formBuf[64];
Result rc = 0;
*outLen = 0;
if (cap) out[0] = 0;
if (date && dateLen) date[0] = 0;
LightLock_Lock(&caLock);
bool haveCa = caLen || olvpatch_fetch_ca(server, caDer, &caLen);
LightLock_Unlock(&caLock);
if (!haveCa) { if (rcOut) *rcOut = -1; return 0; }
char url[224];
snprintf(url, sizeof(url), "https://%s%s", server, path);
httpcContext ctx;
if (R_FAILED(rc = httpcOpenContext(&ctx, form ? HTTPC_METHOD_POST : HTTPC_METHOD_GET, url, 1))) {
if (rcOut) *rcOut = rc;
return 0;
}
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
httpcSetKeepAlive(&ctx, HTTPC_KEEPALIVE_DISABLED);
httpcAddTrustedRootCA(&ctx, caDer, caLen); // nur der MBMNET-CA vertrauen wir zusätzlich
if (header && value) httpcAddRequestHeaderField(&ctx, header, value);
if (form && form[0]) { // leerer Text: POST ohne Inhalt
u32 n = strlen(form);
if (n >= sizeof(formBuf)) n = sizeof(formBuf) - 1;
memcpy(formBuf, form, n);
httpcAddRequestHeaderField(&ctx, "Content-Type", "application/x-www-form-urlencoded");
httpcAddPostDataRaw(&ctx, formBuf, n);
}
u32 code = 0;
rc = httpcBeginRequest(&ctx);
if (R_SUCCEEDED(rc)) rc = httpcGetResponseStatusCodeTimeout(&ctx, &code, 10ULL * 1000 * 1000 * 1000);
if (R_SUCCEEDED(rc)) {
if (date && dateLen) httpcGetResponseHeader(&ctx, "Date", date, dateLen);
u32 size = 0, got;
do {
got = 0;
rc = httpcDownloadData(&ctx, (u8 *)out + size, cap - 1 - size, &got);
size += got;
} while (rc == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING && size < cap - 1);
out[size] = 0;
*outLen = size;
if (rc == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING) rc = 0; // Puffer voll: abgeschnitten
}
httpcCloseContext(&ctx);
memset(formBuf, 0, sizeof(formBuf)); // Passwort nicht im Speicher liegen lassen
if (rcOut) *rcOut = rc;
return code;
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <3ds/types.h>
#include <stdbool.h>
#include <stddef.h>
// HTTPS zum MBMNET-Server mit der eigenen MBMNET-CA (dieselbe wie für Miiverse, siehe olvpatch.c).
// Für alles mit Geheimnissen (Log-Passwort, Anmelde-Nachweis): nie im Klartext übers Netz.
// form: POST-Daten (application/x-www-form-urlencoded), "" = POST ohne Inhalt, NULL = GET
// Achtung: Der HTTP-Dienst des 3DS schickte POST-Inhalt bei uns nicht mit - Geheimnisse in Kopfzeilen.
// header: zusätzliche Kopfzeile "Name" mit Wert value (NULL = keine)
// date: bekommt die Date-Kopfzeile der Antwort (NULL = egal)
// Gibt den HTTP-Status zurück, 0 bei Netzwerkfehler (*rcOut = Code). out wird mit 0 abgeschlossen.
u32 https_request(const char *server, const char *path, const char *form, const char *header, const char *value,
char *out, u32 cap, u32 *outLen, char *date, size_t dateLen, Result *rcOut);
+542
View File
@@ -0,0 +1,542 @@
// Werkzeuge > Logs: Log-Zeilen aller MBMNET-Server, wie auf der Log-Webseite (MBMNET-Tools/logserver).
//
// Der Log-Server ist nur übers Accounting erreichbar (/mbmnet/logs/..., logsproxy.js), und zwar nur über
// HTTPS mit der MBMNET-CA (dieselbe wie für Miiverse): Das Log-Passwort geht nie im Klartext übers Netz.
// Oben die Zeilen (neueste unten, Steuerkreuz blättert, ganz oben werden ältere nachgeladen), unten die
// Quellen zum Filtern. Ist man ganz unten, lädt die Ansicht alle 5 Sekunden neu (wie "live" im Browser).
#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include "logview.h"
#include "https.h"
#include "ui.h"
#define TOKEN_FILE "sdmc:/3ds/mbmnet/logs-token"
#define MAX_LINES 600
#define MAX_SOURCES 48
#define ROW_H 13
#define ROWS 16
#define SRC_ROW_H 24
#define SRC_ROWS 7
#define RESP_MAX (192 * 1024)
typedef struct {
u32 id;
u64 t; // ms seit 1970 (UTC, vom Server)
char level; // 'i', 'w', 'e'
char src[28]; // Quelle (ohne Host)
char msg[404];
} Line;
typedef struct {
char key[80], name[32], host[32];
bool online;
u32 errors, warns;
} Source;
typedef enum { LV_NEED_PASSWORD, LV_LOADING, LV_READY, LV_ERROR } LvState;
enum { JOB_NONE, JOB_LOGIN, JOB_RELOAD, JOB_OLDER };
// --- geteilt mit dem Lade-Thread (lock) ---
static LightLock lock;
static LightEvent wake;
static Thread thread;
static volatile bool quit, busy;
static volatile int job;
static char server[64], token[72], password[72];
static Line *lines;
static int lineCount;
static Source sources[MAX_SOURCES];
static int sourceCount;
static volatile LvState state = LV_LOADING;
static char status[128];
static s64 timeOffset; // Sekunden: Konsolenuhr - Serveruhr (für die Uhrzeit der Zeilen)
static u64 lastLoad; // osGetTime() des letzten Ladens
// --- nur Hauptthread ---
static int cursor; // gewählte Zeile
static int top; // erste sichtbare Zeile
static int srcSel; // 0 = alle, sonst sources[srcSel - 1]
static int srcTop;
static bool errorsOnly, detail, following = true;
static char filterKey[80]; // gewählte Quelle (key), leer = alle
// --- HTTP ----------------------------------------------------------------------------
static int parse_month(const char *m)
{
static const char *names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
for (int i = 0; i < 12; i++) if (!strncmp(m, names[i], 3)) return i + 1;
return 0;
}
// Tage seit 1970-01-01 (proleptischer gregorianischer Kalender)
static s64 days_from_civil(int y, int m, int d)
{
y -= m <= 2;
s64 era = (y >= 0 ? y : y - 399) / 400;
s64 yoe = y - era * 400;
s64 doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
s64 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + doe - 719468;
}
// Server-Uhrzeit aus der Date-Kopfzeile ("Mon, 26 Oct 2026 12:34:56 GMT") -> Abstand zur Konsolenuhr
static void sync_clock(const char *date)
{
int d, y, hh, mm, ss;
char mon[4] = "";
if (sscanf(date, "%*3s, %d %3s %d %d:%d:%d", &d, mon, &y, &hh, &mm, &ss) != 6) return;
int m = parse_month(mon);
if (!m) return;
s64 serverNow = days_from_civil(y, m, d) * 86400 + hh * 3600 + mm * 60 + ss;
timeOffset = (s64)time(NULL) - serverNow;
}
// HTTPS-Anfrage ans Accounting (https.c, MBMNET-CA). Status 0 = keine Verbindung (status bekommt den Grund).
static u32 request(const char *path, const char *form, char *out, u32 cap, u32 *outLen)
{
char date[64];
Result rc = 0;
u32 code = https_request(server, path, form, token[0] ? "X-Logs-Token" : NULL, token, out, cap, outLen, date, sizeof(date), &rc);
if (date[0]) sync_clock(date);
if (!code) snprintf(status, sizeof(status), rc == -1 ? "MBMNET-Zertifikat fehlt (Server nicht erreichbar?)"
: "Keine Verbindung (%08lX)", (unsigned long)rc);
return code;
}
static void url_encode(char *out, size_t len, const char *in)
{
static const char hex[] = "0123456789ABCDEF";
size_t n = 0;
for (; *in && n + 4 < len; in++) {
unsigned char c = *in;
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') out[n++] = c;
else { out[n++] = '%'; out[n++] = hex[c >> 4]; out[n++] = hex[c & 15]; }
}
out[n] = 0;
}
static void save_token(const char *t)
{
mkdir("sdmc:/3ds/mbmnet", 0777);
FILE *f = fopen(TOKEN_FILE, "w");
if (f) { fputs(t, f); fclose(f); }
}
static void load_token(void)
{
token[0] = 0;
FILE *f = fopen(TOKEN_FILE, "r");
if (!f) return;
if (fgets(token, sizeof(token), f)) token[strcspn(token, "\r\n")] = 0;
fclose(f);
if (strlen(token) != 64) token[0] = 0;
}
static void forget_token(void)
{
token[0] = 0;
remove(TOKEN_FILE);
}
// Zeile mit Tabs zerlegen
static int split(char *line, char **f, int max)
{
int n = 0;
while (n < max) {
f[n++] = line;
char *tab = strchr(line, '\t');
if (!tab) break;
*tab = 0;
line = tab + 1;
}
return n;
}
static void parse_line(Line *l, char **f)
{
l->id = strtoul(f[0], NULL, 10);
l->t = strtoull(f[1], NULL, 10);
l->level = f[2][0] == 'e' ? 'e' : f[2][0] == 'w' ? 'w' : 'i';
char *bar = strchr(f[3], '|'); // key = "quelle|host"
if (bar) *bar = 0;
snprintf(l->src, sizeof(l->src), "%s", f[3]);
snprintf(l->msg, sizeof(l->msg), "%s", f[4]);
}
// --- Lade-Thread ---------------------------------------------------------------------
static bool load_sources(char *buf)
{
u32 len;
u32 code = request("/mbmnet/logs/sources", NULL, buf, RESP_MAX, &len);
if (code == 401) return false;
if (code != 200) { if (code) snprintf(status, sizeof(status), "Fehler beim Laden (HTTP %lu)", (unsigned long)code); return true; }
Source tmp[MAX_SOURCES];
int n = 0;
char *save = NULL, *f[8];
for (char *line = strtok_r(buf, "\n", &save); line && n < MAX_SOURCES; line = strtok_r(NULL, "\n", &save)) {
if (split(line, f, 8) < 6) continue;
Source *s = &tmp[n++];
snprintf(s->key, sizeof(s->key), "%s", f[0]);
snprintf(s->name, sizeof(s->name), "%s", f[1]);
snprintf(s->host, sizeof(s->host), "%s", f[2]);
s->online = f[3][0] == '1';
s->errors = strtoul(f[4], NULL, 10);
s->warns = strtoul(f[5], NULL, 10);
}
LightLock_Lock(&lock);
memcpy(sources, tmp, sizeof(Source) * n);
sourceCount = n;
LightLock_Unlock(&lock);
return true;
}
// older = vor der ältesten Zeile weiterlesen (sonst die neuesten). false = nicht angemeldet.
static bool load_lines(char *buf, bool older)
{
char path[200], key[96];
url_encode(key, sizeof(key), filterKey);
LightLock_Lock(&lock);
u32 before = older && lineCount ? lines[0].id : 0;
LightLock_Unlock(&lock);
snprintf(path, sizeof(path), "/mbmnet/logs/lines?limit=%d%s%s%s%s", older ? 100 : 200, key[0] ? "&source=" : "", key,
errorsOnly ? "&level=error" : "", "");
if (before) snprintf(path + strlen(path), sizeof(path) - strlen(path), "&before=%lu", (unsigned long)before);
u32 len;
u32 code = request(path, NULL, buf, RESP_MAX, &len);
if (code == 401) return false;
if (code != 200) { if (code) snprintf(status, sizeof(status), "Fehler beim Laden (HTTP %lu)", (unsigned long)code); return true; }
// Neue Zeilen zählen und einlesen
int n = 0;
for (char *p = buf; *p; p++) if (*p == '\n') n++;
if (len && buf[len - 1] != '\n') n++;
Line *fresh = n ? malloc(sizeof(Line) * n) : NULL;
int got = 0;
char *save = NULL, *f[6];
for (char *line = strtok_r(buf, "\n", &save); line && got < n; line = strtok_r(NULL, "\n", &save)) {
if (split(line, f, 6) < 5) continue;
parse_line(&fresh[got++], f);
}
LightLock_Lock(&lock);
if (older) {
// vorne anhängen, höchstens MAX_LINES (die neuesten fallen dann weg)
int keep = lineCount;
if (got + keep > MAX_LINES) keep = MAX_LINES - got;
if (keep < 0) keep = 0;
memmove(lines + got, lines, sizeof(Line) * keep);
memcpy(lines, fresh, sizeof(Line) * (got > MAX_LINES ? MAX_LINES : got));
lineCount = got + keep;
cursor += got;
top += got;
} else {
if (got > MAX_LINES) got = MAX_LINES;
memcpy(lines, fresh, sizeof(Line) * got);
lineCount = got;
}
LightLock_Unlock(&lock);
free(fresh);
snprintf(status, sizeof(status), "%s", "");
lastLoad = osGetTime();
return true;
}
static void worker(void *arg)
{
(void)arg;
char *buf = malloc(RESP_MAX);
while (!quit) {
LightEvent_Wait(&wake);
if (quit) break;
int j = job;
job = JOB_NONE;
if (j == JOB_NONE || !buf) continue;
busy = true;
if (j == JOB_LOGIN) {
// Passwort in der Kopfzeile X-Logs-Password (URL-kodiert, nur HTTPS). Als POST-Inhalt kam es nicht an:
// Der HTTP-Dienst des 3DS schickte 0 Bytes.
char enc[200];
url_encode(enc, sizeof(enc), password);
memset(password, 0, sizeof(password));
token[0] = 0;
u32 len;
char date[64];
Result hrc = 0;
u32 code = https_request(server, "/mbmnet/logs/login", "", "X-Logs-Password", enc, buf, 256, &len, date, sizeof(date), &hrc);
memset(enc, 0, sizeof(enc));
if (date[0]) sync_clock(date);
if (!code) snprintf(status, sizeof(status), "Keine Verbindung (%08lX)", (unsigned long)hrc);
if (code == 200 && len == 64) {
memcpy(token, buf, 64);
token[64] = 0;
save_token(token);
j = JOB_RELOAD;
} else {
if (code == 401) snprintf(status, sizeof(status), "Falsches Passwort.");
else if (code == 429) snprintf(status, sizeof(status), "Zu viele Versuche, 15 Minuten warten.");
else if (code) snprintf(status, sizeof(status), "Anmeldung fehlgeschlagen (HTTP %lu)", (unsigned long)code);
// code 0: status sagt schon, warum keine Verbindung
state = code == 401 ? LV_NEED_PASSWORD : LV_ERROR;
busy = false;
continue;
}
}
bool ok = j == JOB_OLDER ? load_lines(buf, true) : load_sources(buf) && load_lines(buf, false);
if (!ok) { forget_token(); state = LV_NEED_PASSWORD; snprintf(status, sizeof(status), "Bitte anmelden."); }
else state = LV_READY;
busy = false;
}
free(buf);
}
static void start_job(int j)
{
if (busy || job != JOB_NONE) return;
job = j;
if (state != LV_READY) state = LV_LOADING;
LightEvent_Signal(&wake);
}
void logview_open(const char *srv)
{
snprintf(server, sizeof(server), "%s", srv);
if (!lines) lines = calloc(MAX_LINES, sizeof(Line));
if (!thread) {
LightLock_Init(&lock);
LightEvent_Init(&wake, RESET_ONESHOT);
s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
thread = threadCreate(worker, NULL, 32 * 1024, prio + 1, -2, false);
}
detail = false;
following = true;
load_token();
status[0] = 0;
if (!token[0]) { state = LV_NEED_PASSWORD; return; }
state = LV_LOADING;
start_job(JOB_RELOAD);
}
bool logview_finish(void)
{
if (!thread) return true;
quit = true;
LightEvent_Signal(&wake);
if (R_FAILED(threadJoin(thread, 100ULL * 1000 * 1000))) return false;
threadFree(thread);
thread = NULL;
free(lines);
lines = NULL;
return true;
}
// --- Eingaben --------------------------------------------------------------------------
static u64 srcPendingAt; // Quelle mit dem Steuerkreuz gewechselt: kurz warten, dann laden (nicht bei jedem Schritt)
// Quelle wählen. now = sofort laden (L/R, Antippen), sonst nach einer kurzen Pause (Steuerkreuz)
static void select_source(int sel, bool now)
{
if (sel < 0) sel = sourceCount;
if (sel > sourceCount) sel = 0;
srcSel = sel;
snprintf(filterKey, sizeof(filterKey), "%s", sel ? sources[sel - 1].key : "");
if (srcSel < srcTop) srcTop = srcSel;
if (srcSel >= srcTop + SRC_ROWS) srcTop = srcSel - SRC_ROWS + 1;
following = true;
if (now) { srcPendingAt = 0; start_job(JOB_RELOAD); }
else srcPendingAt = osGetTime();
}
static bool ask_password(void)
{
SwkbdState kb;
char input[64] = "";
swkbdInit(&kb, SWKBD_TYPE_NORMAL, 2, sizeof(input) - 1);
swkbdSetPasswordMode(&kb, SWKBD_PASSWORD_HIDE_DELAY);
swkbdSetHintText(&kb, "Passwort der Log-Seite");
swkbdSetButton(&kb, SWKBD_BUTTON_LEFT, "Abbrechen", false);
swkbdSetButton(&kb, SWKBD_BUTTON_RIGHT, "Anmelden", true);
if (swkbdInputText(&kb, input, sizeof(input)) != SWKBD_BUTTON_RIGHT || !input[0]) return false;
snprintf(password, sizeof(password), "%s", input);
memset(input, 0, sizeof(input));
state = LV_LOADING;
snprintf(status, sizeof(status), "Melde an ...");
job = JOB_LOGIN;
LightEvent_Signal(&wake);
return true;
}
typedef struct { float x, y, w, h; } Rect;
// Knopfleiste unten: Live | Nur Fehler | Neu laden | Zurück
static const Rect btnLive = { 6, 204, 74, 30 }, btnFilter = { 84, 204, 76, 30 }, btnReload = { 164, 204, 74, 30 },
btnBack = { 242, 204, 72, 30 };
static bool in(Rect r, touchPosition tp) { return tp.px >= r.x && tp.px < r.x + r.w && tp.py >= r.y && tp.py < r.y + r.h; }
bool logview_update(u32 down, u32 repeat, u32 held)
{
(void)held;
if (state == LV_NEED_PASSWORD && !busy && job == JOB_NONE) {
if (!ask_password()) return false; // abgebrochen: zurück zu den Werkzeugen
}
touchPosition tp = { 0 };
if (down & KEY_TOUCH) hidTouchRead(&tp);
if (down & KEY_B || (down & KEY_TOUCH && in(btnBack, tp))) {
if (detail) detail = false;
else return false;
}
if (down & KEY_A) detail = !detail && lineCount > 0;
if (down & KEY_X || (down & KEY_TOUCH && in(btnFilter, tp))) { errorsOnly = !errorsOnly; following = true; start_job(JOB_RELOAD); }
// Live: ans Ende springen, neu laden und wieder mitlaufen (alle 5 Sekunden neu)
if (down & KEY_Y || (down & KEY_TOUCH && in(btnLive, tp))) { following = true; detail = false; start_job(JOB_RELOAD); }
if (down & KEY_TOUCH && in(btnReload, tp)) start_job(JOB_RELOAD);
if (down & KEY_R) select_source(srcSel + 1, true);
if (down & KEY_L) select_source(srcSel - 1, true);
// Steuerkreuz (unten): Quellen auf dem unteren Bildschirm
if (repeat & KEY_DDOWN) select_source(srcSel + 1, false);
if (repeat & KEY_DUP) select_source(srcSel - 1, false);
if (srcPendingAt && osGetTime() - srcPendingAt > 400 && !busy && job == JOB_NONE) { srcPendingAt = 0; start_job(JOB_RELOAD); }
if (down & KEY_TOUCH && tp.py >= 24 && tp.py < 24 + SRC_ROWS * SRC_ROW_H) {
int row = srcTop + (tp.py - 24) / SRC_ROW_H;
if (row <= sourceCount) select_source(row, true);
}
LightLock_Lock(&lock);
int count = lineCount;
// Schiebepad (oben): Zeilen auf dem oberen Bildschirm, links/rechts seitenweise
bool moved = false;
if (!detail) {
if (repeat & KEY_CPAD_UP) {
moved = true;
if (cursor > 0) cursor--;
else if (count && state == LV_READY) start_job(JOB_OLDER); // ganz oben: ältere nachladen
}
if (repeat & KEY_CPAD_DOWN) { cursor++; moved = true; }
if (repeat & KEY_CPAD_LEFT) { cursor -= ROWS; moved = true; }
if (repeat & KEY_CPAD_RIGHT) { cursor += ROWS; moved = true; }
}
// Live mitlaufen nur, solange man nicht selbst blättert (sonst sprang es sofort wieder nach unten)
if (following && !moved) cursor = count - 1;
if (cursor >= count) cursor = count - 1;
if (cursor < 0) cursor = 0;
following = count == 0 || cursor == count - 1;
if (cursor < top) top = cursor;
if (cursor >= top + ROWS) top = cursor - ROWS + 1;
if (top > count - ROWS) top = count - ROWS;
if (top < 0) top = 0;
LightLock_Unlock(&lock);
// Live: unten angekommen, alle 5 Sekunden neu laden
if (following && state == LV_READY && osGetTime() - lastLoad > 5000) start_job(JOB_RELOAD);
return true;
}
// --- Zeichnen ---------------------------------------------------------------------------
static u32 level_color(char level) { return level == 'e' ? COL_RED : level == 'w' ? COL_AMBER : COL_CARD_HI; }
void logview_draw_top(void)
{
C2D_DrawRectSolid(0, 0, 0, TOP_W, SCR_H, COL_BG);
C2D_DrawRectSolid(0, 0, 0.5f, TOP_W, 20, COL_BG2);
char head[96], line[160];
snprintf(head, sizeof(head), "Logs · %s%s", srcSel ? sources[srcSel - 1].name : "alle Quellen", errorsOnly ? " · nur Fehler" : "");
ui_text(head, 6, 3, 0.45f, COL_WHITE, 0);
const char *right = busy || state == LV_LOADING ? "lädt ..." : status[0] ? status : following ? "live" : "angehalten";
ui_fit(line, sizeof(line), right, 0.38f, 180);
ui_text(line, TOP_W - 6, 4, 0.38f, status[0] ? COL_AMBER : following ? COL_GREEN : COL_MUTED, C2D_AlignRight);
if (state == LV_NEED_PASSWORD) {
ui_text_wrap(status[0] ? status : "Anmeldung mit dem Passwort der Log-Seite.", 20, 100, 0.5f, COL_MUTED, TOP_W - 40);
return;
}
LightLock_Lock(&lock);
if (!lineCount) {
ui_text(state == LV_LOADING ? "Einen Moment ..." : "Keine Zeilen.", TOP_W / 2, 110, 0.5f, COL_MUTED, C2D_AlignCenter);
LightLock_Unlock(&lock);
return;
}
for (int r = 0; r < ROWS && top + r < lineCount; r++) {
const Line *l = &lines[top + r];
float y = 22 + r * ROW_H;
if (top + r == cursor) C2D_DrawRectSolid(0, y, 0.5f, TOP_W, ROW_H, COL_CARD);
time_t when = (time_t)(l->t / 1000 + timeOffset);
struct tm *tm = gmtime(&when);
char hhmm[12];
snprintf(hhmm, sizeof(hhmm), "%02d:%02d:%02d", tm ? tm->tm_hour : 0, tm ? tm->tm_min : 0, tm ? tm->tm_sec : 0);
ui_text(hhmm, 4, y + 1, 0.34f, COL_MUTED, 0);
C2D_DrawRectSolid(46, y + 1, 0.5f, 3, ROW_H - 2, level_color(l->level));
ui_fit(line, sizeof(line), l->src, 0.34f, 56);
ui_text(line, 53, y + 1, 0.34f, COL_MUTED, 0);
ui_fit(line, sizeof(line), l->msg, 0.36f, TOP_W - 116);
ui_text(line, 112, y + 1, 0.36f, l->level == 'e' ? C2D_Color32(0xFF, 0x8A, 0x8A, 0xFF) : COL_WHITE, 0);
}
if (detail && cursor < lineCount) {
const Line *l = &lines[cursor];
ui_round_rect(14, 30, TOP_W - 28, 200, 10, COL_BG2);
C2D_DrawRectSolid(14, 40, 0.5f, 4, 180, level_color(l->level));
snprintf(head, sizeof(head), "%s · Zeile %lu", l->src, (unsigned long)l->id);
ui_text(head, 26, 36, 0.42f, COL_MUTED, 0);
ui_text_wrap(l->msg, 26, 56, 0.42f, COL_WHITE, TOP_W - 60);
}
LightLock_Unlock(&lock);
}
static void button(Rect r, const char *label, bool on)
{
ui_round_rect(r.x, r.y, r.w, r.h, 10, on ? COL_RED : COL_CARD);
ui_text(label, r.x + r.w / 2, r.y + 8, 0.4f, COL_WHITE, C2D_AlignCenter);
}
static void live_button(void)
{
Rect r = btnLive;
ui_round_rect(r.x, r.y, r.w, r.h, 10, following ? C2D_Color32(0x15, 0x80, 0x3D, 0xFF) : COL_CARD);
C2D_DrawCircleSolid(r.x + 14, r.y + r.h / 2, 0.5f, 4, following ? COL_WHITE : COL_MUTED);
ui_text("Live", r.x + r.w / 2 + 6, r.y + 8, 0.4f, following ? COL_WHITE : COL_MUTED, C2D_AlignCenter);
}
void logview_draw_bottom(void)
{
C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG);
C2D_DrawRectSolid(0, 0, 0.5f, BOT_W, 22, COL_BG2);
ui_text("Quellen", 6, 3, 0.45f, COL_WHITE, 0);
ui_text("Steuerkreuz/L/R wechseln · Y Live · A Details", BOT_W - 6, 5, 0.32f, COL_MUTED, C2D_AlignRight);
char line[96];
LightLock_Lock(&lock);
for (int r = 0; r < SRC_ROWS && srcTop + r <= sourceCount; r++) {
int i = srcTop + r;
float y = 24 + r * SRC_ROW_H;
if (i == srcSel) {
C2D_DrawRectSolid(0, y, 0.5f, BOT_W, SRC_ROW_H, COL_CARD_HI);
C2D_DrawRectSolid(0, y + 4, 0.5f, 3, SRC_ROW_H - 8, COL_RED);
}
if (i == 0) { ui_text("Alle Quellen", 14, y + 5, 0.45f, COL_WHITE, 0); continue; }
const Source *s = &sources[i - 1];
C2D_DrawCircleSolid(14, y + SRC_ROW_H / 2, 0.5f, 4, s->online ? COL_GREEN : COL_MUTED);
snprintf(line, sizeof(line), "%s · %s", s->name, s->host);
char fitted[96];
ui_fit(fitted, sizeof(fitted), line, 0.42f, s->errors ? 200 : 270);
ui_text(fitted, 24, y + 5, 0.42f, COL_WHITE, 0);
if (s->errors) {
snprintf(line, sizeof(line), "%lu Fehler", (unsigned long)s->errors);
float w = ui_text_width(line, 0.36f) + 12;
ui_round_rect(BOT_W - 8 - w, y + 4, w, 16, 8, COL_RED);
ui_text(line, BOT_W - 8 - w / 2, y + 5, 0.36f, COL_WHITE, C2D_AlignCenter);
}
}
LightLock_Unlock(&lock);
live_button();
button(btnFilter, errorsOnly ? "Alle" : "Nur Fehler", errorsOnly);
button(btnReload, "Neu laden", false);
button(btnBack, detail ? "Schließen" : "Zurück", false);
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <3ds.h>
#include <stdbool.h>
// Werkzeuge > Logs: die Logs aller MBMNET-Server (Log-Server aus MBMNET-Tools) über das Accounting
// (/mbmnet/logs/..., nur HTTPS mit der MBMNET-CA). Anmeldung mit dem Log-Passwort, das Sitzungs-Token
// liegt danach auf der SD (/3ds/mbmnet/logs-token, 7 Tage gültig).
void logview_open(const char *server);
// Eingaben verarbeiten. false = Ansicht schließen.
bool logview_update(u32 down, u32 repeat, u32 held);
void logview_draw_top(void);
void logview_draw_bottom(void);
// Hintergrund-Thread beenden (beim Schließen der App). false = läuft noch (siehe main.c)
bool logview_finish(void);
+259 -94
View File
@@ -1,7 +1,7 @@
// MBMNET Switcher - schaltet den 3DS zwischen MBMNET und Nintendo (Original) um. // MBMNET Switcher - schaltet den 3DS zwischen MBMNET und Nintendo (Original) um.
// //
// Unten zwei Kacheln: MBMNET und Nintendo. Antippen schaltet um, unter der aktiven steht "Aktiv". // Unten zwei Kacheln: MBMNET und Nintendo. Antippen schaltet um, unter der aktiven steht "Aktiv".
// SELECT blendet die Entwickler-Werkzeuge ein und aus (beim Öffnen immer aus). // SELECT öffnet für Admins (vom Server bestätigt) eine eigene Werkzeug-Seite, darin u. a. die Logs.
#include <3ds.h> #include <3ds.h>
#include <citro2d.h> #include <citro2d.h>
#include <stdio.h> #include <stdio.h>
@@ -18,27 +18,13 @@
#include "ftpctl.h" #include "ftpctl.h"
#include "spotpass.h" #include "spotpass.h"
#include "shopctl.h" #include "shopctl.h"
#include "olvpatch.h"
#include "logview.h"
#include "ui.h"
#define TOP_W 400 // Bedienelemente auf dem unteren Bildschirm. Normal: MBMNET / Nintendo und die Shop-Leiste.
#define BOT_W 320 // Werkzeug-Seite (nur Admins, SELECT): sechs große Knöpfe statt der Kacheln.
#define SCR_H 240 enum { BTN_ENABLE, BTN_DISABLE, BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_REBOOT, BTN_LOGS, BTN_SHOP, BTN_SHOP_DEL, BTN_COUNT };
// Farben (gleiche Palette wie Logo und Webseite)
#define COL_RED C2D_Color32(0xE4, 0x00, 0x2B, 0xFF)
#define COL_RED_DARK C2D_Color32(0x9E, 0x00, 0x1E, 0xFF)
#define COL_BG C2D_Color32(0x16, 0x19, 0x1F, 0xFF)
#define COL_BG2 C2D_Color32(0x22, 0x26, 0x2E, 0xFF)
#define COL_CARD C2D_Color32(0x2A, 0x2F, 0x39, 0xFF)
#define COL_CARD_HI C2D_Color32(0x3A, 0x41, 0x4E, 0xFF)
#define COL_WHITE C2D_Color32(0xFF, 0xFF, 0xFF, 0xFF)
#define COL_MUTED C2D_Color32(0x9A, 0xA3, 0xAF, 0xFF)
#define COL_GREEN C2D_Color32(0x22, 0xC5, 0x5E, 0xFF)
#define COL_AMBER C2D_Color32(0xF5, 0x9E, 0x0B, 0xFF)
#define COL_NINTENDO C2D_Color32(0xE6, 0x00, 0x12, 0xFF)
// Bedienelemente auf dem unteren Bildschirm. Die Werkzeuge gibt es nur für Admins (mit SELECT),
// die Shop-Leiste nur ohne Werkzeuge.
enum { BTN_ENABLE, BTN_DISABLE, BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_REBOOT, BTN_SHOP, BTN_SHOP_DEL, BTN_COUNT };
typedef struct { float x, y, w, h; } Box; typedef struct { float x, y, w, h; } Box;
@@ -46,8 +32,13 @@ typedef struct { float x, y, w, h; } Box;
#define TILE_W 142 #define TILE_W 142
#define TILE_H_FULL 166 #define TILE_H_FULL 166
#define TILE_H_ADMIN 160 #define TILE_H_ADMIN 160
#define TOOL_COUNT 5 #define TOOL_COUNT 6
static const Box toolBoxes[TOOL_COUNT] = { { 12, 184, 56, 42 }, { 72, 184, 56, 42 }, { 132, 184, 56, 42 }, { 192, 184, 56, 42 }, { 252, 184, 56, 42 } }; // Werkzeug-Seite: 2 Spalten, 3 Reihen, in dieser Reihenfolge
static const int toolOrder[TOOL_COUNT] = { BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_LOGS, BTN_REBOOT };
static const Box toolBoxes[TOOL_COUNT] = { { 12, 14, 144, 66 }, { 164, 14, 144, 66 }, { 12, 86, 144, 66 },
{ 164, 86, 144, 66 }, { 12, 158, 144, 66 }, { 164, 158, 144, 66 } };
static int tool_index(int btn) { for (int i = 0; i < TOOL_COUNT; i++) if (toolOrder[i] == btn) return i; return -1; }
static bool logsOpen; // Werkzeuge > Logs ist offen
static const Box shopBox = { 12, 188, 216, 40 }, shopDelBox = { 232, 188, 76, 40 }; static const Box shopBox = { 12, 188, 216, 40 }, shopDelBox = { 232, 188, 76, 40 };
static int shopVersion = -1; // installierte Version des MBMNET Shops (-1 = nicht installiert) static int shopVersion = -1; // installierte Version des MBMNET Shops (-1 = nicht installiert)
@@ -55,8 +46,8 @@ static int shopVersion = -1; // installierte Version des MBMNET Shops (-1 = ni
// Welche Knöpfe sind gerade da? // Welche Knöpfe sind gerade da?
static bool visible(int btn, bool admin) static bool visible(int btn, bool admin)
{ {
if (btn <= BTN_DISABLE) return true; if (btn <= BTN_DISABLE) return !admin; // auf der Werkzeug-Seite keine Kacheln
if (btn <= BTN_REBOOT) return admin; if (btn <= BTN_LOGS) return admin;
if (btn == BTN_SHOP) return !admin; if (btn == BTN_SHOP) return !admin;
return !admin && shopVersion >= 0; // Entfernen nur, wenn installiert return !admin && shopVersion >= 0; // Entfernen nur, wenn installiert
} }
@@ -67,13 +58,9 @@ static Box box_of(int btn, bool admin)
switch (btn) { switch (btn) {
case BTN_ENABLE: return (Box){ 12, TILE_Y, TILE_W, h }; case BTN_ENABLE: return (Box){ 12, TILE_Y, TILE_W, h };
case BTN_DISABLE: return (Box){ 166, TILE_Y, TILE_W, h }; case BTN_DISABLE: return (Box){ 166, TILE_Y, TILE_W, h };
case BTN_DIAG: return toolBoxes[0];
case BTN_FTP: return toolBoxes[1];
case BTN_UNLINK: return toolBoxes[2];
case BTN_SPOTPASS: return toolBoxes[3];
case BTN_SHOP: return shopBox; case BTN_SHOP: return shopBox;
case BTN_SHOP_DEL: return shopDelBox; case BTN_SHOP_DEL: return shopDelBox;
default: return toolBoxes[4]; default: return toolBoxes[tool_index(btn) < 0 ? 0 : tool_index(btn)];
} }
} }
@@ -91,6 +78,41 @@ static void text(const char *s, float x, float y, float size, u32 color, u32 fla
C2D_DrawText(&t, C2D_WithColor | flags, x, y, 0.5f, size, size, color); C2D_DrawText(&t, C2D_WithColor | flags, x, y, 0.5f, size, size, color);
} }
// Für logview.c (ui.h)
void ui_text(const char *s, float x, float y, float size, u32 color, u32 flags) { text(s, x, y, size, color, flags); }
void ui_text_wrap(const char *s, float x, float y, float size, u32 color, float width)
{
C2D_Text t;
C2D_TextParse(&t, textBuf, s);
C2D_TextOptimize(&t);
C2D_DrawText(&t, C2D_WithColor | C2D_WordWrap, x, y, 0.5f, size, size, color, width);
}
static C2D_TextBuf measureBuf; // nur zum Messen, wird jedes Mal geleert
float ui_text_width(const char *s, float size)
{
C2D_Text t;
float w = 0;
C2D_TextBufClear(measureBuf);
C2D_TextParse(&t, measureBuf, s);
C2D_TextGetDimensions(&t, size, size, &w, NULL);
return w;
}
void ui_fit(char *out, size_t len, const char *s, float size, float width)
{
snprintf(out, len, "%s", s);
if (ui_text_width(out, size) <= width) return;
size_t n = strlen(out);
while (n > 1) {
n--;
while (n > 0 && (out[n] & 0xC0) == 0x80) n--; // nicht mitten in einem UTF-8-Zeichen
snprintf(out + n, len - n, "...");
if (ui_text_width(out, size) <= width) return;
}
}
static void round_rect(float x, float y, float w, float h, float r, u32 color) static void round_rect(float x, float y, float w, float h, float r, u32 color)
{ {
C2D_DrawRectSolid(x + r, y, 0.5f, w - 2 * r, h, color); C2D_DrawRectSolid(x + r, y, 0.5f, w - 2 * r, h, color);
@@ -100,6 +122,7 @@ static void round_rect(float x, float y, float w, float h, float r, u32 color)
C2D_DrawCircleSolid(x + r, y + h - r, 0.5f, r, color); C2D_DrawCircleSolid(x + r, y + h - r, 0.5f, r, color);
C2D_DrawCircleSolid(x + w - r, y + h - r, 0.5f, r, color); C2D_DrawCircleSolid(x + w - r, y + h - r, 0.5f, r, color);
} }
void ui_round_rect(float x, float y, float w, float h, float r, u32 color) { round_rect(x, y, w, h, r, color); }
// Das MBMNET-Logo: rotes abgerundetes Quadrat mit einem "M" aus verbundenen Netzwerk-Knoten. // Das MBMNET-Logo: rotes abgerundetes Quadrat mit einem "M" aus verbundenen Netzwerk-Knoten.
// Geometrie identisch zu assets/logo.svg (100x100-Raster). // Geometrie identisch zu assets/logo.svg (100x100-Raster).
@@ -154,7 +177,8 @@ static void draw_top(PatchState st, const char *msg, bool admin, bool isAdmin)
{ {
C2D_DrawRectangle(0, 0, 0, TOP_W, SCR_H, COL_BG2, COL_BG2, COL_BG, COL_BG); C2D_DrawRectangle(0, 0, 0, TOP_W, SCR_H, COL_BG2, COL_BG2, COL_BG, COL_BG);
draw_update_line(admin); draw_update_line(admin);
const char *hint = admin ? "START: Beenden SELECT: Werkzeuge aus" : "START: Beenden SELECT: Werkzeuge"; const char *hint = admin ? "START: Beenden Y: ID lösen SELECT: Werkzeuge aus"
: isAdmin ? "START: Beenden Y: ID lösen SELECT: Werkzeuge" : "START: Beenden Y: ID lösen";
text(hint, 10, 4, 0.4f, COL_MUTED, 0); text(hint, 10, 4, 0.4f, COL_MUTED, 0);
if (ftpOn) { if (ftpOn) {
// FTP-Adresse direkt daneben // FTP-Adresse direkt daneben
@@ -170,23 +194,32 @@ static void draw_top(PatchState st, const char *msg, bool admin, bool isAdmin)
text(ftp, 10 + w + 12, 4, 0.4f, COL_GREEN, 0); text(ftp, 10 + w + 12, 4, 0.4f, COL_GREEN, 0);
} }
float y = msg[0] || admin ? 26 : 62; // Werkzeug-Seite: Überschrift und was gerade wirklich los ist
if (admin) {
draw_logo(16, 26, 40);
text("Werkzeuge", 66, 28, 0.75f, COL_WHITE, 0);
text("Nur für Admins · B oder SELECT: zurück", 66, 52, 0.4f, COL_MUTED, 0);
char line[112], id[20];
const char *label = st == STATE_MBMNET ? "Patch: MBMNET" : st == STATE_FOREIGN ? "Patch: fremd (z. B. Pretendo)" : "Patch: keiner";
snprintf(line, sizeof(line), "%s · angemeldet: %s · v" APP_VERSION, label, nnid_linked(id, sizeof(id)) ? id : "keine ID");
round_rect(16, 76, TOP_W - 32, 22, 8, COL_CARD);
text(line, 26, 80, 0.4f, COL_MUTED, 0);
if (msg[0]) {
round_rect(16, 104, TOP_W - 32, SCR_H - 112, 10, COL_CARD);
text(msg, 28, 110, 0.46f, COL_WHITE, 0);
}
return;
}
float y = msg[0] ? 26 : 62;
draw_logo(TOP_W / 2 - 32, y, 64); draw_logo(TOP_W / 2 - 32, y, 64);
text("MBMNET", TOP_W / 2, y + 70, 0.9f, COL_WHITE, C2D_AlignCenter); text("MBMNET", TOP_W / 2, y + 70, 0.9f, COL_WHITE, C2D_AlignCenter);
y += 108; y += 108;
// Admins: was gerade wirklich los ist
if (admin) {
char line[96], id[20];
const char *label = st == STATE_MBMNET ? "Patch: MBMNET" : st == STATE_FOREIGN ? "Patch: fremd (z. B. Pretendo)" : "Patch: keiner";
snprintf(line, sizeof(line), "%s · NNID: %s%s", label, nnid_linked(id, sizeof(id)) ? id : "keine", isAdmin ? " · Admin" : "");
text(line, TOP_W / 2, y, 0.42f, COL_MUTED, C2D_AlignCenter);
y += 18;
}
if (msg[0]) { if (msg[0]) {
round_rect(16, y, TOP_W - 32, SCR_H - y - 8, 10, COL_CARD); round_rect(16, y, TOP_W - 32, SCR_H - y - 8, 10, COL_CARD);
text(msg, 28, y + 6, 0.46f, COL_WHITE, 0); text(msg, 28, y + 6, 0.46f, COL_WHITE, 0);
} else if (!admin) { } else {
text("Tippe unten auf MBMNET oder Nintendo.", TOP_W / 2, 200, 0.45f, COL_MUTED, C2D_AlignCenter); text("Tippe unten auf MBMNET oder Nintendo.", TOP_W / 2, 200, 0.45f, COL_MUTED, C2D_AlignCenter);
} }
} }
@@ -245,19 +278,29 @@ static void draw_shop_bar(int selected)
static void draw_bottom(int selected, PatchState st, bool admin) static void draw_bottom(int selected, PatchState st, bool admin)
{ {
C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG); C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG);
draw_tile(BTN_ENABLE, selected == BTN_ENABLE, st == STATE_MBMNET, admin); if (!admin) {
draw_tile(BTN_DISABLE, selected == BTN_DISABLE, st == STATE_ORIGINAL, admin); draw_tile(BTN_ENABLE, selected == BTN_ENABLE, st == STATE_MBMNET, admin);
if (!admin) { draw_shop_bar(selected); return; } draw_tile(BTN_DISABLE, selected == BTN_DISABLE, st == STATE_ORIGINAL, admin);
draw_shop_bar(selected);
const char *labels[TOOL_COUNT] = { "Diagnose", ftpOn ? "FTP aus" : "FTP an", "ID lösen", "SpotPass", "Neustart" }; return;
}
// Werkzeug-Seite: sechs große Knöpfe mit kurzer Erklärung
for (int i = 0; i < TOOL_COUNT; i++) { for (int i = 0; i < TOOL_COUNT; i++) {
int btn = toolOrder[i];
const char *label = btn == BTN_DIAG ? "Diagnose" : btn == BTN_FTP ? (ftpOn ? "FTP aus" : "FTP an")
: btn == BTN_UNLINK ? "ID lösen" : btn == BTN_SPOTPASS ? "SpotPass-Test"
: btn == BTN_LOGS ? "Logs" : "Neustart";
const char *hint = btn == BTN_DIAG ? "Patches und Konten prüfen" : btn == BTN_FTP ? (ftpOn ? "läuft" : "SD-Karte im WLAN")
: btn == BTN_UNLINK ? "nur auf dem 3DS" : btn == BTN_SPOTPASS ? "Mitteilung abholen"
: btn == BTN_LOGS ? "alle MBMNET-Server" : "3DS neu starten";
Box b = toolBoxes[i]; Box b = toolBoxes[i];
bool sel = selected == BTN_DIAG + i; bool sel = selected == btn;
bool on = i == 1 && ftpOn; bool on = btn == BTN_FTP && ftpOn;
round_rect(b.x, b.y, b.w, b.h, 10, sel ? COL_CARD_HI : COL_CARD); if (sel) round_rect(b.x - 2, b.y - 2, b.w + 4, b.h + 4, 13, COL_RED);
if (sel) C2D_DrawRectSolid(b.x, b.y + 10, 0.5f, 4, b.h - 20, COL_RED); round_rect(b.x, b.y, b.w, b.h, 11, sel ? COL_CARD_HI : COL_CARD);
if (on) C2D_DrawCircleSolid(b.x + 12, b.y + b.h / 2, 0.5f, 4, COL_GREEN); if (on) C2D_DrawCircleSolid(b.x + 14, b.y + 21, 0.5f, 4, COL_GREEN);
text(labels[i], b.x + b.w / 2, b.y + 13, 0.45f, on ? COL_GREEN : COL_WHITE, C2D_AlignCenter); text(label, b.x + b.w / 2, b.y + 14, 0.58f, on ? COL_GREEN : COL_WHITE, C2D_AlignCenter);
text(hint, b.x + b.w / 2, b.y + 40, 0.38f, COL_MUTED, C2D_AlignCenter);
} }
} }
@@ -271,30 +314,77 @@ static void draw_bottom(int selected, PatchState st, bool admin)
#define UNLINKED_MARKER "sdmc:/3ds/mbmnet/nnid-geloest" #define UNLINKED_MARKER "sdmc:/3ds/mbmnet/nnid-geloest"
static bool old_mbmnet_link(void) { FILE *f = fopen(UNLINKED_MARKER, "r"); if (f) fclose(f); return f != NULL; } static bool old_mbmnet_link(void) { FILE *f = fopen(UNLINKED_MARKER, "r"); if (f) fclose(f); return f != NULL; }
// Auffrischen beim Start (SpotPass anmelden, Patches neu bauen) läuft im Hintergrund: Dabei holt die App // Auffrischen beim Start läuft im Hintergrund, in Schritten. Tippt man auf Umschalten, bricht es ab, wo es kann:
// das Miiverse-Zertifikat vom Server, und solange das WLAN noch verbindet, dauert das Sekunden. // Das Umschalten wartet nur, solange Patch-Dateien geschrieben werden oder der Konto-Dienst beschäftigt ist.
static volatile bool maintaining; static volatile bool maintaining; // Thread läuft noch (evtl. nur noch das Zertifikat vom Server holen)
static volatile bool maintenanceBusy; // schreibt Patches / gleicht das Konto ab: Umschalten muss warten
static volatile bool switchRequested; // jemand will umschalten: nichts Neues mehr anfangen
static volatile bool maintenanceChanged; static volatile bool maintenanceChanged;
static Thread maintenanceThread; static Thread maintenanceThread;
// Erst "beschäftigt" melden, dann nachsehen, ob umgeschaltet werden soll (sonst könnten beide gleichzeitig schreiben)
static bool begin_busy(void)
{
maintenanceBusy = true;
if (!switchRequested) return true;
maintenanceBusy = false;
return false;
}
// Was das Auffrischen gerade macht (bei SELECT angezeigt, wenn es keine Werkzeuge gibt)
static char maintStep[64] = "startet";
static void maint_step(const char *step) { snprintf(maintStep, sizeof(maintStep), "%s", step); }
static bool refresh_patches(void)
{
char detail[160];
bool changed = false;
if (!begin_busy()) return false;
if (patch_enable(MBMNET_SERVER, &changed, detail, sizeof(detail)) && changed) maintenanceChanged = true;
maintenanceBusy = false;
return changed;
}
static void maintenance_worker(void *arg) static void maintenance_worker(void *arg)
{ {
(void)arg; (void)arg;
char ignore[96], detail[160]; char ignore[96];
spotpass_register(MBMNET_SERVER, ignore, sizeof(ignore)); // 1. Patches auffrischen (Miiverse-CA aus dem Zwischenspeicher; nur beim allerersten Mal vom Server)
// Konto mit dem MBMNET-Server abgleichen: holt u. a. den Mii, den man auf der Webseite eingestellt hat bool hadCa = olvpatch_ca_cached();
patch_progress = maint_step;
maint_step("Patches");
refresh_patches();
patch_progress = NULL;
maint_step("SpotPass");
// 2. SpotPass-Aufgabe anmelden
if (begin_busy()) {
spotpass_register(MBMNET_SERVER, ignore, sizeof(ignore));
maintenanceBusy = false;
}
maint_step("Konto-Abgleich");
// 3. Konto mit dem MBMNET-Server abgleichen (holt u. a. den Mii von der Webseite). Höchstens 5 s,
// bei einem Umschalt-Wunsch sofort abbrechen.
u8 slot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET); u8 slot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET);
char id[20]; char id[20];
if (slot && frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id))) { Handle done;
Handle done; if (slot && frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id)) && begin_busy()
if (R_SUCCEEDED(svcCreateEvent(&done, RESET_ONESHOT))) { && R_SUCCEEDED(svcCreateEvent(&done, RESET_ONESHOT))) {
if (R_SUCCEEDED(ACTA_SyncAccountInfo(slot, done))) svcWaitSynchronization(done, 15ULL * 1000 * 1000 * 1000); if (R_SUCCEEDED(ACTA_SyncAccountInfo(slot, done))) {
svcCloseHandle(done); // Warten in 100-ms-Schritten (eine Zeitüberschreitung ist kein Fehlercode, nur != 0)
for (int i = 0; i < 50 && svcWaitSynchronization(done, 100ULL * 1000 * 1000) != 0; i++) {
if (switchRequested) { ACT_Cancel(); svcWaitSynchronization(done, 2ULL * 1000 * 1000 * 1000); break; }
}
} }
svcCloseHandle(done);
} }
bool changed = false; maintenanceBusy = false;
// Patches auffrischen: neue Versionen bringen neue Ziele mit (z. B. Miiverse ab v2.8.0)
if (patch_enable(MBMNET_SERVER, &changed, detail, sizeof(detail)) && changed) maintenanceChanged = true; // 4. Miiverse-CA frisch vom Server (nur Netz; Patches nur neu bauen, wenn sie sich geändert hat)
bool caChanged = false;
maint_step("Miiverse-Zertifikat");
if (!switchRequested && olvpatch_refresh_ca(MBMNET_SERVER, &caChanged) && caChanged && hadCa)
refresh_patches(); // prüft selbst noch einmal, ob umgeschaltet werden soll
maint_step("fertig");
maintaining = false; maintaining = false;
} }
@@ -440,9 +530,11 @@ static bool disable(bool confirmed, char *msg, size_t msglen)
// anmelden). Den Namen merken, für den Hinweis auf den Zweitnamen. // anmelden). Den Namen merken, für den Hinweis auf den Zweitnamen.
char id[20] = ""; char id[20] = "";
if (frdswitch_current() == FRIEND_ACCOUNT_MBMNET && nnid_linked(id, sizeof(id))) remember_mbmnet_id(id); if (frdswitch_current() == FRIEND_ACCOUNT_MBMNET && nnid_linked(id, sizeof(id))) remember_mbmnet_id(id);
bool freed = free_mbmnet_name(note, sizeof(note));
bool created = false; bool created = false;
if (frdswitch_current() != FRIEND_ACCOUNT_NINTENDO && !frdswitch_to(FRIEND_ACCOUNT_NINTENDO, &created, msg, msglen)) return false; if (frdswitch_current() != FRIEND_ACCOUNT_NINTENDO && !frdswitch_to(FRIEND_ACCOUNT_NINTENDO, &created, msg, msglen)) return false;
// Erst jetzt (MBMNET-Konto nicht mehr geladen) den Namen für Pretendo freigeben. Vorher gelöst, meldete
// sich der Freunde-Dienst neu an und ließ sich nicht abmelden (D8A0C4E5).
bool freed = free_mbmnet_name(note, sizeof(note));
patch_disable(detail, sizeof(detail)); patch_disable(detail, sizeof(detail));
spotpass_unregister(); spotpass_unregister();
if (freed) snprintf(msg, msglen, "Nintendo aktiv! Der 3DS startet neu.\n%s", note); if (freed) snprintf(msg, msglen, "Nintendo aktiv! Der 3DS startet neu.\n%s", note);
@@ -456,7 +548,7 @@ static bool run(int action, char *msg, size_t msglen)
bool confirmed = armed == action; bool confirmed = armed == action;
armed = -1; armed = -1;
// Während ein Update lädt oder installiert wird, nicht umschalten (der 3DS würde mittendrin neu starten) // Während ein Update lädt oder installiert wird, nicht umschalten (der 3DS würde mittendrin neu starten)
if (action != BTN_DIAG && action != BTN_FTP && action != BTN_SPOTPASS && action < BTN_SHOP && update_pending()) { if (action != BTN_DIAG && action != BTN_FTP && action != BTN_SPOTPASS && action != BTN_LOGS && action < BTN_SHOP && update_pending()) {
snprintf(msg, msglen, "Moment: Das Update auf v%s wird gerade installiert.\nDanach startet die App neu.", update_new_version()); snprintf(msg, msglen, "Moment: Das Update auf v%s wird gerade installiert.\nDanach startet die App neu.", update_new_version());
return false; return false;
} }
@@ -465,17 +557,28 @@ static bool run(int action, char *msg, size_t msglen)
case BTN_DISABLE: return disable(confirmed, msg, msglen); case BTN_DISABLE: return disable(confirmed, msg, msglen);
case BTN_DIAG: patch_diagnose(msg, msglen); return false; case BTN_DIAG: patch_diagnose(msg, msglen); return false;
case BTN_UNLINK: { case BTN_UNLINK: {
// Verknüpfte NNID nur auf dem 3DS lösen (ohne Server) - z. B. wenn das Konto gelöscht wurde // Verknüpfte ID nur auf dem 3DS lösen (ohne Server). Bei aktivem MBMNET gezielt den MBMNET-Platz -
// nicht den, auf dem act gerade steht (der war nach einem Soft-Reboot manchmal noch der Pretendo-Platz).
char id[20]; char id[20];
if (!nnid_linked(id, sizeof(id))) { snprintf(msg, msglen, "Hier ist keine ID verknüpft."); return false; } bool mbmnet = patch_state() == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET;
u8 slot = mbmnet ? frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET) : 0;
bool linked = slot ? frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id)) : nnid_linked(id, sizeof(id));
if (!linked) { snprintf(msg, msglen, "Hier ist keine ID verknüpft."); return false; }
if (!confirmed) { if (!confirmed) {
armed = BTN_UNLINK; armed = BTN_UNLINK;
snprintf(msg, msglen, "\"%s\" vom 3DS lösen?\nNur auf dem 3DS, das Konto selbst bleibt unberührt.\n" snprintf(msg, msglen, "\"%s\"%s vom 3DS lösen?\nNur auf dem 3DS, das Konto selbst bleibt unberührt.\n"
"Zum Bestätigen nochmal antippen.", id); "Zum Bestätigen nochmal Y drücken.", id, slot ? " (MBMNET-Platz)" : "");
return false; return false;
} }
if (!nnid_unlink(msg, msglen)) return false; if (slot ? !nnid_unlink_slot(slot, msg, msglen) : !nnid_unlink(msg, msglen)) return false;
snprintf(msg, msglen, "\"%s\" gelöst. Der 3DS startet neu.\nDanach kannst du eine andere ID verknüpfen.", id); if (slot) {
char hint[160];
remember_mbmnet_id(id);
link_hint(hint, sizeof(hint));
snprintf(msg, msglen, "\"%s\" gelöst. Der 3DS startet neu.\nDanach neu verknüpfen:\n%s", id, hint);
} else {
snprintf(msg, msglen, "\"%s\" gelöst. Der 3DS startet neu.\nDanach kannst du eine andere ID verknüpfen.", id);
}
return true; return true;
} }
case BTN_FTP: case BTN_FTP:
@@ -488,6 +591,11 @@ static bool run(int action, char *msg, size_t msglen)
if (spotpass_test_start(MBMNET_SERVER, msg, msglen)) spotpassTestFrames = 0; if (spotpass_test_start(MBMNET_SERVER, msg, msglen)) spotpassTestFrames = 0;
return false; return false;
case BTN_REBOOT: return true; case BTN_REBOOT: return true;
case BTN_LOGS:
logsOpen = true;
logview_open(MBMNET_SERVER);
msg[0] = 0;
return false;
case BTN_SHOP: case BTN_SHOP:
if (shopctl_job() == SHOPJOB_RUNNING) { snprintf(msg, msglen, "Der Shop wird gerade installiert ..."); return false; } if (shopctl_job() == SHOPJOB_RUNNING) { snprintf(msg, msglen, "Der Shop wird gerade installiert ..."); return false; }
if (shopVersion >= 0 && shopVersion >= APP_TITLE_VERSION) { if (shopVersion >= 0 && shopVersion >= APP_TITLE_VERSION) {
@@ -529,7 +637,9 @@ int main(int argc, char **argv)
C2D_Prepare(); C2D_Prepare();
C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT); C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT); C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);
textBuf = C2D_TextBufNew(4096); textBuf = C2D_TextBufNew(8192); // Logs brauchen mehr Platz
hidSetRepeatParameters(18, 4); // gedrückt halten: nach 0,3 s alle 4 Bilder ein Schritt (Logs blättern)
measureBuf = C2D_TextBufNew(1024);
nnid_init(); nnid_init();
frdswitch_init(); frdswitch_init();
@@ -542,13 +652,24 @@ int main(int argc, char **argv)
UpdateState lastUpd = UPD_IDLE; UpdateState lastUpd = UPD_IDLE;
int restartIn = 0; // Frames bis zum automatischen Neustart der App nach einem Update int restartIn = 0; // Frames bis zum automatischen Neustart der App nach einem Update
int rebootIn = 0; // Frames bis zum Neustart des 3DS nach dem Umschalten int rebootIn = 0; // Frames bis zum Neustart des 3DS nach dem Umschalten
bool softReboot = false;
// Patches alter Versionen lagen an einer Stelle, an der Luma sie nicht lädt // Patches alter Versionen lagen an einer Stelle, an der Luma sie nicht lädt
if (patch_migrate()) if (patch_migrate())
snprintf(msg, sizeof(msg), "Neuer Speicherort für die Patches!\nBitte MBMNET einmal neu antippen."); snprintf(msg, sizeof(msg), "Neuer Speicherort für die Patches!\nBitte MBMNET einmal neu antippen.");
PatchState st = patch_state(); PatchState st = patch_state();
// Nur im MBMNET-Modus ist die verknüpfte NNID ein MBMNET-Konto // Nur im MBMNET-Modus ist die verknüpfte NNID ein MBMNET-Konto
admin_start(server, st == STATE_MBMNET ? nnid_pid() : 0); // Werkzeuge nur für Admins, und nur wenn der Server es per signiertem Anmelde-Nachweis bestätigt.
// Das geht nur, solange das MBMNET-Konto angemeldet ist (MBMNET aktiv, ID verknüpft).
{
char mid[20];
u8 mslot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET);
admin_prepare(server, st == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET && mslot
&& frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, mid, sizeof(mid)), mslot);
admin_check_async(); // Freigabe der Konsole beim Server erfragen (Hintergrund)
}
// Ein SpotPass-Test von früher (App mitten im Test verlassen) darf nicht weiterlaufen
spotpass_test_cancel();
// MBMNET aktiv, aber das Freundekonto hat eine fremde Kennung: darauf hinweisen // MBMNET aktiv, aber das Freundekonto hat eine fremde Kennung: darauf hinweisen
if (st == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET) { if (st == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET) {
@@ -575,6 +696,7 @@ int main(int argc, char **argv)
int selected = st == STATE_MBMNET ? BTN_DISABLE : BTN_ENABLE; int selected = st == STATE_MBMNET ? BTN_DISABLE : BTN_ENABLE;
bool toolsToggled = false; bool toolsToggled = false;
bool showWhy = false; // Meldung zur Freischaltung der Werkzeuge anzeigen (nach SELECT)
int queued = -1; // Aktion, die nach dem nächsten Bild läuft (damit sofort etwas passiert) int queued = -1; // Aktion, die nach dem nächsten Bild läuft (damit sofort etwas passiert)
while (aptMainLoop()) { while (aptMainLoop()) {
@@ -582,24 +704,48 @@ int main(int argc, char **argv)
u32 down = hidKeysDown(); u32 down = hidKeysDown();
if (--ftpCheckIn <= 0) { ftpCheckIn = 120; ftpOn = ftpctl_running(); } if (--ftpCheckIn <= 0) { ftpCheckIn = 120; ftpOn = ftpctl_running(); }
if (down & KEY_START && rebootIn == 0) break; // während des Umschaltens nicht abbrechen if (down & KEY_START && rebootIn == 0) break; // während des Umschaltens nicht abbrechen
if (down & KEY_SELECT) toolsToggled = !toolsToggled; // Logs offen: die haben ihre eigene Bedienung
if (logsOpen) {
if (!admin_is() || !logview_update(down, hidKeysDownRepeat(), hidKeysHeld())) logsOpen = false;
down = 0;
}
bool wasTools = toolsToggled;
if (down & KEY_SELECT && admin_is()) toolsToggled = !toolsToggled;
else if (down & KEY_SELECT) {
admin_check_async(); // nach dem Freischalten auf der Webseite: gleich nochmal fragen
showWhy = true;
}
// Stand der Freischaltung live anzeigen, bis etwas anderes passiert
if (showWhy && (admin_is() || down & (KEY_A | KEY_B | KEY_Y | KEY_TOUCH))) { showWhy = false; if (admin_is()) msg[0] = 0; }
if (showWhy) snprintf(msg, sizeof(msg), "%s", admin_why());
if (down & KEY_B && toolsToggled) toolsToggled = false;
if (!admin_is()) toolsToggled = false; // Werkzeuge nur für Admins (Server hat es bestätigt)
// Werkzeuge nur mit SELECT, beim Öffnen immer aus // Werkzeuge nur mit SELECT, beim Öffnen immer aus
bool admin = toolsToggled; bool admin = toolsToggled;
if (!visible(selected, admin)) selected = admin || selected <= BTN_REBOOT ? BTN_ENABLE : BTN_SHOP; if (admin != wasTools) { selected = admin ? BTN_DIAG : BTN_ENABLE; armed = -1; }
if (!visible(selected, admin)) selected = admin ? BTN_DIAG : selected <= BTN_LOGS ? BTN_ENABLE : BTN_SHOP;
int action = -1; int action = -1;
// Oben die beiden Kacheln, unten (nur mit Werkzeugen) Diagnose / FTP / ID lösen / SpotPass / Neustart // Oben die beiden Kacheln, unten (nur mit Werkzeugen) Diagnose / FTP / ID lösen / SpotPass / Neustart
bool left = down & (KEY_LEFT | KEY_CPAD_LEFT), right = down & (KEY_RIGHT | KEY_CPAD_RIGHT); bool left = down & (KEY_LEFT | KEY_CPAD_LEFT), right = down & (KEY_RIGHT | KEY_CPAD_RIGHT);
bool up = down & (KEY_UP | KEY_CPAD_UP), dn = down & (KEY_DOWN | KEY_CPAD_DOWN); bool up = down & (KEY_UP | KEY_CPAD_UP), dn = down & (KEY_DOWN | KEY_CPAD_DOWN);
if (selected < BTN_DIAG) { if (left || right) selected ^= 1; } if (admin) {
else if (selected >= BTN_SHOP) { if ((left || right) && shopVersion >= 0) selected = selected == BTN_SHOP ? BTN_SHOP_DEL : BTN_SHOP; } // Werkzeug-Raster: 2 Spalten, 3 Reihen
else if (left) selected = selected == BTN_DIAG ? BTN_REBOOT : selected - 1; int i = tool_index(selected);
else if (right) selected = selected == BTN_REBOOT ? BTN_DIAG : selected + 1; if (i < 0) i = 0;
if (dn && selected < BTN_DIAG) selected = admin ? (selected == BTN_ENABLE ? BTN_DIAG : BTN_REBOOT) if (left || right) i ^= 1;
: (selected == BTN_DISABLE && shopVersion >= 0 ? BTN_SHOP_DEL : BTN_SHOP); if (up && i >= 2) i -= 2;
else if (up && selected >= BTN_SHOP) selected = selected == BTN_SHOP_DEL ? BTN_DISABLE : BTN_ENABLE; if (dn && i + 2 < TOOL_COUNT) i += 2;
else if (up && selected >= BTN_DIAG) selected = selected == BTN_REBOOT ? BTN_DISABLE : BTN_ENABLE; selected = toolOrder[i];
} else {
if (selected < BTN_DIAG) { if (left || right) selected ^= 1; }
else if (selected >= BTN_SHOP) { if ((left || right) && shopVersion >= 0) selected = selected == BTN_SHOP ? BTN_SHOP_DEL : BTN_SHOP; }
if (dn && selected < BTN_DIAG) selected = selected == BTN_DISABLE && shopVersion >= 0 ? BTN_SHOP_DEL : BTN_SHOP;
else if (up && selected >= BTN_SHOP) selected = selected == BTN_SHOP_DEL ? BTN_DISABLE : BTN_ENABLE;
}
if (down & KEY_A) action = selected; if (down & KEY_A) action = selected;
// Y: verknüpfte ID lösen (für alle, mit Bestätigung durch ein zweites Y)
if (down & KEY_Y) action = BTN_UNLINK;
if (down & KEY_TOUCH) { if (down & KEY_TOUCH) {
touchPosition tp; touchPosition tp;
hidTouchRead(&tp); hidTouchRead(&tp);
@@ -616,10 +762,14 @@ int main(int argc, char **argv)
// Umschalten & Co. warten, bis das Auffrischen im Hintergrund durch ist (meist nur einen Moment) // Umschalten & Co. warten, bis das Auffrischen im Hintergrund durch ist (meist nur einen Moment)
bool waitForMaintenance = maintaining && queued >= 0 && queued != BTN_FTP && queued != BTN_SPOTPASS bool waitForMaintenance = maintaining && queued >= 0 && queued != BTN_FTP && queued != BTN_SPOTPASS
&& queued != BTN_SHOP && queued != BTN_SHOP_DEL; && queued != BTN_SHOP && queued != BTN_SHOP_DEL;
if (waitForMaintenance) {
switchRequested = true; // Auffrischen: nichts Neues mehr anfangen
waitForMaintenance = maintenanceBusy; // nur warten, solange es Dateien schreibt / das Konto abgleicht
}
if (queued >= 0 && !waitForMaintenance) { if (queued >= 0 && !waitForMaintenance) {
int a = queued; int a = queued;
queued = -1; queued = -1;
if (run(a, msg, sizeof(msg))) rebootIn = 45; // ca. 0,75 Sekunden zum Lesen if (run(a, msg, sizeof(msg))) rebootIn = 15; // ca. 0,25 Sekunden zum Lesen
st = patch_state(); st = patch_state();
} else if (action >= 0 && rebootIn == 0 && queued < 0) { } else if (action >= 0 && rebootIn == 0 && queued < 0) {
queued = action; queued = action;
@@ -661,9 +811,9 @@ int main(int argc, char **argv)
spotpassTestFrames = -1; spotpassTestFrames = -1;
// Neustart erst, wenn die Freunde-Anmeldung durch ist (höchstens ein paar Sekunden) // Neustart erst, wenn die Freunde-Anmeldung durch ist (höchstens ein paar Sekunden)
if (rebootIn > 0 && --rebootIn == 0 && R_FAILED(APT_HardwareResetAsync())) { // Schneller Neustart wie bei Nimbus (Soft-Reboot über ns:s, ohne boot9strap/Luma-Splash): erst die App
snprintf(msg, sizeof(msg), "Neustart fehlgeschlagen.\nBitte den 3DS von Hand neu starten."); // sauber beenden, dann neu starten (unten nach dem Aufräumen)
} if (rebootIn > 0 && --rebootIn == 0) { softReboot = true; break; }
if (restartIn > 0 && --restartIn == 0) { if (restartIn > 0 && --restartIn == 0) {
if (relaunch_prepare(selfPath)) break; if (relaunch_prepare(selfPath)) break;
@@ -674,10 +824,12 @@ int main(int argc, char **argv)
C3D_FrameBegin(C3D_FRAME_SYNCDRAW); C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
C2D_TargetClear(top, COL_BG); C2D_TargetClear(top, COL_BG);
C2D_SceneBegin(top); C2D_SceneBegin(top);
draw_top(st, msg, admin, admin_is()); if (logsOpen) logview_draw_top();
else draw_top(st, msg, admin, admin_is());
C2D_TargetClear(bot, COL_BG); C2D_TargetClear(bot, COL_BG);
C2D_SceneBegin(bot); C2D_SceneBegin(bot);
draw_bottom(selected, st, admin); if (logsOpen) logview_draw_bottom();
else draw_bottom(selected, st, admin);
C3D_FrameEnd(0); C3D_FrameEnd(0);
} }
@@ -685,13 +837,25 @@ int main(int argc, char **argv)
// schreiben sie nach dem Freigeben hinein und Luma zeigt einen Absturz (z. B. beim Neustart nach "Nintendo"). // schreiben sie nach dem Freigeben hinein und Luma zeigt einen Absturz (z. B. beim Neustart nach "Nintendo").
ftpctl_stop(NULL, 0); ftpctl_stop(NULL, 0);
shopctl_exit(); shopctl_exit();
if (spotpassTestFrames >= 0) spotpass_test_cancel(); // App mitten im Test beendet
// Update- und Admin-Abfrage nur kurz abwarten: Läuft noch etwas, beendet sich die App hart (schneller
// Neustart nach dem Umschalten). Das Auffrischen schreibt Patch-Dateien - darauf warten (beim Umschalten
// ist es ohnehin schon fertig, Aktionen warten darauf).
bool clean = true; bool clean = true;
if (maintenanceThread) { if (maintenanceThread) {
if (R_SUCCEEDED(threadJoin(maintenanceThread, 20ULL * 1000 * 1000 * 1000))) threadFree(maintenanceThread); switchRequested = true;
else clean = false; for (int i = 0; i < 2000 && maintenanceBusy; i++) svcSleepThread(10ULL * 1000 * 1000); // höchstens 20 s
if (R_SUCCEEDED(threadJoin(maintenanceThread, 100ULL * 1000 * 1000))) threadFree(maintenanceThread);
else clean = false; // holt nur noch das Zertifikat: hart beenden ist ungefährlich (atomar geschrieben)
} }
clean = admin_finish() && clean; clean = admin_finish() && clean;
clean = logview_finish() && clean;
clean = update_finish() && clean; clean = update_finish() && clean;
if (softReboot) {
Result rc = nsInit();
if (R_SUCCEEDED(rc)) rc = NS_RebootSystem(); // kehrt bei Erfolg nicht zurück
if (R_FAILED(rc)) APT_HardwareResetAsync(); // Notlösung: kompletter Neustart
}
if (!clean) { if (!clean) {
// Hart beenden, ohne Speicher freizugeben (der Kernel räumt alles auf) // Hart beenden, ohne Speicher freizugeben (der Kernel räumt alles auf)
aptExit(); aptExit();
@@ -702,6 +866,7 @@ int main(int argc, char **argv)
httpcExit(); httpcExit();
if (am) amExit(); if (am) amExit();
C2D_TextBufDelete(textBuf); C2D_TextBufDelete(textBuf);
C2D_TextBufDelete(measureBuf);
C2D_Fini(); C2D_Fini();
C3D_Fini(); C3D_Fini();
gfxExit(); gfxExit();
+23 -1
View File
@@ -6,6 +6,8 @@
// vorher in /3ds/mbmnet/alte-nnid.txt gesichert. // vorher in /3ds/mbmnet/alte-nnid.txt gesichert.
#include <3ds.h> #include <3ds.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h> #include <string.h>
#include <strings.h> #include <strings.h>
#include <sys/stat.h> #include <sys/stat.h>
@@ -18,16 +20,36 @@
static bool ready; static bool ready;
// Für Anfragen an den Account-Server (Konto-Abgleich, Anmelde-Nachweis) muss sich die App bei act anmelden:
// SDK-Version und ein gemeinsamer Speicherblock, den act als Arbeitsspeicher nutzt. actInit() aus libctru macht
// das nicht - ohne lehnte act jede Server-Anfrage mit "ungültiger Zustand" ab (C8A14F4B, E0A14CCE).
#define ACT_SDK_VERSION 0xB0502C8
#define ACT_SHMEM_SIZE 0x20000
static u8 *actShm;
static Handle actShmBlock;
Result nnid_init(void) Result nnid_init(void)
{ {
Result rc = actInit(false); // false = act:a (Verwaltung) Result rc = actInit(false); // false = act:a (Verwaltung)
ready = R_SUCCEEDED(rc); ready = R_SUCCEEDED(rc);
if (!ready) return rc;
actShm = memalign(0x1000, ACT_SHMEM_SIZE);
if (actShm) {
memset(actShm, 0, ACT_SHMEM_SIZE);
if (R_SUCCEEDED(svcCreateMemoryBlock(&actShmBlock, (u32)actShm, ACT_SHMEM_SIZE, 0, MEMPERM_READ | MEMPERM_WRITE)))
ACT_Initialize(ACT_SDK_VERSION, ACT_SHMEM_SIZE, actShmBlock);
else { free(actShm); actShm = NULL; }
}
return rc; return rc;
} }
void nnid_exit(void) void nnid_exit(void)
{ {
if (ready) actExit(); if (!ready) return;
actExit();
if (actShmBlock) svcCloseHandle(actShmBlock);
free(actShm);
actShm = NULL;
} }
static u8 current_slot(void) static u8 current_slot(void)
+55 -8
View File
@@ -13,6 +13,7 @@
#include <3ds.h> #include <3ds.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include "olvpatch.h" #include "olvpatch.h"
@@ -47,21 +48,67 @@ static u32 fetch(const char *server, const char *path, u8 *buf, u32 cap)
return R_SUCCEEDED(rc) && status == 200 && got < cap ? got : 0; return R_SUCCEEDED(rc) && status == 200 && got < cap ? got : 0;
} }
bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen) // Zwischenspeicher der CA auf der SD: Beim Umschalten nicht jedes Mal den Server fragen (das dauerte
// Sekunden, solange das WLAN noch verbindet). Frisch geholt wird beim Auffrischen im Hintergrund.
#define CA_DER_CACHE "sdmc:/3ds/mbmnet/cache/olv-ca.der"
static u32 read_cached(u8 *der)
{ {
static u8 pem[2048]; FILE *f = fopen(CA_DER_CACHE, "rb");
u32 n = fetch(server, "/olv/ca.der", der, CA_MAX + 1); if (!f) return 0;
u32 n = fread(der, 1, CA_MAX + 1, f);
fclose(f);
FILE *pem = fopen(CA_PEM_FILE, "rb"); // die PEM-Datei für den Browser muss auch da sein
if (!pem) return 0;
fclose(pem);
return n > 0 && n <= CA_MAX ? n : 0;
}
// Erst in eine Hilfsdatei schreiben, dann umbenennen: Wird die App mittendrin beendet, bleibt die alte Datei heil
static bool write_atomic(const char *path, const u8 *data, u32 n)
{
char tmp[96];
snprintf(tmp, sizeof(tmp), "%s.neu", path);
FILE *f = fopen(tmp, "wb");
bool ok = f && fwrite(data, 1, n, f) == n;
if (f) fclose(f);
if (ok) { remove(path); ok = rename(tmp, path) == 0; }
if (!ok) remove(tmp);
return ok;
}
// Holt die CA vom Server in den Zwischenspeicher. *changed: anders als vorher.
bool olvpatch_refresh_ca(const char *server, bool *changed)
{
static u8 der[CA_MAX + 1], old[CA_MAX + 1], pem[2048];
*changed = false;
u32 n = fetch(server, "/olv/ca.der", der, sizeof(der));
if (n == 0 || n > CA_MAX) return false; if (n == 0 || n > CA_MAX) return false;
u32 p = fetch(server, "/olv/ca.pem", pem, sizeof(pem)); u32 p = fetch(server, "/olv/ca.pem", pem, sizeof(pem));
if (p == 0) return false; if (p == 0) return false;
// Der Browser der Miiverse-App liest die CA von der SD-Karte u32 oldLen = read_cached(old);
FILE *f = fopen(CA_PEM_FILE, "wb"); if (oldLen == n && memcmp(old, der, n) == 0) return true; // unverändert
bool ok = f && fwrite(pem, 1, p, f) == p; mkdir("sdmc:/3ds/mbmnet", 0777);
if (f) fclose(f); mkdir("sdmc:/3ds/mbmnet/cache", 0777);
*derLen = n; // Der Browser der Miiverse-App liest die CA (PEM) von der SD-Karte
bool ok = write_atomic(CA_PEM_FILE, pem, p) && write_atomic(CA_DER_CACHE, der, n);
*changed = ok;
return ok; return ok;
} }
bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen)
{
if ((*derLen = read_cached(der))) return true;
bool changed;
return olvpatch_refresh_ca(server, &changed) && (*derLen = read_cached(der)) != 0;
}
bool olvpatch_ca_cached(void)
{
static u8 der[CA_MAX + 1];
return read_cached(der) != 0;
}
bool olvpatch_supported(const u8 *code, u32 size) bool olvpatch_supported(const u8 *code, u32 size)
{ {
u32 off = DISCOVERY_ADDR - CODE_BASE; u32 off = DISCOVERY_ADDR - CODE_BASE;
+4 -1
View File
@@ -3,8 +3,11 @@
// Miiverse-Patch (siehe olvpatch.c). Die CA kommt vom MBMNET-Server (/olv/ca.der, /olv/ca.pem). // Miiverse-Patch (siehe olvpatch.c). Die CA kommt vom MBMNET-Server (/olv/ca.der, /olv/ca.pem).
// Holt die MBMNET-CA (DER, höchstens 889 Bytes) und legt die PEM-Fassung für den Browser auf die SD. // CA aus dem Zwischenspeicher auf der SD (schnell); ist keiner da, einmal vom Server holen
bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen); bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen);
// CA frisch vom Server in den Zwischenspeicher. *changed: hat sich geändert (dann Patches neu bauen)
bool olvpatch_refresh_ca(const char *server, bool *changed);
bool olvpatch_ca_cached(void);
// Ist das genau die Miiverse-Version, für die der Patch gebaut ist? (entpackter .code) // Ist das genau die Miiverse-Version, für die der Patch gebaut ist? (entpackter .code)
bool olvpatch_supported(const u8 *code, u32 size); bool olvpatch_supported(const u8 *code, u32 size);
// Baut den IPS-Patch. Gibt die Länge zurück, 0 bei Fehler. // Baut den IPS-Patch. Gibt die Länge zurück, 0 bei Fehler.
+23 -6
View File
@@ -63,6 +63,7 @@ typedef struct {
const char *label; // Name für Meldungen const char *label; // Name für Meldungen
Rule rules[MAX_RULES]; Rule rules[MAX_RULES];
bool optional; // Anwendung statt Systemmodul: nur patchen, wenn installiert und Adresse gefunden bool optional; // Anwendung statt Systemmodul: nur patchen, wenn installiert und Adresse gefunden
bool soft; // Systemmodul, aber verzichtbar: Adresse nicht gefunden = ohne diesen Patch weiter
} Target; } Target;
#define TITLES_DIR "sdmc:/luma/titles" #define TITLES_DIR "sdmc:/luma/titles"
@@ -79,6 +80,13 @@ static const Target targets[] = {
{ RULE_EXACT, "https://%s%saccount.nintendo.net/v%u/api/", "/v1/api/" }, { RULE_EXACT, "https://%s%saccount.nintendo.net/v%u/api/", "/v1/api/" },
{ RULE_PREFIX, "https://account.nintendo.net" }, { RULE_PREFIX, "https://account.nintendo.net" },
} }, } },
// SpotPass (BOSS): holt vor jedem Abruf die Policy-Liste von nppl.c.app.nintendowifi.net (abgeschaltet).
// Die App stellt die Adresse zwar zur Laufzeit um (boss:P SetPolicyListUrl), nach einem Neustart gilt
// aber wieder Nintendos - dann scheitern alle Abrufe im Hintergrund, bis man die App öffnet. Der Patch
// macht die MBMNET-Adresse dauerhaft. Verzichtbar: Fehlt die Adresse im Modul, läuft alles andere weiter.
{ 0x0004013000003402ULL, SYSMODULE_DIR, "SpotPass", {
{ RULE_PREFIX, "https://nppl.c.app.nintendowifi.net" },
}, false, true },
// Miiverse (JPN, USA, EUR): fragt zuerst den Discovery-Server, wohin es geht, und lädt dann seine Seiten // Miiverse (JPN, USA, EUR): fragt zuerst den Discovery-Server, wohin es geht, und lädt dann seine Seiten
// per HTTPS. Gebaut wird der Patch in olvpatch.c (die Regel hier dient nur der Diagnose). Nur die // per HTTPS. Gebaut wird der Patch in olvpatch.c (die Regel hier dient nur der Diagnose). Nur die
// installierte Region wird gepatcht. Luma lädt Patches für Anwendungen aus /luma/titles/<ID>/code.ips (wie Nimbus). // installierte Region wird gepatcht. Luma lädt Patches für Anwendungen aus /luma/titles/<ID>/code.ips (wie Nimbus).
@@ -365,7 +373,7 @@ static bool build_patch(const Target *t, const char *server, Patch *p, char *msg
} }
free(code); free(code);
if (p->count == 0 && t->optional) return false; if (p->count == 0 && (t->optional || t->soft)) return false;
if (p->count == 0) { if (p->count == 0) {
char diag[64]; char diag[64];
patch_diagnose(diag, sizeof(diag)); patch_diagnose(diag, sizeof(diag));
@@ -417,12 +425,10 @@ static bool write_ips(const char *path, const u8 *data, u32 n, bool *changed)
static u32 build_miiverse(const Target *t, const char *server, u8 *out, u32 cap) static u32 build_miiverse(const Target *t, const char *server, u8 *out, u32 cap)
{ {
static u8 der[1024]; static u8 der[1024];
static u32 derLen; u32 derLen = 0;
static int caState; // 0 = noch nicht geholt, 1 = da, -1 = nicht zu holen
char fp[64]; char fp[64];
fingerprint(t->tid, fp, sizeof(fp)); // leer = kein Zwischenspeicher, gebaut wird trotzdem fingerprint(t->tid, fp, sizeof(fp)); // leer = kein Zwischenspeicher, gebaut wird trotzdem
if (caState == 0) caState = olvpatch_fetch_ca(server, der, &derLen) ? 1 : -1; if (!olvpatch_fetch_ca(server, der, &derLen)) return 0; // aus dem Zwischenspeicher (aktualisiert das Auffrischen)
if (caState < 0) return 0;
u32 sum = 2166136261u; // FNV-1a über die CA: neue CA -> neuer Patch u32 sum = 2166136261u; // FNV-1a über die CA: neue CA -> neuer Patch
for (u32 i = 0; i < derLen; i++) sum = (sum ^ der[i]) * 16777619u; for (u32 i = 0; i < derLen; i++) sum = (sum ^ der[i]) * 16777619u;
@@ -517,6 +523,15 @@ static void remove_splash(void)
// --- Aktivieren / Deaktivieren ----------------------------------------------- // --- Aktivieren / Deaktivieren -----------------------------------------------
void (*patch_progress)(const char *step);
static void progress(const char *what, const Target *t)
{
if (!patch_progress) return;
char line[64];
snprintf(line, sizeof(line), "%s %s-Modul", what, t->label);
patch_progress(line);
}
bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen) bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen)
{ {
*changed = false; *changed = false;
@@ -532,6 +547,7 @@ bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen)
static u8 ips[TARGET_COUNT][IPS_CAP]; static u8 ips[TARGET_COUNT][IPS_CAP];
u32 ipsLen[TARGET_COUNT]; u32 ipsLen[TARGET_COUNT];
for (size_t i = 0; i < TARGET_COUNT; i++) { for (size_t i = 0; i < TARGET_COUNT; i++) {
progress("Patches:", &targets[i]);
if (targets[i].optional) { // Miiverse if (targets[i].optional) { // Miiverse
ipsLen[i] = build_miiverse(&targets[i], server, ips[i], IPS_CAP); ipsLen[i] = build_miiverse(&targets[i], server, ips[i], IPS_CAP);
continue; continue;
@@ -541,8 +557,9 @@ bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen)
ipsLen[i] = cache_load(&targets[i], key, ips[i], IPS_CAP); ipsLen[i] = cache_load(&targets[i], key, ips[i], IPS_CAP);
if (ipsLen[i]) continue; if (ipsLen[i]) continue;
static Patch patch; static Patch patch;
progress("Patches: lese", &targets[i]);
if (!build_patch(&targets[i], server, &patch, msg, msglen)) { if (!build_patch(&targets[i], server, &patch, msg, msglen)) {
if (targets[i].optional) { ipsLen[i] = 0; continue; } // nicht installiert: auslassen if (targets[i].optional || targets[i].soft) { ipsLen[i] = 0; msg[0] = 0; continue; } // auslassen
return false; return false;
} }
ipsLen[i] = build_ips(&patch, ips[i], IPS_CAP); ipsLen[i] = build_ips(&patch, ips[i], IPS_CAP);
+2
View File
@@ -22,6 +22,8 @@ bool patch_migrate(void);
// Erzeugt die Patches (Freunde + Konto) und aktiviert MBMNET. msg bekommt eine lesbare Meldung. // Erzeugt die Patches (Freunde + Konto) und aktiviert MBMNET. msg bekommt eine lesbare Meldung.
// *changed: true, wenn sich Patch-Dateien geändert haben (dann ist ein Neustart nötig). // *changed: true, wenn sich Patch-Dateien geändert haben (dann ist ein Neustart nötig).
bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen); bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen);
// Wird bei jedem Schritt von patch_enable mit einer kurzen Beschreibung aufgerufen (Anzeige, Fehlersuche)
extern void (*patch_progress)(const char *step);
// Schreibt alle Adress-Texte aus den Modulen nach DIAG_FILE (zur Fehlersuche). // Schreibt alle Adress-Texte aus den Modulen nach DIAG_FILE (zur Fehlersuche).
bool patch_diagnose(char *msg, size_t msglen); bool patch_diagnose(char *msg, size_t msglen);
// Entfernt unsere Patches und stellt eventuell gesicherte fremde Patches wieder her. // Entfernt unsere Patches und stellt eventuell gesicherte fremde Patches wieder her.
+11 -2
View File
@@ -11,7 +11,7 @@
#define TASK_NEWS "mbmnews" #define TASK_NEWS "mbmnews"
#define TASK_TEST "mbmtest" #define TASK_TEST "mbmtest"
#define NEWS_INTERVAL (2 * 60) // Sekunden zwischen zwei Abrufen #define NEWS_INTERVAL (15 * 60) // Sekunden zwischen zwei Abrufen (alle 2 Minuten zog zu viel Akku)
// SpotPass braucht für jede Anwendung mit Aufgaben einen angemeldeten Speicher (Extdata), wie bei Spielen. // SpotPass braucht für jede Anwendung mit Aufgaben einen angemeldeten Speicher (Extdata), wie bei Spielen.
// Die Mitteilungen selbst landen im News-Modul, der Speicher bleibt praktisch leer. // Die Mitteilungen selbst landen im News-Modul, der Speicher bleibt praktisch leer.
@@ -205,7 +205,9 @@ bool spotpass_test_start(const char *server, char *msg, size_t msglen)
return false; return false;
} }
const char *step = ""; const char *step = "";
rc = setup_task(server, TASK_TEST, testUrl, 60, &step); // Abstand 24 h: Selbst wenn die Aufgabe stehen bleibt (App mitten im Test verlassen), kommt der Test
// nicht jede Minute wieder. Normalerweise löscht spotpass_test_poll/spotpass_test_cancel sie.
rc = setup_task(server, TASK_TEST, testUrl, 24 * 60 * 60, &step);
if (R_SUCCEEDED(rc)) { step = "Aufgabe starten"; rc = bossStartTaskImmediate(TASK_TEST); } if (R_SUCCEEDED(rc)) { step = "Aufgabe starten"; rc = bossStartTaskImmediate(TASK_TEST); }
bossExit(); bossExit();
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
@@ -217,6 +219,13 @@ bool spotpass_test_start(const char *server, char *msg, size_t msglen)
return true; return true;
} }
void spotpass_test_cancel(void)
{
if (R_FAILED(bossInit(0, false))) return;
bossDeleteTask(TASK_TEST, 0);
bossExit();
}
bool spotpass_test_poll(int seconds, char *msg, size_t msglen) bool spotpass_test_poll(int seconds, char *msg, size_t msglen)
{ {
if (R_FAILED(bossInit(0, false))) return true; if (R_FAILED(bossInit(0, false))) return true;
+4 -1
View File
@@ -3,7 +3,7 @@
#include <stddef.h> #include <stddef.h>
// SpotPass (BOSS): Der 3DS holt MBMNET-Nachrichten und Ankündigungen selbst im Hintergrund ab (auch im // SpotPass (BOSS): Der 3DS holt MBMNET-Nachrichten und Ankündigungen selbst im Hintergrund ab (auch im
// Standby, ohne App) und zeigt sie als Mitteilung im HOME-Menü. Die Aufgabe ruft alle 2 Minuten // Standby, ohne App) und zeigt sie als Mitteilung im HOME-Menü. Die Aufgabe ruft alle 15 Minuten
// http://<server>/spotpass/<Geräte-ID>/news.boss ab (MBMNET Accounting, spotpass.js). // http://<server>/spotpass/<Geräte-ID>/news.boss ab (MBMNET Accounting, spotpass.js).
// Hintergrund-Aufgabe anmelden (bei aktivem MBMNET). false + msg bei Fehler. // Hintergrund-Aufgabe anmelden (bei aktivem MBMNET). false + msg bei Fehler.
@@ -15,3 +15,6 @@ void spotpass_unregister(void);
bool spotpass_test_start(const char *server, char *msg, size_t msglen); bool spotpass_test_start(const char *server, char *msg, size_t msglen);
// true = Test noch nicht fertig. msg bekommt den Stand bzw. das Ergebnis. // true = Test noch nicht fertig. msg bekommt den Stand bzw. das Ergebnis.
bool spotpass_test_poll(int seconds, char *msg, size_t msglen); bool spotpass_test_poll(int seconds, char *msg, size_t msglen);
// Test-Aufgabe löschen (beim Start, beim Verlassen der App): Bleibt sie stehen, ruft der 3DS den Test
// immer wieder ab und zeigt jedes Mal eine neue Mitteilung.
void spotpass_test_cancel(void);
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <citro2d.h>
// Zeichenhilfen und Farben der MBMNET-App (umgesetzt in main.c), auch für logview.c
#define TOP_W 400
#define BOT_W 320
#define SCR_H 240
#define COL_RED C2D_Color32(0xE4, 0x00, 0x2B, 0xFF)
#define COL_RED_DARK C2D_Color32(0x9E, 0x00, 0x1E, 0xFF)
#define COL_BG C2D_Color32(0x16, 0x19, 0x1F, 0xFF)
#define COL_BG2 C2D_Color32(0x22, 0x26, 0x2E, 0xFF)
#define COL_CARD C2D_Color32(0x2A, 0x2F, 0x39, 0xFF)
#define COL_CARD_HI C2D_Color32(0x3A, 0x41, 0x4E, 0xFF)
#define COL_WHITE C2D_Color32(0xFF, 0xFF, 0xFF, 0xFF)
#define COL_MUTED C2D_Color32(0x9A, 0xA3, 0xAF, 0xFF)
#define COL_GREEN C2D_Color32(0x22, 0xC5, 0x5E, 0xFF)
#define COL_AMBER C2D_Color32(0xF5, 0x9E, 0x0B, 0xFF)
#define COL_NINTENDO C2D_Color32(0xE6, 0x00, 0x12, 0xFF)
void ui_text(const char *s, float x, float y, float size, u32 color, u32 flags);
void ui_text_wrap(const char *s, float x, float y, float size, u32 color, float width);
float ui_text_width(const char *s, float size);
// Text auf eine Breite kürzen ("...")
void ui_fit(char *out, size_t len, const char *s, float size, float width);
void ui_round_rect(float x, float y, float w, float h, float r, u32 color);
+1 -1
View File
@@ -273,7 +273,7 @@ bool update_finish(void)
if (!thread) return true; if (!thread) return true;
// Läuft die Abfrage noch, nicht freigeben (sonst schreibt der Thread in freigegebenen Speicher). // Läuft die Abfrage noch, nicht freigeben (sonst schreibt der Thread in freigegebenen Speicher).
// false = läuft noch, die App muss sich hart beenden (main.c). // false = läuft noch, die App muss sich hart beenden (main.c).
if (R_FAILED(threadJoin(thread, 15ULL * 1000 * 1000 * 1000))) return false; if (R_FAILED(threadJoin(thread, 100ULL * 1000 * 1000))) return false;
threadFree(thread); threadFree(thread);
thread = NULL; thread = NULL;
return true; return true;
+1 -1
View File
@@ -21,7 +21,7 @@ void update_download(void);
bool update_pending(void); bool update_pending(void);
UpdateState update_state(void); UpdateState update_state(void);
const char *update_new_version(void); const char *update_new_version(void);
// Wartet auf den Hintergrund-Thread (höchstens 15 s). false = läuft noch. // Wartet auf den Hintergrund-Thread (höchstens 0,1 s). false = läuft noch.
bool update_finish(void); bool update_finish(void);
// Laufender Download-Versuch (1-3) // Laufender Download-Versuch (1-3)
int update_attempt(void); int update_attempt(void);