// 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". // SELECT blendet die Entwickler-Werkzeuge ein und aus (beim Öffnen immer aus). #include <3ds.h> #include #include #include #include #include #include "patcher.h" #include "update.h" #include "relaunch.h" #include "nnid.h" #include "frdswitch.h" #include "admin.h" #include "ftpctl.h" #include "spotpass.h" #include "shopctl.h" #include "olvpatch.h" #define TOP_W 400 #define BOT_W 320 #define SCR_H 240 // 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; #define TILE_Y 14 #define TILE_W 142 #define TILE_H_FULL 166 #define TILE_H_ADMIN 160 #define TOOL_COUNT 5 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 } }; 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) // Welche Knöpfe sind gerade da? static bool visible(int btn, bool admin) { if (btn <= BTN_DISABLE) return true; if (btn <= BTN_REBOOT) return admin; if (btn == BTN_SHOP) return !admin; return !admin && shopVersion >= 0; // Entfernen nur, wenn installiert } static Box box_of(int btn, bool admin) { float h = admin ? TILE_H_ADMIN : TILE_H_FULL; switch (btn) { case BTN_ENABLE: return (Box){ 12, 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_DEL: return shopDelBox; default: return toolBoxes[4]; } } static C2D_TextBuf textBuf; static int spotpassTestFrames = -1; // läuft der SpotPass-Test? (Bilder seit dem Start) static bool ftpOn; // läuft das FTP-Modul? (alle 2 s geprüft, nicht in jedem Bild) // --- Zeichenhilfen ------------------------------------------------------------ static void text(const char *s, float x, float y, float size, u32 color, u32 flags) { C2D_Text t; C2D_TextParse(&t, textBuf, s); C2D_TextOptimize(&t); C2D_DrawText(&t, C2D_WithColor | flags, x, y, 0.5f, size, size, 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, y + r, 0.5f, w, h - 2 * r, color); C2D_DrawCircleSolid(x + r, y + r, 0.5f, r, color); C2D_DrawCircleSolid(x + w - r, y + 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); } // Das MBMNET-Logo: rotes abgerundetes Quadrat mit einem "M" aus verbundenen Netzwerk-Knoten. // Geometrie identisch zu assets/logo.svg (100x100-Raster). static void draw_logo(float x, float y, float size) { float s = size / 100.0f; round_rect(x, y, size, size, 22 * s, COL_RED_DARK); round_rect(x, y, size, size - 4 * s, 22 * s, COL_RED); static const float pts[5][2] = { {24, 72}, {24, 30}, {50, 56}, {76, 30}, {76, 72} }; for (int i = 0; i < 4; i++) { C2D_DrawLine(x + pts[i][0] * s, y + pts[i][1] * s, COL_WHITE, x + pts[i + 1][0] * s, y + pts[i + 1][1] * s, COL_WHITE, 7 * s, 0.5f); } for (int i = 0; i < 5; i++) { C2D_DrawCircleSolid(x + pts[i][0] * s, y + pts[i][1] * s, 0.5f, 9 * s, COL_WHITE); if (i == 2) C2D_DrawCircleSolid(x + pts[i][0] * s, y + pts[i][1] * s, 0.5f, 4 * s, COL_RED); } } // Nintendo-Schriftzug im roten Rahmen (nachgezeichnet, kein Bild) static void draw_nintendo(float cx, float cy, float w) { float h = w * 0.36f, r = h / 2; round_rect(cx - w / 2, cy - h / 2, w, h, r, COL_NINTENDO); round_rect(cx - w / 2 + 3, cy - h / 2 + 3, w - 6, h - 6, r - 3, COL_WHITE); text("Nintendo", cx, cy - h * 0.36f, w / 150.0f, COL_NINTENDO, C2D_AlignCenter); } // --- Bildschirme --------------------------------------------------------------- static void draw_update_line(bool admin) { char line[64]; const char *v = update_new_version(); switch (update_state()) { case UPD_CHECKING: snprintf(line, sizeof(line), "v" APP_VERSION); break; case UPD_AVAILABLE: snprintf(line, sizeof(line), "Update auf v%s bereit", v); break; case UPD_DOWNLOADING: if (update_attempt() > 1) snprintf(line, sizeof(line), "lade v%s (Versuch %d) ...", v, update_attempt()); else snprintf(line, sizeof(line), "lade v%s ...", v); break; case UPD_INSTALLED: snprintf(line, sizeof(line), "v%s installiert", v); break; case UPD_ERROR: snprintf(line, sizeof(line), admin ? "Update-Server offline" : "v" APP_VERSION); break; default: snprintf(line, sizeof(line), "v" APP_VERSION); break; } u32 color = update_state() == UPD_INSTALLED ? COL_GREEN : update_pending() ? COL_AMBER : COL_MUTED; text(line, TOP_W - 10, 4, 0.4f, color, C2D_AlignRight); } 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); draw_update_line(admin); const char *hint = admin ? "START: Beenden SELECT: Werkzeuge aus" : isAdmin ? "START: Beenden SELECT: Werkzeuge" : "START: Beenden"; text(hint, 10, 4, 0.4f, COL_MUTED, 0); if (ftpOn) { // FTP-Adresse direkt daneben C2D_Text t; float w = 0; C2D_TextParse(&t, textBuf, hint); C2D_TextGetDimensions(&t, 0.4f, 0.4f, &w, NULL); char ftp[112]; const char *a = ftpctl_address(); const char *st = ftpctl_status(); if (a[0]) snprintf(ftp, sizeof(ftp), "FTP %s", a); else snprintf(ftp, sizeof(ftp), "FTP: %s", st[0] ? st : "startet ..."); text(ftp, 10 + w + 12, 4, 0.4f, COL_GREEN, 0); } float y = msg[0] || admin ? 26 : 62; draw_logo(TOP_W / 2 - 32, y, 64); text("MBMNET", TOP_W / 2, y + 70, 0.9f, COL_WHITE, C2D_AlignCenter); 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]) { round_rect(16, y, TOP_W - 32, SCR_H - y - 8, 10, COL_CARD); text(msg, 28, y + 6, 0.46f, COL_WHITE, 0); } else if (!admin) { text("Tippe unten auf MBMNET oder Nintendo.", TOP_W / 2, 200, 0.45f, COL_MUTED, C2D_AlignCenter); } } static void draw_tile(int btn, bool sel, bool active, bool admin) { Box b = box_of(btn, admin); if (sel) round_rect(b.x - 3, b.y - 3, b.w + 6, b.h + 6, 15, COL_RED); round_rect(b.x, b.y, b.w, b.h, 12, active ? COL_CARD_HI : COL_CARD); float cx = b.x + b.w / 2, logoCy = b.y + b.h / 2 - 12; if (btn == BTN_ENABLE) draw_logo(cx - 36, logoCy - 36, 72); else draw_nintendo(cx, logoCy, 116); if (active) { C2D_DrawCircleSolid(cx - 26, b.y + b.h - 26, 0.5f, 5, COL_GREEN); text("Aktiv", cx + 6, b.y + b.h - 36, 0.6f, COL_GREEN, C2D_AlignCenter); } } // Kleiner Einkaufswagen (wie das Symbol des MBMNET Shops) static void draw_cart(float x, float y, float size) { float s = size / 100.0f, t = 9 * s; round_rect(x, y, size, size, 22 * s, COL_RED); C2D_DrawLine(x + 13 * s, y + 23 * s, COL_WHITE, x + 23 * s, y + 23 * s, COL_WHITE, t, 0.5f); C2D_DrawLine(x + 23 * s, y + 23 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, t, 0.5f); C2D_DrawLine(x + 33 * s, y + 61 * s, COL_WHITE, x + 30 * s, y + 70 * s, COL_WHITE, t, 0.5f); C2D_DrawLine(x + 30 * s, y + 70 * s, COL_WHITE, x + 78 * s, y + 70 * s, COL_WHITE, t, 0.5f); C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 84 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, 0.5f); C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, 0.5f); C2D_DrawCircleSolid(x + 37 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE); C2D_DrawCircleSolid(x + 72 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE); } static void draw_shop_bar(int selected) { bool busy = shopctl_job() == SHOPJOB_RUNNING; bool outdated = shopVersion >= 0 && shopVersion < APP_TITLE_VERSION; Box b = shopBox; bool sel = selected == BTN_SHOP; round_rect(b.x, b.y, b.w, b.h, 10, sel ? COL_CARD_HI : COL_CARD); if (sel) C2D_DrawRectSolid(b.x, b.y + 10, 0.5f, 4, b.h - 20, COL_RED); draw_cart(b.x + 10, b.y + 7, 26); const char *label = busy ? "Shop wird installiert ..." : shopVersion < 0 ? "MBMNET Shop installieren" : outdated ? "Shop aktualisieren" : "MBMNET Shop öffnen"; text(label, b.x + 44, b.y + 11, 0.5f, busy ? COL_AMBER : COL_WHITE, 0); if (shopVersion < 0) return; b = shopDelBox; sel = selected == BTN_SHOP_DEL; round_rect(b.x, b.y, b.w, b.h, 10, sel ? COL_CARD_HI : COL_CARD); if (sel) C2D_DrawRectSolid(b.x, b.y + 10, 0.5f, 4, b.h - 20, COL_RED); text("Entfernen", b.x + b.w / 2, b.y + 12, 0.42f, COL_MUTED, C2D_AlignCenter); } static void draw_bottom(int selected, PatchState st, bool admin) { C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG); draw_tile(BTN_ENABLE, selected == BTN_ENABLE, st == STATE_MBMNET, admin); draw_tile(BTN_DISABLE, selected == BTN_DISABLE, st == STATE_ORIGINAL, admin); if (!admin) { draw_shop_bar(selected); return; } const char *labels[TOOL_COUNT] = { "Diagnose", ftpOn ? "FTP aus" : "FTP an", "ID lösen", "SpotPass", "Neustart" }; for (int i = 0; i < TOOL_COUNT; i++) { Box b = toolBoxes[i]; bool sel = selected == BTN_DIAG + i; bool on = i == 1 && ftpOn; round_rect(b.x, b.y, b.w, b.h, 10, sel ? COL_CARD_HI : COL_CARD); if (sel) C2D_DrawRectSolid(b.x, b.y + 10, 0.5f, 4, b.h - 20, COL_RED); if (on) C2D_DrawCircleSolid(b.x + 12, b.y + b.h / 2, 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); } } // --- Aktionen -------------------------------------------------------------------- // // Wie bei Pretendo hat MBMNET ein eigenes Freundekonto (Nr. 3) mit eigenem Konto-Platz. Umschalten = // Patches an/aus + auf das andere Freundekonto wechseln + Neustart. Beide Anmeldungen bleiben dabei // erhalten, die Nintendo-Freundesliste auch. // Merker älterer Versionen (bis v2.4): Die MBMNET-ID wurde damals im Nintendo-Platz verknüpft #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; } // Auffrischen beim Start läuft im Hintergrund, in Schritten. Tippt man auf Umschalten, bricht es ab, wo es kann: // Das Umschalten wartet nur, solange Patch-Dateien geschrieben werden oder der Konto-Dienst beschäftigt ist. 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 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; } 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) { (void)arg; char ignore[96]; // 1. Patches auffrischen (Miiverse-CA aus dem Zwischenspeicher; nur beim allerersten Mal vom Server) bool hadCa = olvpatch_ca_cached(); refresh_patches(); // 2. SpotPass-Aufgabe anmelden if (begin_busy()) { spotpass_register(MBMNET_SERVER, ignore, sizeof(ignore)); maintenanceBusy = false; } // 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); char id[20]; Handle done; if (slot && frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id)) && begin_busy() && R_SUCCEEDED(svcCreateEvent(&done, RESET_ONESHOT))) { if (R_SUCCEEDED(ACTA_SyncAccountInfo(slot, 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); } maintenanceBusy = false; // 4. Miiverse-CA frisch vom Server (nur Netz; Patches nur neu bauen, wenn sie sich geändert hat) bool caChanged = false; if (!switchRequested && olvpatch_refresh_ca(MBMNET_SERVER, &caChanged) && caChanged && hadCa) refresh_patches(); // prüft selbst noch einmal, ob umgeschaltet werden soll maintaining = false; } static int armed = -1; // Sicherheitsabfrage (zweimal antippen) static bool launchShop; // Sprung in den MBMNET Shop vorbereitet: App beenden // Name der MBMNET-ID, gemerkt beim Wechsel zu Nintendo. Der 3DS erlaubt dieselbe ID nicht in zwei Konto-Plätzen // (022-2571): Heißt die Pretendo-ID genauso, muss sie beim Wechsel zurück zu MBMNET dort gelöst werden. #define MBMNET_ID_FILE "sdmc:/3ds/mbmnet/mbmnet-id.txt" static void remember_mbmnet_id(const char *id) { FILE *f = fopen(MBMNET_ID_FILE, "w"); if (f) { fputs(id, f); fclose(f); } } static void remembered_mbmnet_id(char *out, size_t len) { out[0] = 0; FILE *f = fopen(MBMNET_ID_FILE, "r"); if (!f) return; if (fgets(out, len, f)) out[strcspn(out, "\r\n")] = 0; fclose(f); } // Wie man den MBMNET-Platz verknüpft. Heißt die MBMNET-ID wie eine Nintendo- oder Pretendo-ID, die schon in // einem anderen Platz steht, lässt der 3DS sie dort nicht zu (022-2571) - dann den Zweitnamen ".m" // nehmen (kennt der MBMNET-Server). So bleiben alle IDs verknüpft und man muss sich nie neu anmelden. static void link_hint(char *out, size_t len) { char name[20] = ""; remembered_mbmnet_id(name, sizeof(name)); if (!name[0]) frdswitch_slot_linked(FRIEND_ACCOUNT_NINTENDO, name, sizeof(name)); if (!name[0]) frdswitch_slot_linked(2, name, sizeof(name)); // Pretendo (Nimbus) if (name[0]) snprintf(out, len, "Einstellungen > Nintendo Network ID > Vorhandene ID.\nHeißt sie wie \"%s\": mit \"%.14s.m\" anmelden.", name, name); else snprintf(out, len, "Einstellungen > Nintendo Network ID > Vorhandene ID.\nGleicher Name wie bei Pretendo? Dann Name + \".m\"."); } // Umschalten auf MBMNET. true = der 3DS muss jetzt neu starten. static bool enable(bool confirmed, char *msg, size_t msglen) { char detail[160] = "", id[20]; if (patch_state() == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET) { // Freundekonto mit fremder Kennung (z. B. von Pretendo geholt): neu anlegen, dann holt es sich // beim nächsten Start eine Kennung bei MBMNET u32 pid = frdswitch_my_pid(); if (pid && !frdswitch_pid_is_mbmnet(pid)) { if (!confirmed) { armed = BTN_ENABLE; snprintf(msg, msglen, "Dein MBMNET-Freundekonto hat eine fremde Kennung\n(%lu, nicht von MBMNET). Neu anlegen?\n" "Die Verknüpfung mit deinem MBMNET-Konto bleibt.\nZum Bestätigen nochmal antippen.", (unsigned long)pid); return false; } if (!frdswitch_recreate_mbmnet(msg, msglen)) return false; snprintf(msg, msglen, "MBMNET-Freundekonto neu angelegt. Der 3DS startet neu.\n" "Danach holt es sich bei MBMNET deinen Freundescode."); return true; } bool linked = frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id)); if (linked) { snprintf(msg, msglen, "MBMNET ist schon aktiv (Konto %s).", id); return false; } char hint[160]; link_hint(hint, sizeof(hint)); snprintf(msg, msglen, "MBMNET ist schon aktiv. Jetzt einmal verknüpfen:\n%s", hint); return false; } // Umstieg von älteren Versionen: Dort steckte die MBMNET-ID im Nintendo-Platz. Sie muss dort raus, // sonst lässt sie sich im eigenen MBMNET-Platz nicht verknüpfen (Fehler 022-2571). bool migrate = old_mbmnet_link() && frdswitch_slot_linked(FRIEND_ACCOUNT_NINTENDO, id, sizeof(id)); if (migrate && !confirmed) { armed = BTN_ENABLE; snprintf(msg, msglen, "Umstellung auf ein eigenes MBMNET-Freundekonto:\n\"%s\" wird aus dem Nintendo-Platz gelöst\n" "und danach im MBMNET-Platz neu verknüpft.\nZum Bestätigen nochmal antippen.", id); return false; } bool changed = false; if (!patch_enable(MBMNET_SERVER, &changed, detail, sizeof(detail))) { snprintf(msg, msglen, "%s", detail); return false; } if (migrate && !nnid_unlink(msg, msglen)) return false; remove(UNLINKED_MARKER); bool created = false; if (!frdswitch_to(FRIEND_ACCOUNT_MBMNET, &created, msg, msglen)) { char ignore[160]; patch_disable(ignore, sizeof(ignore)); // nichts halb umgestellt lassen return false; } bool linked = frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id)); char ignore[96]; spotpass_register(MBMNET_SERVER, ignore, sizeof(ignore)); // Mitteilungen per SpotPass char hint[160]; link_hint(hint, sizeof(hint)); if (linked) snprintf(msg, msglen, "MBMNET aktiv! Der 3DS startet neu.\nDeine MBMNET-Anmeldung und -Freundesliste sind wieder da."); else snprintf(msg, msglen, "MBMNET aktiv! Der 3DS startet neu.\n%sDanach einmal (nur dieses eine Mal):\n%.120s", created ? "Eigenes MBMNET-Freundekonto angelegt. " : "", hint); return true; } // Umstellung auf den Zweitnamen (einmalig): Steht im MBMNET-Platz noch die ID unter dem echten Namen, blockiert // sie eine gleichnamige Pretendo-ID (022-2571). Dann wird sie EINMAL gelöst; beim nächsten Wechsel zu MBMNET // meldet man sich mit ".m" an, und ab dann bleibt alles verknüpft. note bekommt den Hinweis. #define ALIAS_DONE_MARKER "sdmc:/3ds/mbmnet/zweitname-umgestellt" static bool free_mbmnet_name(char *note, size_t len) { note[0] = 0; FILE *m = fopen(ALIAS_DONE_MARKER, "r"); if (m) { fclose(m); return false; } char id[20]; u8 slot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET); if (!slot || !frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id))) return false; size_t n = strlen(id); if (n >= 2 && !strcasecmp(id + n - 2, ".m")) return false; // schon der Zweitname remember_mbmnet_id(id); if (!nnid_unlink_slot(slot, note, len)) return false; mkdir("sdmc:/3ds/mbmnet", 0777); m = fopen(ALIAS_DONE_MARKER, "w"); if (m) { fputs(id, m); fclose(m); } snprintf(note, len, "\"%s\" ist jetzt frei für Pretendo.\nZurück bei MBMNET einmal mit \"%.14s.m\" anmelden,\ndanach nie wieder.", id, id); return true; } // Zurück zu Nintendo. true = der 3DS muss jetzt neu starten. static bool disable(bool confirmed, char *msg, size_t msglen) { (void)confirmed; char detail[160] = ""; PatchState st = patch_state(); char note[200]; if (st == STATE_ORIGINAL && frdswitch_current() == FRIEND_ACCOUNT_NINTENDO) { if (free_mbmnet_name(note, sizeof(note))) snprintf(msg, msglen, "Nintendo ist schon aktiv.\n%s", note); else if (note[0]) snprintf(msg, msglen, "%s", note); else snprintf(msg, msglen, "Nintendo ist schon aktiv."); return false; } if (st == STATE_FOREIGN) { snprintf(msg, msglen, "MBMNET ist nicht aktiv (anderer Patch)."); return false; } // Die MBMNET-ID bleibt verknüpft (früher wurde sie hier gelöst, dann musste man sich jedes Mal neu // anmelden). Den Namen merken, für den Hinweis auf den Zweitnamen. char id[20] = ""; if (frdswitch_current() == FRIEND_ACCOUNT_MBMNET && nnid_linked(id, sizeof(id))) remember_mbmnet_id(id); bool created = 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)); spotpass_unregister(); if (freed) snprintf(msg, msglen, "Nintendo aktiv! Der 3DS startet neu.\n%s", note); else snprintf(msg, msglen, "Nintendo aktiv! Der 3DS startet neu.\nDeine Nintendo-Freundesliste ist wieder da."); return true; } // Führt eine Aktion aus. true = der 3DS muss jetzt neu starten. static bool run(int action, char *msg, size_t msglen) { bool confirmed = armed == action; armed = -1; // 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()) { snprintf(msg, msglen, "Moment: Das Update auf v%s wird gerade installiert.\nDanach startet die App neu.", update_new_version()); return false; } switch (action) { case BTN_ENABLE: return enable(confirmed, msg, msglen); case BTN_DISABLE: return disable(confirmed, msg, msglen); case BTN_DIAG: patch_diagnose(msg, msglen); return false; case BTN_UNLINK: { // Verknüpfte NNID nur auf dem 3DS lösen (ohne Server) - z. B. wenn das Konto gelöscht wurde char id[20]; if (!nnid_linked(id, sizeof(id))) { snprintf(msg, msglen, "Hier ist keine ID verknüpft."); return false; } if (!confirmed) { armed = BTN_UNLINK; snprintf(msg, msglen, "\"%s\" vom 3DS lösen?\nNur auf dem 3DS, das Konto selbst bleibt unberührt.\n" "Zum Bestätigen nochmal antippen.", id); return false; } if (!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); return true; } case BTN_FTP: if (ftpctl_running()) ftpctl_stop(msg, msglen); else ftpctl_start(msg, msglen); ftpOn = ftpctl_running(); return false; case BTN_SPOTPASS: if (spotpassTestFrames >= 0) { snprintf(msg, msglen, "SpotPass-Test läuft schon ..."); return false; } if (spotpass_test_start(MBMNET_SERVER, msg, msglen)) spotpassTestFrames = 0; return false; case BTN_REBOOT: return true; case BTN_SHOP: if (shopctl_job() == SHOPJOB_RUNNING) { snprintf(msg, msglen, "Der Shop wird gerade installiert ..."); return false; } if (shopVersion >= 0 && shopVersion >= APP_TITLE_VERSION) { if (shopctl_launch(msg, msglen)) launchShop = true; return false; } if (shopVersion < 0 && !confirmed) { armed = BTN_SHOP; snprintf(msg, msglen, "MBMNET Shop installieren?\nEine eigene App mit Homebrew aus der Universal-DB\n" "(Spiele, Tools, Emulatoren). Zum Bestätigen nochmal antippen."); return false; } shopctl_install(MBMNET_SERVER); snprintf(msg, msglen, shopVersion < 0 ? "Installiere den MBMNET Shop ..." : "Aktualisiere den MBMNET Shop ..."); return false; case BTN_SHOP_DEL: if (shopVersion < 0) return false; if (!confirmed) { armed = BTN_SHOP_DEL; snprintf(msg, msglen, "MBMNET Shop entfernen?\nApps, die du darüber installiert hast, bleiben.\nZum Bestätigen nochmal antippen."); return false; } if (shopctl_remove(msg, msglen)) shopVersion = -1; return false; } return false; } static bool inside(Box b, touchPosition tp) { return tp.px >= b.x && tp.px < b.x + b.w && tp.py >= b.y && tp.py < b.y + b.h; } int main(int argc, char **argv) { gfxInitDefault(); httpcInit(0); // am:net: Updates (als CIA) und den MBMNET Shop installieren. Unter Luma auch als .3dsx erlaubt. bool am = R_SUCCEEDED(amInit()); C3D_Init(C3D_DEFAULT_CMDBUF_SIZE); C2D_Init(C2D_DEFAULT_MAX_OBJECTS); C2D_Prepare(); C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT); C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT); textBuf = C2D_TextBufNew(4096); nnid_init(); frdswitch_init(); const char *server = MBMNET_SERVER; char msg[256] = ""; // Pfad der laufenden App (vom Homebrew Launcher übergeben), damit das Update sie ersetzen kann const char *selfPath = argc > 0 && strncmp(argv[0], "sdmc:/", 6) == 0 ? argv[0] : "sdmc:/3ds/mbmnet.3dsx"; update_start(server, selfPath); UpdateState lastUpd = UPD_IDLE; 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 bool softReboot = false; // Patches alter Versionen lagen an einer Stelle, an der Luma sie nicht lädt if (patch_migrate()) snprintf(msg, sizeof(msg), "Neuer Speicherort für die Patches!\nBitte MBMNET einmal neu antippen."); PatchState st = patch_state(); // Nur im MBMNET-Modus ist die verknüpfte NNID ein MBMNET-Konto // Werkzeuge nur für Admins: gefragt wird mit der ID im MBMNET-Platz (auch wenn gerade Nintendo aktiv ist) { u8 mslot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET); char mid[20]; u32 mpid = 0; if (mslot && frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, mid, sizeof(mid))) ACT_GetAccountInfo(&mpid, sizeof(mpid), mslot, INFO_TYPE_PRINCIPAL_ID); admin_start(server, mpid); } // 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 if (st == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET) { u32 pid = frdswitch_my_pid(); if (pid && !frdswitch_pid_is_mbmnet(pid)) snprintf(msg, sizeof(msg), "Dein MBMNET-Freundekonto hat eine fremde Kennung (%lu).\n" "Tippe auf MBMNET, um es neu anzulegen.", (unsigned long)pid); } // Bei aktivem MBMNET: SpotPass-Aufgabe (neu) anmelden und Patches auffrischen - im Hintergrund if (st == STATE_MBMNET) { maintaining = true; s32 prio = 0x30; svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); maintenanceThread = threadCreate(maintenance_worker, NULL, 64 * 1024, prio + 1, -2, false); if (!maintenanceThread) maintaining = false; } // Das Hintergrund-Modul ist abgeschaltet (Kernel-Absturz): Reste wegräumen, nie wieder starten ftpctl_cleanup_module(); // MBMNET Shop: installiert? Ältere Version gleich im Hintergrund aktualisieren. shopVersion = am ? shopctl_version() : -1; if (shopVersion >= 0 && shopVersion < APP_TITLE_VERSION) shopctl_install(server); ftpOn = ftpctl_running(); int ftpCheckIn = 120; int selected = st == STATE_MBMNET ? BTN_DISABLE : BTN_ENABLE; bool toolsToggled = false; int queued = -1; // Aktion, die nach dem nächsten Bild läuft (damit sofort etwas passiert) while (aptMainLoop()) { hidScanInput(); u32 down = hidKeysDown(); if (--ftpCheckIn <= 0) { ftpCheckIn = 120; ftpOn = ftpctl_running(); } if (down & KEY_START && rebootIn == 0) break; // während des Umschaltens nicht abbrechen if (down & KEY_SELECT && admin_is()) toolsToggled = !toolsToggled; if (!admin_is()) toolsToggled = false; // Werkzeuge nur für Admins (Server hat es bestätigt) // Werkzeuge nur mit SELECT, beim Öffnen immer aus bool admin = toolsToggled; if (!visible(selected, admin)) selected = admin || selected <= BTN_REBOOT ? BTN_ENABLE : BTN_SHOP; int action = -1; // 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 up = down & (KEY_UP | KEY_CPAD_UP), dn = down & (KEY_DOWN | KEY_CPAD_DOWN); 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; } else if (left) selected = selected == BTN_DIAG ? BTN_REBOOT : selected - 1; else if (right) selected = selected == BTN_REBOOT ? BTN_DIAG : selected + 1; if (dn && selected < BTN_DIAG) selected = admin ? (selected == BTN_ENABLE ? BTN_DIAG : BTN_REBOOT) : (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; else if (up && selected >= BTN_DIAG) selected = selected == BTN_REBOOT ? BTN_DISABLE : BTN_ENABLE; if (down & KEY_A) action = selected; if (down & KEY_TOUCH) { touchPosition tp; hidTouchRead(&tp); for (int i = 0; i < BTN_COUNT; i++) if (visible(i, admin) && inside(box_of(i, admin), tp)) selected = action = i; } // Auffrischen fertig if (maintenanceChanged && !maintaining) { maintenanceChanged = false; if (!msg[0]) snprintf(msg, sizeof(msg), "Neue MBMNET-Patches (Miiverse) geschrieben.\n" "Bitte den 3DS einmal neu starten (Werkzeuge: Neustart)."); } // 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 && 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) { int a = queued; queued = -1; if (run(a, msg, sizeof(msg))) rebootIn = 15; // ca. 0,25 Sekunden zum Lesen st = patch_state(); } else if (action >= 0 && rebootIn == 0 && queued < 0) { queued = action; snprintf(msg, sizeof(msg), action <= BTN_DISABLE ? "Schalte um ..." : "Einen Moment ..."); } // Update gefunden: gleich laden, egal ob MBMNET oder Nintendo aktiv ist. Nicht während des // Umschaltens, sonst startet der 3DS mitten in der Installation neu. UpdateState upd = update_state(); if (upd == UPD_AVAILABLE && rebootIn == 0 && queued < 0) { update_download(); snprintf(msg, sizeof(msg), "Lade Update auf v%s ...", update_new_version()); upd = update_state(); } // Update fertig: kurz anzeigen, dann neu starten if (upd == UPD_INSTALLED && lastUpd != UPD_INSTALLED) { snprintf(msg, sizeof(msg), "Update auf v%s installiert!\nDie App startet gleich neu ...", update_new_version()); restartIn = 90; // ca. 1,5 Sekunden } if (upd == UPD_ERROR && lastUpd == UPD_DOWNLOADING) { char why[96]; update_error(why, sizeof(why)); snprintf(msg, sizeof(msg), "Update nach 3 Versuchen fehlgeschlagen:\n%s\nProtokoll: /3ds/mbmnet/update.log", why); } lastUpd = upd; // MBMNET Shop fertig installiert (oder fehlgeschlagen) ShopJob sj = shopctl_job(); if (sj == SHOPJOB_DONE || sj == SHOPJOB_FAILED) { snprintf(msg, sizeof(msg), "%s", shopctl_message()); shopctl_acknowledge(); shopVersion = shopctl_version(); } if (launchShop) break; // SpotPass-Test: jede Sekunde nachsehen if (spotpassTestFrames >= 0 && ++spotpassTestFrames % 60 == 0 && !spotpass_test_poll(spotpassTestFrames / 60, msg, sizeof(msg))) spotpassTestFrames = -1; // Neustart erst, wenn die Freunde-Anmeldung durch ist (höchstens ein paar Sekunden) // Schneller Neustart wie bei Nimbus (Soft-Reboot über ns:s, ohne boot9strap/Luma-Splash): erst die App // sauber beenden, dann neu starten (unten nach dem Aufräumen) if (rebootIn > 0 && --rebootIn == 0) { softReboot = true; break; } if (restartIn > 0 && --restartIn == 0) { if (relaunch_prepare(selfPath)) break; snprintf(msg, sizeof(msg), "Update installiert!\nBitte die App von Hand neu starten (START)."); } C2D_TextBufClear(textBuf); C3D_FrameBegin(C3D_FRAME_SYNCDRAW); C2D_TargetClear(top, COL_BG); C2D_SceneBegin(top); draw_top(st, msg, admin, admin_is()); C2D_TargetClear(bot, COL_BG); C2D_SceneBegin(bot); draw_bottom(selected, st, admin); C3D_FrameEnd(0); } // Hintergrund-Threads, die noch auf den Server warten, dürfen ihren Speicher nicht verlieren: Sonst // schreiben sie nach dem Freigeben hinein und Luma zeigt einen Absturz (z. B. beim Neustart nach "Nintendo"). ftpctl_stop(NULL, 0); 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; if (maintenanceThread) { switchRequested = true; 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 = 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) { // Hart beenden, ohne Speicher freizugeben (der Kernel räumt alles auf) aptExit(); svcExitProcess(); } frdswitch_exit(); nnid_exit(); httpcExit(); if (am) amExit(); C2D_TextBufDelete(textBuf); C2D_Fini(); C3D_Fini(); gfxExit(); return 0; }