874 lines
41 KiB
C
874 lines
41 KiB
C
// 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 öffnet für Admins (vom Server bestätigt) eine eigene Werkzeug-Seite, darin u. a. die Logs.
|
|
#include <3ds.h>
|
|
#include <citro2d.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/stat.h>
|
|
|
|
#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"
|
|
#include "logview.h"
|
|
#include "ui.h"
|
|
|
|
// Bedienelemente auf dem unteren Bildschirm. Normal: MBMNET / Nintendo und die Shop-Leiste.
|
|
// Werkzeug-Seite (nur Admins, SELECT): sechs große Knöpfe statt der Kacheln.
|
|
enum { BTN_ENABLE, BTN_DISABLE, BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_REBOOT, BTN_LOGS, 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 6
|
|
// 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 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 !admin; // auf der Werkzeug-Seite keine Kacheln
|
|
if (btn <= BTN_LOGS) 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_SHOP: return shopBox;
|
|
case BTN_SHOP_DEL: return shopDelBox;
|
|
default: return toolBoxes[tool_index(btn) < 0 ? 0 : tool_index(btn)];
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
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);
|
|
}
|
|
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.
|
|
// 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 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);
|
|
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);
|
|
}
|
|
|
|
// 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);
|
|
text("MBMNET", TOP_W / 2, y + 70, 0.9f, COL_WHITE, C2D_AlignCenter);
|
|
y += 108;
|
|
|
|
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 {
|
|
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);
|
|
if (!admin) {
|
|
draw_tile(BTN_ENABLE, selected == BTN_ENABLE, st == STATE_MBMNET, admin);
|
|
draw_tile(BTN_DISABLE, selected == BTN_DISABLE, st == STATE_ORIGINAL, admin);
|
|
draw_shop_bar(selected);
|
|
return;
|
|
}
|
|
// Werkzeug-Seite: sechs große Knöpfe mit kurzer Erklärung
|
|
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];
|
|
bool sel = selected == btn;
|
|
bool on = btn == BTN_FTP && ftpOn;
|
|
if (sel) round_rect(b.x - 2, b.y - 2, b.w + 4, b.h + 4, 13, 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 + 14, b.y + 21, 0.5f, 4, COL_GREEN);
|
|
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);
|
|
}
|
|
}
|
|
|
|
// --- 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;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
(void)arg;
|
|
char ignore[96];
|
|
// 1. Patches auffrischen (Miiverse-CA aus dem Zwischenspeicher; nur beim allerersten Mal vom Server)
|
|
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);
|
|
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;
|
|
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;
|
|
}
|
|
|
|
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 "<Name>.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 "<Name>.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_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());
|
|
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 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];
|
|
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) {
|
|
armed = BTN_UNLINK;
|
|
snprintf(msg, msglen, "\"%s\"%s vom 3DS lösen?\nNur auf dem 3DS, das Konto selbst bleibt unberührt.\n"
|
|
"Zum Bestätigen nochmal Y drücken.", id, slot ? " (MBMNET-Platz)" : "");
|
|
return false;
|
|
}
|
|
if (slot ? !nnid_unlink_slot(slot, msg, msglen) : !nnid_unlink(msg, msglen)) return false;
|
|
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;
|
|
}
|
|
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_LOGS:
|
|
logsOpen = true;
|
|
logview_open(MBMNET_SERVER);
|
|
msg[0] = 0;
|
|
return false;
|
|
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(8192); // Logs brauchen mehr Platz
|
|
measureBuf = C2D_TextBufNew(1024);
|
|
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, 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
|
|
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;
|
|
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)
|
|
|
|
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
|
|
// 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
|
|
bool admin = toolsToggled;
|
|
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;
|
|
// 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 (admin) {
|
|
// Werkzeug-Raster: 2 Spalten, 3 Reihen
|
|
int i = tool_index(selected);
|
|
if (i < 0) i = 0;
|
|
if (left || right) i ^= 1;
|
|
if (up && i >= 2) i -= 2;
|
|
if (dn && i + 2 < TOOL_COUNT) i += 2;
|
|
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;
|
|
// 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) {
|
|
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);
|
|
if (logsOpen) logview_draw_top();
|
|
else draw_top(st, msg, admin, admin_is());
|
|
C2D_TargetClear(bot, COL_BG);
|
|
C2D_SceneBegin(bot);
|
|
if (logsOpen) logview_draw_bottom();
|
|
else 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 = logview_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_TextBufDelete(measureBuf);
|
|
C2D_Fini();
|
|
C3D_Fini();
|
|
gfxExit();
|
|
return 0;
|
|
}
|