MBMNET-App v2.8.2: Quellcode (GPLv3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-10-18 17:05:44 +02:00
co-authored by Claude Opus 5.5
commit d264cd93cd
42 changed files with 7054 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
// Entwickler-Werkzeuge nur für Admins: Die App fragt bei MBMNET, ob die verknüpfte NNID einem Admin
// 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.
#include <3ds.h>
#include <stdio.h>
#include <string.h>
#include "admin.h"
#define CACHE_FILE "sdmc:/3ds/mbmnet/admin"
static volatile bool isAdmin;
static Thread thread;
static char url[128];
static void remember(bool on)
{
FILE *f = fopen(CACHE_FILE, "w");
if (f) { fputs(on ? "1" : "0", f); fclose(f); }
}
static void worker(void *arg)
{
(void)arg;
httpcContext ctx;
if (R_FAILED(httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1))) return;
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
u32 status = 0, got = 0;
char body[32] = { 0 };
Result rc = httpcBeginRequest(&ctx);
if (R_SUCCEEDED(rc)) rc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 8ULL * 1000 * 1000 * 1000);
if (R_SUCCEEDED(rc) && status == 200) httpcDownloadData(&ctx, (u8 *)body, sizeof(body) - 1, &got);
httpcCloseContext(&ctx);
if (status != 200 || strncmp(body, "admin=", 6) != 0) return; // keine Antwort: gemerkte gilt weiter
isAdmin = body[6] == '1';
remember(isAdmin);
}
void admin_start(const char *server, u32 pid)
{
FILE *f = fopen(CACHE_FILE, "r");
if (f) { isAdmin = fgetc(f) == '1'; fclose(f); }
if (!pid) return;
snprintf(url, sizeof(url), "http://%s/mbmnet/app-info?pid=%lu", server, (unsigned long)pid);
s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
thread = threadCreate(worker, NULL, 16 * 1024, prio + 1, -2, false);
}
bool admin_is(void) { return isAdmin; }
void admin_finish(void)
{
if (!thread) return;
if (R_SUCCEEDED(threadJoin(thread, 3ULL * 1000 * 1000 * 1000))) threadFree(thread);
thread = NULL;
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <3ds/types.h>
// Fragt im Hintergrund bei MBMNET, ob die verknüpfte NNID (pid) einem Admin gehört.
// pid = 0: nicht fragen, nur die gemerkte Antwort vom letzten Mal benutzen.
void admin_start(const char *server, u32 pid);
// Admin? (gemerkte Antwort, bis der Server geantwortet hat)
bool admin_is(void);
void admin_finish(void);
+58
View File
@@ -0,0 +1,58 @@
// Entpacker für das "Backwards LZ"-Format, mit dem Nintendo die .code von
// Systemmodulen komprimiert. Luma3DS wendet code.ips-Patches auf die
// ENTPACKTE .code an, deshalb müssen wir genauso rechnen.
#include <string.h>
#include "blz.h"
static u32 read32(const u8 *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | ((u32)p[3] << 24); }
bool blz_looks_compressed(const u8 *buf, u32 size)
{
if (size < 8) return false;
u32 enc = read32(buf + size - 8);
u32 add = read32(buf + size - 4);
u32 hdr = enc >> 24;
u32 enclen = enc & 0xFFFFFF;
return hdr >= 8 && hdr <= 0x10 && enclen >= hdr && enclen <= size && add > 0 && add < 0x1000000;
}
u32 blz_decompressed_size(const u8 *buf, u32 size)
{
return size + read32(buf + size - 4);
}
// buf muss mindestens blz_decompressed_size() Bytes groß sein.
// Daten liegen am Anfang von buf, es wird rückwärts in-place entpackt.
bool blz_decompress(u8 *buf, u32 size)
{
u32 enc = read32(buf + size - 8);
u32 hdr = enc >> 24;
u32 enclen = enc & 0xFFFFFF;
u32 outsize = blz_decompressed_size(buf, size);
u8 *src = buf + size - hdr;
u8 *srcEnd = buf + size - enclen;
u8 *dst = buf + outsize;
while (src > srcEnd) {
u8 flags = *--src;
for (int i = 0; i < 8 && src > srcEnd; i++, flags <<= 1) {
if (flags & 0x80) {
if (src - 2 < buf) return false;
u8 hi = *--src;
u8 lo = *--src;
u32 len = (hi >> 4) + 3;
u32 disp = (((hi & 0x0F) << 8) | lo) + 3;
for (u32 j = 0; j < len; j++) {
if (dst - 1 < buf || dst - 1 + disp >= buf + outsize) return false;
dst--;
*dst = dst[disp];
}
} else {
if (dst - 1 < buf) return false;
*--dst = *--src;
}
}
}
return true;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <3ds/types.h>
bool blz_looks_compressed(const u8 *buf, u32 size);
u32 blz_decompressed_size(const u8 *buf, u32 size);
bool blz_decompress(u8 *buf, u32 size);
+245
View File
@@ -0,0 +1,245 @@
// Eigenes MBMNET-Freundekonto, wie bei Pretendos Nimbus:
//
// Freundekonto 1: Nintendo - mit Konto-Platz 1 (Nintendo Network ID) und der Nintendo-Freundesliste
// Freundekonto 3: MBMNET - mit eigenem Konto-Platz (MBMNET-Konto) und eigener Freundesliste
//
// (Freundekonto 2 benutzt Pretendos Nimbus, das bleibt unangetastet.)
//
// Umschalten: beide Konten abmelden, das andere laden, seinen Konto-Platz zum Standard machen, Neustart.
// Beim ersten Mal legt die App das MBMNET-Freundekonto an. Es hat noch keine Kennung; beim ersten
// Login holt es sich bei MBMNET ein neues Konto (NASC "passwd"), der Server gibt ihm den Freundescode
// des MBMNET-Kontos von der Webseite.
#include <3ds.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "frdswitch.h"
#define RESULT_LOCAL_ACCOUNT_NOT_EXISTS 0xC880C4ED
#define LOG_FILE "sdmc:/3ds/mbmnet/konto.log"
static bool ready;
static FILE *logf;
static const char *failedStep;
static Result step(const char *name, Result rc)
{
if (logf) fprintf(logf, "%-36s %08lX%s\n", name, (unsigned long)rc, R_FAILED(rc) ? " <- FEHLER" : "");
if (R_FAILED(rc) && !failedStep) failedStep = name;
return rc;
}
static void note(const char *name, Result rc)
{
if (logf) fprintf(logf, "%-36s %08lX%s\n", name, (unsigned long)rc, R_FAILED(rc) ? " (ignoriert)" : "");
}
Result frdswitch_init(void)
{
Result rc = ndmuInit();
if (R_SUCCEEDED(rc)) rc = frdInit(false); // frd:a (Verwaltung)
if (R_SUCCEEDED(rc)) rc = actInit(false); // act:a
// Ohne diese SDK-Version lehnt der Freunde-Dienst Anlegen und Umschalten ab (so macht es Nimbus;
// daran scheiterte unser erster Versuch mit C960C4F6)
if (R_SUCCEEDED(rc)) rc = FRD_SetClientSdkVersion(0x70000c8);
ready = R_SUCCEEDED(rc);
return rc;
}
void frdswitch_exit(void)
{
actExit();
frdExit();
ndmuExit();
}
u8 frdswitch_current(void)
{
u8 id = 0;
if (ready) FRD_GetMyLocalAccountId(&id);
return id;
}
// Konto-Platz (act) zu einem Freundekonto. 0 = keiner.
static u32 act_slot_of(u8 friendAccount)
{
u8 count = 0;
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return 0;
for (u8 slot = 1; slot <= count; slot++) {
u8 fp = 0;
if (R_SUCCEEDED(ACT_GetAccountInfo(&fp, sizeof(fp), slot, INFO_TYPE_FP_LOCAL_ACCOUNT_ID)) && fp == friendAccount) return slot;
}
return 0;
}
bool frdswitch_slot_linked(u8 friendAccount, char *id, size_t len)
{
id[0] = 0;
u32 slot = ready ? act_slot_of(friendAccount) : 0;
if (!slot) return false;
AccountId acc = { 0 };
if (R_FAILED(ACT_GetAccountInfo(acc, sizeof(acc), slot, INFO_TYPE_ACCOUNT_ID))) return false;
snprintf(id, len, "%s", acc);
return id[0] != 0;
}
// Beide Konten abmelden. Das Freundekonto lässt sich nur offline entladen.
static Result unload(void)
{
note("Konto (act) abmelden", ACTA_UnloadConsoleAccount()); // ohne geladenes Konto: "ungültiger Zustand"
Result rc = step("Offline gehen (exklusiv)", NDMU_EnterExclusiveState(NDM_EXCLUSIVE_STATE_LOCAL_COMMUNICATIONS));
if (R_FAILED(rc)) return rc;
for (int i = 0; i < 150; i++) { // höchstens 15 Sekunden
bool online = true;
if (R_FAILED(FRD_IsOnline(&online)) || !online) break;
svcSleepThread(100 * 1000 * 1000);
}
// Hängt der Freunde-Dienst noch an einer Anfrage, kurz wiederholen
for (int attempt = 1; attempt <= 20; attempt++) {
rc = FRDA_UnloadLocalAccount();
if (R_SUCCEEDED(rc)) {
if (logf) fprintf(logf, "%-36s %08lX (Versuch %d)\n", "Freundekonto abmelden", (unsigned long)rc, attempt);
return rc;
}
svcSleepThread(500 * 1000 * 1000);
}
return step("Freundekonto abmelden (10 s)", rc);
}
// Freundekonto laden. "Schon geladen" (z. B. direkt nach dem Anlegen) zählt als Erfolg.
static Result load_friend_account(u8 friendAccount)
{
Result rc = FRDA_LoadLocalAccount(friendAccount);
if (R_FAILED(rc) && frdswitch_current() == friendAccount) {
note("Freundekonto laden (war schon geladen)", rc);
return 0;
}
return step("Freundekonto laden", rc);
}
// Konto-Platz (act) des Freundekontos zum Standard machen; fehlt er, anlegen. *created: neu angelegt.
static Result use_act_slot(u8 friendAccount, u32 *slotOut, bool *created)
{
Result rc = 0;
u32 slot = act_slot_of(friendAccount);
*created = false;
if (!slot) {
u8 count = 0;
rc = step("Konto-Plätze zählen", ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS));
if (R_SUCCEEDED(rc)) rc = step("Konto-Platz anlegen", ACTA_CreateConsoleAccount());
if (R_FAILED(rc)) return rc;
slot = count + 1;
rc = step("Konto-Platz speichern", ACTA_CommitConsoleAccount(slot));
if (R_FAILED(rc)) return rc;
*created = true;
}
if (logf) fprintf(logf, " -> Konto-Platz %lu\n", (unsigned long)slot);
*slotOut = slot;
return step("Standard-Konto setzen", ACTA_SetDefaultAccount(slot));
}
bool frdswitch_to(u8 target, bool *created, char *msg, size_t msglen)
{
*created = false;
if (!ready) {
snprintf(msg, msglen, "Kontodienste nicht verfügbar.\nIst Luma3DS aktuell?");
return false;
}
mkdir("sdmc:/3ds/mbmnet", 0777);
logf = fopen(LOG_FILE, "w");
failedStep = NULL;
u8 from = frdswitch_current();
if (logf) fprintf(logf, "MBMNET: Freundekonto %u -> %u (App v%s)\n\n", from, target, APP_VERSION);
u32 slot = 0;
bool newSlot = false;
Result rc = unload();
if (R_SUCCEEDED(rc)) {
rc = FRDA_LoadLocalAccount(target);
if (rc == (Result)RESULT_LOCAL_ACCOUNT_NOT_EXISTS && target == FRIEND_ACCOUNT_MBMNET) {
// Erstes Mal: eigenes MBMNET-Freundekonto anlegen (normale Umgebung, die Patches lenken sie um)
note("Freundekonto laden (gibt es noch nicht)", rc);
rc = step("MBMNET-Freundekonto anlegen", FRDA_CreateLocalAccount(FRIEND_ACCOUNT_MBMNET, 0, 0, 1));
if (R_SUCCEEDED(rc)) { *created = true; rc = load_friend_account(target); }
} else if (R_FAILED(rc) && frdswitch_current() == target) {
note("Freundekonto laden (war schon geladen)", rc);
rc = 0;
} else {
step("Freundekonto laden", rc);
}
if (R_SUCCEEDED(rc)) rc = use_act_slot(target, &slot, &newSlot);
// Neuer Platz: frisch zurücksetzen, damit "Vorhandene ID verknüpfen" angeboten wird (darf scheitern)
if (R_SUCCEEDED(rc) && newSlot) note("Konto-Platz zurücksetzen", ACTA_UnbindServerAccount(slot, true));
}
if (R_FAILED(rc)) {
// Zurück auf das vorherige Freundekonto, damit der 3DS nicht ohne/mit falschem Konto dasteht.
// Erst entladen - ein geladenes Konto lässt sich nicht einfach ersetzen.
u8 back = from ? from : FRIEND_ACCOUNT_NINTENDO;
if (frdswitch_current() != back) {
note("Rückweg: Freundekonto abmelden", FRDA_UnloadLocalAccount());
note("Rückweg: altes Freundekonto laden", FRDA_LoadLocalAccount(back));
}
NDMU_LeaveExclusiveState();
}
if (logf) { fprintf(logf, "\nErgebnis: %s\n", R_SUCCEEDED(rc) ? "OK" : "FEHLER"); fclose(logf); logf = NULL; }
if (R_FAILED(rc)) {
snprintf(msg, msglen, "Umschalten fehlgeschlagen bei:\n%s (Fehler %08lX)\nProtokoll: /3ds/mbmnet/konto.log",
failedStep ? failedStep : "?", (unsigned long)rc);
return false;
}
return true;
}
u32 frdswitch_my_pid(void)
{
FriendKey key = { 0 };
if (!ready || R_FAILED(FRD_GetMyFriendKey(&key))) return 0;
return key.principalId;
}
bool frdswitch_pid_is_mbmnet(u32 pid)
{
return pid >= MBMNET_PID_FIRST && pid <= MBMNET_PID_LAST;
}
// Das MBMNET-Freundekonto neu anlegen (es hat eine Kennung von einem anderen Server, z. B. Pretendo).
// Der Konto-Platz mit der verknüpften MBMNET-ID bleibt, nur das Freundekonto wird ersetzt. Danach
// Neustart: Beim ersten Login holt es sich bei MBMNET eine neue Kennung.
bool frdswitch_recreate_mbmnet(char *msg, size_t msglen)
{
if (!ready || frdswitch_current() != FRIEND_ACCOUNT_MBMNET) {
snprintf(msg, msglen, "MBMNET-Freundekonto ist nicht geladen.");
return false;
}
mkdir("sdmc:/3ds/mbmnet", 0777);
logf = fopen(LOG_FILE, "w");
failedStep = NULL;
if (logf) fprintf(logf, "MBMNET: Freundekonto 3 neu anlegen, alte Kennung %lu (App v%s)\n\n",
(unsigned long)frdswitch_my_pid(), APP_VERSION);
u32 slot = 0;
bool newSlot = false;
Result rc = unload();
if (R_SUCCEEDED(rc)) rc = step("MBMNET-Freundekonto neu anlegen", FRDA_CreateLocalAccount(FRIEND_ACCOUNT_MBMNET, 0, 0, 1));
if (R_SUCCEEDED(rc)) rc = load_friend_account(FRIEND_ACCOUNT_MBMNET);
if (R_SUCCEEDED(rc)) rc = use_act_slot(FRIEND_ACCOUNT_MBMNET, &slot, &newSlot);
if (R_FAILED(rc)) {
if (frdswitch_current() != FRIEND_ACCOUNT_MBMNET) note("Rückweg: Freundekonto laden", FRDA_LoadLocalAccount(FRIEND_ACCOUNT_MBMNET));
NDMU_LeaveExclusiveState();
}
if (logf) { fprintf(logf, "\nErgebnis: %s\n", R_SUCCEEDED(rc) ? "OK" : "FEHLER"); fclose(logf); logf = NULL; }
if (R_FAILED(rc)) {
snprintf(msg, msglen, "Neu anlegen fehlgeschlagen bei:\n%s (Fehler %08lX)\nProtokoll: /3ds/mbmnet/konto.log",
failedStep ? failedStep : "?", (unsigned long)rc);
return false;
}
return true;
}
u8 frdswitch_slot_of(u8 friendAccount)
{
return ready ? (u8)act_slot_of(friendAccount) : 0;
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <3ds/types.h>
#include <stddef.h>
#define FRIEND_ACCOUNT_NINTENDO 1
#define FRIEND_ACCOUNT_MBMNET 3 // 2 benutzt Pretendos Nimbus
// Kennungen (PIDs), die MBMNET für Freundekonten vergibt (FRIEND_PID_BASE in db.js)
#define MBMNET_PID_FIRST 1900000001u
#define MBMNET_PID_LAST 1999999999u
Result frdswitch_init(void);
void frdswitch_exit(void);
// Geladenes Freundekonto (1 = Nintendo, 3 = MBMNET, 0 = unbekannt)
u8 frdswitch_current(void);
// Ist im Konto-Platz dieses Freundekontos eine ID verknüpft? (Name in id)
bool frdswitch_slot_linked(u8 friendAccount, char *id, size_t len);
// Auf ein Freundekonto umschalten (MBMNET: beim ersten Mal anlegen, *created = true).
// Danach muss der 3DS neu starten. false + msg bei Fehler.
bool frdswitch_to(u8 target, bool *created, char *msg, size_t msglen);
// Kennung des geladenen Freundekontos (0 = noch keine)
u32 frdswitch_my_pid(void);
bool frdswitch_pid_is_mbmnet(u32 pid);
// MBMNET-Freundekonto neu anlegen (hat eine fremde Kennung). Danach Neustart.
bool frdswitch_recreate_mbmnet(char *msg, size_t msglen);
// Konto-Platz (act) eines Freundekontos, 0 = keiner
u8 frdswitch_slot_of(u8 friendAccount);
+2
View File
@@ -0,0 +1,2 @@
// FTP-Kern (Befehle, Pfade, Schreibschutz) aus dem FTP-Modul wiederverwenden - eine Quelle für beides
#include "../ftpmodul/source/ftp.c"
+109
View File
@@ -0,0 +1,109 @@
// FTP-Server in der App (Entwickler-Werkzeuge). Läuft in einem eigenen Thread, solange die App offen ist.
//
// Das Hintergrund-Modul (ftpmodul/, eigener Prozess auf Kern 1) ist abgeschaltet: Es brachte den
// Kernel-Scheduler auf Kern 1 zum Absturz (PC FFF21584, kaputter Thread in der Warteschlange). Die App
// räumt seine Dateien weg, damit es nie wieder startet.
//
// Ohne Passwort, Port 5000, Lesen von der ganzen SD, Schreiben/Löschen nur in /3ds/mbmnet (ftp.c).
#include <3ds.h>
#include <arpa/inet.h>
#include <malloc.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "ftpctl.h"
#include "../ftpmodul/source/ftp.h"
#define SOC_BUFFER_SIZE 0x100000
static u32 *socBuffer;
static Thread thread;
static volatile bool running;
static char address[32];
static char status[64];
bool ftp_should_exit(void) { return !running; }
static void server(void *arg)
{
(void)arg;
while (running) {
u32 ip = (u32)gethostid();
if (!ip) { snprintf(status, sizeof(status), "kein WLAN"); address[0] = 0; svcSleepThread(1000000000LL); continue; }
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(CONTROL_PORT), .sin_addr.s_addr = INADDR_ANY };
if (fd < 0 || bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0 || listen(fd, 2) != 0) {
if (fd >= 0) close(fd);
snprintf(status, sizeof(status), "Port %d belegt", CONTROL_PORT);
svcSleepThread(2000000000LL);
continue;
}
snprintf(address, sizeof(address), "%lu.%lu.%lu.%lu:%d", (unsigned long)(ip & 0xFF), (unsigned long)((ip >> 8) & 0xFF),
(unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24), CONTROL_PORT);
ftp_serve(fd, ip); // bis WLAN weg, IP neu oder "FTP aus"
close(fd);
address[0] = 0;
}
}
// Reste des abgeschalteten Hintergrund-Moduls entfernen (sonst startete eine alte App-Version es wieder)
void ftpctl_cleanup_module(void)
{
remove("sdmc:/luma/sysmodules/0004013000F1E002.cxi");
remove("sdmc:/luma/sysmodules/0004013000F1E001.cxi");
remove("sdmc:/3ds/mbmnet/ftp-an");
remove("sdmc:/3ds/mbmnet/ftp-stopp");
remove("sdmc:/3ds/mbmnet/ftp-adresse.txt");
}
bool ftpctl_running(void) { return running; }
bool ftpctl_start(char *msg, size_t msglen)
{
if (running) return true;
socBuffer = memalign(0x1000, SOC_BUFFER_SIZE);
if (!socBuffer || R_FAILED(socInit(socBuffer, SOC_BUFFER_SIZE))) {
free(socBuffer);
socBuffer = NULL;
snprintf(msg, msglen, "FTP: Netzwerk nicht verfügbar.");
return false;
}
running = true;
status[0] = address[0] = 0;
s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
thread = threadCreate(server, NULL, 32 * 1024, prio + 1, -2, false);
if (!thread) {
running = false;
socExit();
free(socBuffer);
socBuffer = NULL;
snprintf(msg, msglen, "FTP: konnte nicht starten.");
return false;
}
snprintf(msg, msglen, "FTP-Server an (läuft, solange die App offen ist).\nOhne Passwort, Port 5000. Herunterladen von der ganzen SD,\n"
"Hochladen/Löschen nur in /3ds/mbmnet.");
return true;
}
void ftpctl_stop(char *msg, size_t msglen)
{
if (running) {
running = false;
threadJoin(thread, 5000000000LL);
threadFree(thread);
thread = NULL;
socExit();
free(socBuffer);
socBuffer = NULL;
}
address[0] = 0;
if (msg) snprintf(msg, msglen, "FTP-Server aus.");
}
bool ftpctl_resume(char *msg, size_t msglen) { (void)msg; (void)msglen; return false; }
const char *ftpctl_address(void) { return address; }
const char *ftpctl_status(void) { return status[0] ? status : "startet ..."; }
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <3ds/types.h>
#include <stddef.h>
// FTP-Server in der App (läuft, solange die App offen ist)
bool ftpctl_running(void);
bool ftpctl_start(char *msg, size_t msglen);
void ftpctl_stop(char *msg, size_t msglen);
// Nach einem Neustart des 3DS: wieder starten, wenn FTP zuletzt an war. true = gestartet (msg gesetzt)
bool ftpctl_resume(char *msg, size_t msglen);
// "192.168.1.42:5000", leer solange das Modul noch keine Adresse hat
const char *ftpctl_address(void);
// Letzte Zeile aus dem Protokoll des Moduls (/3ds/mbmnet/ftp.log), leer wenn keins
const char *ftpctl_status(void);
// Reste des abgeschalteten Hintergrund-Moduls entfernen
void ftpctl_cleanup_module(void);
+135
View File
@@ -0,0 +1,135 @@
// Ändert Optionen in Lumas Konfiguration (/luma/config.ini):
// splash_position -> Boot-Splash "MBMNET aktiv" anzeigen
// enable_external_firm_and_modules -> nötig, damit Luma die Patches in /luma/sysmodules lädt
// enable_game_patching -> nötig, damit Luma Patches in /luma/titles lädt (Miiverse)
// Beim Zurücksetzen wird eine Option nur wieder ausgeschaltet, wenn MBMNET sie eingeschaltet hat
// (Merker in /3ds/mbmnet/...).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include "lumaconfig.h"
#define CONFIG_FILE "sdmc:/luma/config.ini"
#define SPLASH_MARKER "sdmc:/3ds/mbmnet/splash-war-aus"
#define MODULES_MARKER "sdmc:/3ds/mbmnet/module-waren-aus"
#define PATCHING_MARKER "sdmc:/3ds/mbmnet/game-patching-war-aus"
// Setzt "key = value". Gibt den alten Wert in oldValue zurück. false, wenn Datei/Option fehlt.
static bool set_option(const char *key, const char *value, char *oldValue, size_t oldLen)
{
FILE *f = fopen(CONFIG_FILE, "rb");
if (!f) return false;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size <= 0 || size > 64 * 1024) { fclose(f); return false; }
char *in = malloc(size + 1);
char *out = malloc(size + 128);
bool ok = in && out && fread(in, 1, size, f) == (size_t)size;
fclose(f);
if (!ok) { free(in); free(out); return false; }
in[size] = 0;
size_t keyLen = strlen(key);
bool found = false;
size_t o = 0;
for (char *line = in; *line;) {
char *end = strchr(line, '\n');
size_t len = end ? (size_t)(end - line + 1) : strlen(line);
char *p = line;
while (*p == ' ' || *p == '\t') p++;
char *eq = strchr(p, '=');
bool isKey = !found && strncmp(p, key, keyLen) == 0 && (p[keyLen] == ' ' || p[keyLen] == '=')
&& eq && eq < line + len;
if (isKey) {
char *v = eq + 1;
while (*v == ' ') v++;
size_t vl = strcspn(v, "\r\n");
snprintf(oldValue, oldLen, "%.*s", (int)vl, v);
o += sprintf(out + o, "%s = %s%s", key, value, (end && end > line && end[-1] == '\r') ? "\r\n" : "\n");
found = true;
} else {
memcpy(out + o, line, len);
o += len;
}
line += len;
}
if (found && strcasecmp(oldValue, value) != 0) {
f = fopen(CONFIG_FILE, "wb");
ok = f && fwrite(out, 1, o, f) == o;
if (f) fclose(f);
}
free(in);
free(out);
return found && ok;
}
static void touch(const char *path)
{
FILE *m = fopen(path, "w");
if (m) fclose(m);
}
static bool exists(const char *path)
{
struct stat st;
return stat(path, &st) == 0;
}
bool lumaconfig_enable_splash(void)
{
char old[32] = "";
if (!set_option("splash_position", "after payloads", old, sizeof(old))) return false;
if (strcasecmp(old, "off") == 0) touch(SPLASH_MARKER);
return true;
}
void lumaconfig_restore_splash(void)
{
if (!exists(SPLASH_MARKER)) return; // war schon vorher an: nichts ändern
char old[32] = "";
set_option("splash_position", "off", old, sizeof(old));
remove(SPLASH_MARKER);
}
bool lumaconfig_enable_modules(void)
{
char old[8] = "";
if (!set_option("enable_external_firm_and_modules", "1", old, sizeof(old))) return false;
if (strcmp(old, "0") == 0) touch(MODULES_MARKER);
return true;
}
void lumaconfig_keep_modules(void)
{
remove(MODULES_MARKER);
}
void lumaconfig_restore_modules(void)
{
if (!exists(MODULES_MARKER)) return;
char old[8] = "";
set_option("enable_external_firm_and_modules", "0", old, sizeof(old));
remove(MODULES_MARKER);
}
bool lumaconfig_enable_game_patching(void)
{
char old[8] = "";
if (!set_option("enable_game_patching", "1", old, sizeof(old))) return false;
if (strcmp(old, "0") != 0) return false; // war schon an
touch(PATCHING_MARKER);
return true;
}
void lumaconfig_restore_game_patching(void)
{
if (!exists(PATCHING_MARKER)) return;
char old[8] = "";
set_option("enable_game_patching", "0", old, sizeof(old));
remove(PATCHING_MARKER);
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <stdbool.h>
// Splash in Lumas Konfiguration einschalten ("after payloads"). false, wenn config.ini fehlt.
bool lumaconfig_enable_splash(void);
// Splash wieder ausschalten, falls MBMNET ihn eingeschaltet hat.
void lumaconfig_restore_splash(void);
// "Enable loading external FIRMs and system modules" einschalten - ohne diese Option lädt
// Luma die Patches in /luma/sysmodules nicht.
bool lumaconfig_enable_modules(void);
// Wieder ausschalten, falls MBMNET sie eingeschaltet hat.
void lumaconfig_restore_modules(void);
// Option dauerhaft anlassen (das FTP-Modul braucht sie auch im Nintendo-Modus)
void lumaconfig_keep_modules(void);
// "Enable game patching" einschalten - ohne sie lädt Luma die Patches in /luma/titles (Miiverse) nicht.
// true, wenn sie dadurch erst eingeschaltet wurde (dann ist ein Neustart nötig).
bool lumaconfig_enable_game_patching(void);
// Wieder ausschalten, falls MBMNET sie eingeschaltet hat.
void lumaconfig_restore_game_patching(void);
+547
View File
@@ -0,0 +1,547 @@
// 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 <citro2d.h>
#include <stdio.h>
#include <string.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"
#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.
enum { BTN_ENABLE, BTN_DISABLE, BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_REBOOT, BTN_COUNT };
typedef struct { float x, y, w, h; } Box;
#define TILE_Y 14
#define TILE_W 142
#define TILE_H_FULL 210
#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 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];
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" : "START: Beenden SELECT: Werkzeuge";
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);
}
}
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) 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; }
static int armed = -1; // Sicherheitsabfrage (zweimal antippen)
// 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);
}
// 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));
snprintf(msg, msglen, linked ? "MBMNET ist schon aktiv (Konto %s)." : "MBMNET ist schon aktiv.\n"
"Jetzt dein MBMNET-Konto verknüpfen:\nEinstellungen > Nintendo Network ID", id);
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));
// Die gemerkte MBMNET-ID steckt noch in einem anderen Platz (z. B. Pretendo mit gleichem Namen)?
char mbmnetId[20];
remembered_mbmnet_id(mbmnetId, sizeof(mbmnetId));
u8 mbmnetSlot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET);
bool sameNameElsewhere = !migrate && mbmnetId[0] && nnid_name_elsewhere(mbmnetId, mbmnetSlot);
if (sameNameElsewhere && !confirmed) {
armed = BTN_ENABLE;
snprintf(msg, msglen, "\"%s\" ist noch in einem anderen Platz verknüpft (z. B. Pretendo).\n"
"Der 3DS erlaubt sie nur einmal: Sie wird dort gelöst, damit du sie\nbei MBMNET verknüpfen kannst. Zum Bestätigen nochmal antippen.", mbmnetId);
return false;
}
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);
if (sameNameElsewhere && !nnid_unlink_named(mbmnetId, mbmnetSlot, msg, msglen)) {
char ignore[160];
patch_disable(ignore, sizeof(ignore));
return false;
}
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
snprintf(msg, msglen, "MBMNET aktiv! Der 3DS startet neu.\n%s",
created ? "Eigenes MBMNET-Freundekonto angelegt. Danach einmal:\nEinstellungen > Nintendo Network ID > Vorhandene ID"
: linked ? "Deine MBMNET-Anmeldung und -Freundesliste sind wieder da."
: "Danach einmal: Einstellungen > Nintendo Network 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();
if (st == STATE_ORIGINAL && frdswitch_current() == FRIEND_ACCOUNT_NINTENDO) {
snprintf(msg, msglen, "Nintendo ist schon aktiv.");
return false;
}
if (st == STATE_FOREIGN) {
snprintf(msg, msglen, "MBMNET ist nicht aktiv (anderer Patch).");
return false;
}
// MBMNET-ID aus dem MBMNET-Platz lösen und merken: Heißt die Pretendo-ID genauso, ließe sie sich sonst
// im Pretendo-Platz nicht verknüpfen (022-2571). Beim Wechsel zurück zu MBMNET neu anmelden.
char id[20] = "";
bool unlinked = false;
if (frdswitch_current() == FRIEND_ACCOUNT_MBMNET && nnid_linked(id, sizeof(id))) {
remember_mbmnet_id(id);
if (!nnid_unlink(msg, msglen)) return false;
unlinked = true;
}
bool created = false;
if (frdswitch_current() != FRIEND_ACCOUNT_NINTENDO && !frdswitch_to(FRIEND_ACCOUNT_NINTENDO, &created, msg, msglen)) return false;
patch_disable(detail, sizeof(detail));
spotpass_unregister();
if (unlinked)
snprintf(msg, msglen, "Nintendo aktiv! Der 3DS startet neu.\n\"%s\" wurde vom MBMNET-Platz gelöst (für Pretendo).\n"
"Zurück zu MBMNET: danach einmal neu anmelden.", id);
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 && 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;
}
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);
// Als installierte CIA: am:net zum Installieren von Updates
bool am = !envIsHomebrew() && 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
// 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
admin_start(server, st == STATE_MBMNET ? nnid_pid() : 0);
// 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);
}
// Mitteilungen per SpotPass: bei aktivem MBMNET die Hintergrund-Aufgabe (neu) anmelden
if (st == STATE_MBMNET) {
char ignore[96];
spotpass_register(server, ignore, sizeof(ignore));
// Patches auffrischen: neue Versionen bringen neue Ziele mit (z. B. Miiverse ab v2.8.0)
bool changed = false;
char detail[160];
if (patch_enable(server, &changed, detail, sizeof(detail)) && changed && !msg[0])
snprintf(msg, sizeof(msg), "Neue MBMNET-Patches (Miiverse) geschrieben.\n"
"Bitte den 3DS einmal neu starten (Werkzeuge: Neustart).");
}
// Das Hintergrund-Modul ist abgeschaltet (Kernel-Absturz): Reste wegräumen, nie wieder starten
ftpctl_cleanup_module();
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) toolsToggled = !toolsToggled;
// Werkzeuge nur mit SELECT, beim Öffnen immer aus
bool admin = toolsToggled;
if (!admin && selected >= BTN_DIAG) selected = BTN_ENABLE;
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);
if (selected < BTN_DIAG) { if (left || right) selected ^= 1; }
else if (left) selected = selected == BTN_DIAG ? BTN_REBOOT : selected - 1;
else if (right) selected = selected == BTN_REBOOT ? BTN_DIAG : selected + 1;
if (admin && down & (KEY_DOWN | KEY_CPAD_DOWN) && selected < BTN_DIAG) selected = selected == BTN_ENABLE ? BTN_DIAG : BTN_REBOOT;
if (down & (KEY_UP | KEY_CPAD_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 < (admin ? BTN_COUNT : BTN_DIAG); i++)
if (inside(box_of(i, admin), tp)) selected = action = i;
}
if (queued >= 0) {
int a = queued;
queued = -1;
if (run(a, msg, sizeof(msg))) rebootIn = 45; // ca. 0,75 Sekunden zum Lesen
st = patch_state();
} else if (action >= 0 && rebootIn == 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;
// 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)
if (rebootIn > 0 && --rebootIn == 0 && R_FAILED(APT_HardwareResetAsync())) {
snprintf(msg, sizeof(msg), "Neustart fehlgeschlagen.\nBitte den 3DS von Hand neu starten.");
}
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);
}
ftpctl_stop(NULL, 0);
admin_finish();
update_finish();
frdswitch_exit();
nnid_exit();
httpcExit();
if (am) amExit();
C2D_TextBufDelete(textBuf);
C2D_Fini();
C3D_Fini();
gfxExit();
return 0;
}
+217
View File
@@ -0,0 +1,217 @@
// Nintendo Network ID auf dem 3DS: anzeigen und lokal lösen.
//
// Solange eine NNID verknüpft ist, bieten die Systemeinstellungen kein
// "Vorhandene ID verknüpfen" an. Damit man sich mit dem MBMNET-Konto anmelden kann,
// löst die App die Verknüpfung lokal (ohne Server). Name und PID der alten ID werden
// vorher in /3ds/mbmnet/alte-nnid.txt gesichert.
#include <3ds.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <time.h>
#include "nnid.h"
#define OLD_NNID_FILE "sdmc:/3ds/mbmnet/alte-nnid.txt"
#define LOG_FILE "sdmc:/3ds/mbmnet/konto.log"
static bool ready;
Result nnid_init(void)
{
Result rc = actInit(false); // false = act:a (Verwaltung)
ready = R_SUCCEEDED(rc);
return rc;
}
void nnid_exit(void)
{
if (ready) actExit();
}
static u8 current_slot(void)
{
u8 slot = 0;
ACT_GetCommonInfo(&slot, sizeof(slot), INFO_TYPE_COMMON_CURRENT_ACCOUNT_SLOT);
return slot;
}
bool nnid_linked(char *name, size_t len)
{
name[0] = 0;
if (!ready) return false;
AccountId id = { 0 };
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), current_slot(), INFO_TYPE_ACCOUNT_ID))) return false;
snprintf(name, len, "%s", id);
return name[0] != 0;
}
u32 nnid_pid(void)
{
u32 pid = 0;
char name[20];
if (nnid_linked(name, sizeof(name))) ACT_GetAccountInfo(&pid, sizeof(pid), current_slot(), INFO_TYPE_PRINCIPAL_ID);
return pid;
}
// Verknüpfte IDs in ANDEREN Konto-Plätzen (z. B. die Pretendo-ID aus Nimbus' zweitem Platz).
// Eine ID, die dort steht, darf der 3DS nicht noch einmal verknüpfen (Fehler 022-2571).
int nnid_others(char *names, size_t len)
{
names[0] = 0;
if (!ready) return 0;
u8 count = 0, current = current_slot();
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return 0;
int found = 0;
for (u8 slot = 1; slot <= count; slot++) {
if (slot == current) continue;
AccountId id = { 0 };
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) || !id[0]) continue;
size_t n = strlen(names);
snprintf(names + n, len - n, "%s\"%s\"", found ? ", " : "", id);
found++;
}
return found;
}
bool nnid_unlink_others(char *msg, size_t msglen)
{
if (!ready) return false;
u8 count = 0, current = current_slot();
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return false;
mkdir("sdmc:/3ds/mbmnet", 0777);
FILE *log = fopen(LOG_FILE, "a");
FILE *old = fopen(OLD_NNID_FILE, "a");
bool ok = true;
for (u8 slot = 1; slot <= count; slot++) {
if (slot == current) continue;
AccountId id = { 0 };
u32 pid = 0;
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) || !id[0]) continue;
ACT_GetAccountInfo(&pid, sizeof(pid), slot, INFO_TYPE_PRINCIPAL_ID);
if (old) fprintf(old, "Konto-Platz %u ID: %s PID: %lu (anderer Platz, z. B. Pretendo)\n", slot, id, (unsigned long)pid);
Result rc = ACTA_UnbindServerAccount(slot, true);
if (log) fprintf(log, "Platz %u (%s) lösen %08lX\n", slot, id, (unsigned long)rc);
if (R_FAILED(rc)) {
snprintf(msg, msglen, "ID \"%s\" (Platz %u) konnte nicht gelöst werden\n(Fehler %08lX).", id, slot, (unsigned long)rc);
ok = false;
}
}
if (old) fclose(old);
if (log) fclose(log);
return ok;
}
bool nnid_unlink(char *msg, size_t msglen)
{
if (!ready) {
snprintf(msg, msglen, "Kontodienst nicht verfügbar.\nIst Luma3DS aktuell?");
return false;
}
u8 slot = current_slot();
AccountId id = { 0 };
u32 pid = 0;
ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID);
ACT_GetAccountInfo(&pid, sizeof(pid), slot, INFO_TYPE_PRINCIPAL_ID);
if (!id[0]) return true; // nichts verknüpft
mkdir("sdmc:/3ds/mbmnet", 0777);
// Alte ID merken, damit der Name nicht verloren geht
FILE *f = fopen(OLD_NNID_FILE, "a");
if (f) {
time_t now = time(NULL);
char date[32];
strftime(date, sizeof(date), "%Y-%m-%d %H:%M", localtime(&now));
fprintf(f, "%s Konto-Platz %u NNID: %s PID: %lu\n", date, slot, id, (unsigned long)pid);
fclose(f);
}
FILE *log = fopen(LOG_FILE, "w");
if (log) fprintf(log, "MBMNET: NNID lösen (App v%s)\nKonto-Platz %u, NNID %s\n\n", APP_VERSION, slot, id);
Result rc = ACTA_UnbindServerAccount(slot, true);
if (log) fprintf(log, "%-30s %08lX\n", "Verknüpfung lösen", (unsigned long)rc);
if (R_FAILED(rc)) {
// Manche Zustände erlauben das nur bei abgemeldetem Konto: abmelden und erneut versuchen
Result u = ACTA_UnloadConsoleAccount();
if (log) fprintf(log, "%-30s %08lX\n", "Konto abmelden", (unsigned long)u);
rc = ACTA_UnbindServerAccount(slot, true);
if (log) fprintf(log, "%-30s %08lX\n", "Verknüpfung lösen (2.)", (unsigned long)rc);
}
if (log) { fprintf(log, "\nErgebnis: %s\n", R_SUCCEEDED(rc) ? "OK" : "FEHLER"); fclose(log); }
if (R_FAILED(rc)) {
snprintf(msg, msglen, "NNID konnte nicht gelöst werden (Fehler %08lX).\nProtokoll: /3ds/mbmnet/konto.log", (unsigned long)rc);
return false;
}
return true;
}
// Für die Diagnose: alle Konto-Plätze mit ihren Daten
void nnid_dump(FILE *f)
{
if (!ready) { fprintf(f, " Kontodienst nicht verfügbar\n"); return; }
u8 count = 0, current = 0, def = 0;
ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS);
ACT_GetCommonInfo(&current, sizeof(current), INFO_TYPE_COMMON_CURRENT_ACCOUNT_SLOT);
ACT_GetCommonInfo(&def, sizeof(def), INFO_TYPE_COMMON_DEFAULT_ACCOUNT_SLOT);
u8 friendAccount = 0;
FRD_GetMyLocalAccountId(&friendAccount);
fprintf(f, " %u Plätze, aktiv: %u, Standard: %u, geladenes Freundekonto: %u (1 = Nintendo, 3 = MBMNET)\n",
count, current, def, friendAccount);
for (u8 slot = 1; slot <= count; slot++) {
AccountId id = { 0 }, assigned = { 0 };
u32 pid = 0, assignedPid = 0;
u8 fp = 0;
bool committed = false;
Result r1 = ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID);
ACT_GetAccountInfo(assigned, sizeof(assigned), slot, INFO_TYPE_ASSIGNED_ACCOUNT_ID);
ACT_GetAccountInfo(&pid, sizeof(pid), slot, INFO_TYPE_PRINCIPAL_ID);
ACT_GetAccountInfo(&assignedPid, sizeof(assignedPid), slot, INFO_TYPE_ASSIGNED_PRINCIPAL_ID);
ACT_GetAccountInfo(&fp, sizeof(fp), slot, INFO_TYPE_FP_LOCAL_ACCOUNT_ID);
ACT_GetAccountInfo(&committed, sizeof(committed), slot, INFO_TYPE_IS_COMMITTED);
fprintf(f, " Platz %u: ID \"%s\" (Länge %u, Abfrage %08lX) PID %lu zugewiesen \"%s\"/%lu Freunde-Konto %u gespeichert %d\n",
slot, id, (unsigned)strlen(id), (unsigned long)r1, (unsigned long)pid, assigned,
(unsigned long)assignedPid, fp, committed);
}
}
// Gibt es die ID "name" in einem Konto-Platz außer exceptSlot?
bool nnid_name_elsewhere(const char *name, u8 exceptSlot)
{
if (!ready || !name[0]) return false;
u8 count = 0;
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return false;
for (u8 slot = 1; slot <= count; slot++) {
if (slot == exceptSlot) continue;
AccountId id = { 0 };
if (R_SUCCEEDED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) && !strcasecmp(id, name)) return true;
}
return false;
}
// Löst die ID "name" in allen Konto-Plätzen außer exceptSlot (nur auf dem 3DS). false + msg bei Fehler.
bool nnid_unlink_named(const char *name, u8 exceptSlot, char *msg, size_t msglen)
{
if (!ready) return false;
u8 count = 0;
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return false;
FILE *log = fopen(LOG_FILE, "a");
bool ok = true;
for (u8 slot = 1; slot <= count; slot++) {
if (slot == exceptSlot) continue;
AccountId id = { 0 };
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) || strcasecmp(id, name)) continue;
Result rc = ACTA_UnbindServerAccount(slot, true);
if (log) fprintf(log, "Platz %u (%s) lösen (gleicher Name wie MBMNET) %08lX\n", slot, id, (unsigned long)rc);
if (R_FAILED(rc)) {
snprintf(msg, msglen, "\"%s\" (Platz %u) ließ sich nicht lösen (Fehler %08lX).", id, slot, (unsigned long)rc);
ok = false;
}
}
if (log) fclose(log);
return ok;
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <3ds/types.h>
#include <stddef.h>
#include <stdio.h>
Result nnid_init(void);
void nnid_exit(void);
// true, wenn auf dem aktuellen Konto eine NNID verknüpft ist (Name in name)
bool nnid_linked(char *name, size_t len);
// PID der verknüpften NNID (0 = keine)
u32 nnid_pid(void);
// IDs, die in ANDEREN Konto-Plätzen verknüpft sind (z. B. Pretendo). Anzahl, Namen in names.
int nnid_others(char *names, size_t len);
// Löst diese IDs lokal (Namen werden in /3ds/mbmnet/alte-nnid.txt gesichert)
bool nnid_unlink_others(char *msg, size_t msglen);
// Löst die verknüpfte NNID lokal (Name/PID werden in /3ds/mbmnet/alte-nnid.txt gesichert)
bool nnid_unlink(char *msg, size_t msglen);
// Gibt es die ID "name" in einem Konto-Platz außer exceptSlot?
bool nnid_name_elsewhere(const char *name, u8 exceptSlot);
// Löst die ID "name" in allen Konto-Plätzen außer exceptSlot (nur auf dem 3DS)
bool nnid_unlink_named(const char *name, u8 exceptSlot, char *msg, size_t msglen);
// Schreibt alle Konto-Plätze in die Diagnose
void nnid_dump(FILE *f);
+109
View File
@@ -0,0 +1,109 @@
// Miiverse-Patch: leitet die Miiverse-App auf MBMNET um und lässt sie der MBMNET-Zertifizierungsstelle (CA)
// vertrauen, denn ihre Seiten lädt sie nur per HTTPS.
//
// Die Code-Änderungen stammen aus Pretendos Nimbus (patches/miiverse, GPLv3, nach Stary und Sono): mit armips
// übersetzt, statt Pretendos Discovery-Adresse und GlobalSign-Zertifikat kommen MBMNET-Adresse und -CA hinein.
// Sie gelten für die letzte Version der Miiverse-App (alle Regionen gleich). Vorher wird geprüft, dass die
// Nintendo-Discovery-Adresse genau an der erwarteten Stelle steht - sonst wird nichts gepatcht.
//
// 0x176F28 fügt der HTTP-Verbindung die CA hinzu (httpC:AddRootCA), Länge der CA an Byte 72
// 0x1D7FF8 / 0x1D80EC / 0x38DC30 SD-Karte zusätzlich einhängen und wieder aushängen
// 0x1E3224 / 0x1E3468 der Browser lädt seine CA aus sdmc:/3ds/mbmnet-ca.pem
// 0x38DCB0 die CA selbst (DER, höchstens 889 Bytes)
#include <3ds.h>
#include <stdio.h>
#include <string.h>
#include "olvpatch.h"
#define CODE_BASE 0x100000
#define DISCOVERY_ADDR 0x15748C
#define DISCOVERY_URL "https://discovery.olv.nintendo.net/v1/endpoint"
#define CA_ADDR 0x38DCB0
#define CA_MAX 889
#define CA_PEM_FILE "sdmc:/3ds/mbmnet-ca.pem"
static const u8 olv_176F28[] = { 0x3C, 0x00, 0x9F, 0xE5, 0x70, 0x4F, 0x1D, 0xEE, 0x0C, 0x10, 0x95, 0xE5, 0x14, 0x80, 0x95, 0xE5, 0x80, 0x00, 0xA4, 0xE5, 0x04, 0x10, 0x84, 0xE5, 0x08, 0x00, 0xA0, 0xE1, 0x24, 0x80, 0x9F, 0xE5, 0x24, 0x10, 0x9F, 0xE5, 0x08, 0x80, 0x84, 0xE5, 0x10, 0x10, 0x84, 0xE5, 0x08, 0x82, 0xA0, 0xE1, 0x0A, 0x80, 0x88, 0xE3, 0x0C, 0x80, 0x84, 0xE5, 0x32, 0x00, 0x00, 0xEF, 0x00, 0x00, 0xA0, 0xE1, 0x08, 0x00, 0x00, 0xEA, 0x82, 0x00, 0x24, 0x00, 0x79, 0x03, 0x00, 0x00, 0xB0, 0xDC, 0x38, 0x00, 0x00, 0x00, 0xA0, 0xE1, 0x00, 0x00, 0xA0, 0xE1, 0x00, 0x00, 0xA0, 0xE1 };
static const u8 olv_1D7FF8[] = { 0x0C, 0xD7, 0x06, 0xEA };
static const u8 olv_1D80EC[] = { 0xD4, 0xD6, 0x06, 0xEA };
static const u8 olv_1E3224[] = { 0x01, 0x20, 0xA0, 0xE3 };
static const u8 olv_1E3468[] = "3ds/mbmnet-ca.pem"; // mit Nullbyte: 18 Bytes
static const u8 olv_38DC30[] = { 0x00, 0x40, 0x2D, 0xE9, 0x2E, 0x28, 0xF9, 0xEB, 0x18, 0x00, 0x9F, 0xE5, 0x00, 0x40, 0xBD, 0xE8, 0xB6, 0x93, 0xF8, 0xEA, 0x00, 0x40, 0x2D, 0xE9, 0x02, 0x29, 0xF9, 0xEB, 0x04, 0x00, 0x9F, 0xE5, 0x00, 0x40, 0xBD, 0xE8, 0xBB, 0x92, 0xFA, 0xEA, 0x9C, 0x81, 0x1D, 0x00 };
#define CA_LEN_BYTE 72 // in olv_176F28: Länge der CA (Literal-Pool)
// Lädt eine Datei vom MBMNET-Server. Gibt die Länge zurück, 0 bei Fehler.
static u32 fetch(const char *server, const char *path, u8 *buf, u32 cap)
{
char url[160];
snprintf(url, sizeof(url), "http://%s%s", server, path);
httpcContext ctx;
if (R_FAILED(httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1))) return 0;
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
u32 status = 0, got = 0;
Result rc = httpcBeginRequest(&ctx);
if (R_SUCCEEDED(rc)) rc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 8ULL * 1000 * 1000 * 1000);
if (R_SUCCEEDED(rc) && status == 200) rc = httpcDownloadData(&ctx, buf, cap, &got);
httpcCloseContext(&ctx);
return R_SUCCEEDED(rc) && status == 200 && got < cap ? got : 0;
}
bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen)
{
static u8 pem[2048];
u32 n = fetch(server, "/olv/ca.der", der, CA_MAX + 1);
if (n == 0 || n > CA_MAX) return false;
u32 p = fetch(server, "/olv/ca.pem", pem, sizeof(pem));
if (p == 0) return false;
// Der Browser der Miiverse-App liest die CA von der SD-Karte
FILE *f = fopen(CA_PEM_FILE, "wb");
bool ok = f && fwrite(pem, 1, p, f) == p;
if (f) fclose(f);
*derLen = n;
return ok;
}
bool olvpatch_supported(const u8 *code, u32 size)
{
u32 off = DISCOVERY_ADDR - CODE_BASE;
return off + sizeof(DISCOVERY_URL) <= size && memcmp(code + off, DISCOVERY_URL, sizeof(DISCOVERY_URL)) == 0;
}
static u32 ips_record(u8 *out, u32 n, u32 cap, u32 addr, const u8 *data, u32 len)
{
if (n + 5 + len + 3 > cap) return 0;
u32 off = addr - CODE_BASE;
u8 head[5] = { off >> 16, off >> 8, off, len >> 8, len };
memcpy(out + n, head, 5);
memcpy(out + n + 5, data, len);
return n + 5 + len;
}
u32 olvpatch_build(const char *server, const u8 *der, u32 derLen, u8 *out, u32 cap)
{
// Discovery: "https://discovery.olv.nintendo.net/v1/endpoint" -> "http://<server>/v1/endpoint", Rest Nullbytes
u8 discovery[sizeof(DISCOVERY_URL)] = { 0 };
int len = snprintf((char *)discovery, sizeof(discovery), "http://%s/v1/endpoint", server);
if (len <= 0 || len >= (int)sizeof(discovery) || derLen == 0 || derLen > CA_MAX) return 0;
u8 caHook[sizeof(olv_176F28)];
memcpy(caHook, olv_176F28, sizeof(caHook));
caHook[CA_LEN_BYTE] = derLen;
caHook[CA_LEN_BYTE + 1] = derLen >> 8;
u32 n = 5;
memcpy(out, "PATCH", 5);
struct { u32 addr; const u8 *data; u32 len; } recs[] = {
{ DISCOVERY_ADDR, discovery, sizeof(discovery) },
{ 0x176F28, caHook, sizeof(caHook) },
{ 0x1D7FF8, olv_1D7FF8, sizeof(olv_1D7FF8) },
{ 0x1D80EC, olv_1D80EC, sizeof(olv_1D80EC) },
{ 0x1E3224, olv_1E3224, sizeof(olv_1E3224) },
{ 0x1E3468, olv_1E3468, sizeof(olv_1E3468) },
{ 0x38DC30, olv_38DC30, sizeof(olv_38DC30) },
{ CA_ADDR, der, derLen },
};
for (size_t i = 0; i < sizeof(recs) / sizeof(recs[0]); i++)
if (!(n = ips_record(out, n, cap, recs[i].addr, recs[i].data, recs[i].len))) return 0;
memcpy(out + n, "EOF", 3);
return n + 3;
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <3ds/types.h>
// 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.
bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen);
// Ist das genau die Miiverse-Version, für die der Patch gebaut ist? (entpackter .code)
bool olvpatch_supported(const u8 *code, u32 size);
// Baut den IPS-Patch. Gibt die Länge zurück, 0 bei Fehler.
u32 olvpatch_build(const char *server, const u8 *der, u32 derLen, u8 *out, u32 cap);
+885
View File
@@ -0,0 +1,885 @@
// Erzeugt und verwaltet die MBMNET-Patches.
//
// Gepatcht werden zwei Systemmodule:
// Freunde (0004013000003202): https://nasc.nintendowifi.net -> http://<server>
// Konto (0004013000003802): https://account.nintendo.net -> http://<server>
//
// Ablauf je Modul:
// 1. .code lesen (direkt aus dem NAND, sonst aus einem Dump auf der SD)
// 2. falls komprimiert: entpacken (Luma patcht die entpackte Version)
// 3. alle Nintendo-Adressen suchen und durch die MBMNET-Adresse ersetzen
// 4. als IPS-Patch nach /luma/titles/<TID>/code.ips schreiben
//
// Am NAND wird nie etwas verändert. Luma3DS wendet die Patches nur beim Laden an.
#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <dirent.h>
#include "blz.h"
#include "patcher.h"
#include "lumaconfig.h"
#include "olvpatch.h"
#include "nnid.h"
#define SYSMODULE_DIR "sdmc:/luma/sysmodules"
#define MAX_HITS 8
// Boot-Splash: Luma zeigt beim Einschalten "MBMNET aktiv" (Bild in splash_data.c)
#define SPLASH_FILE "sdmc:/luma/splash.bin"
#define SPLASH_BACKUP "sdmc:/luma/splash.bin.vor-mbmnet"
#define SPLASH_SIZE (400 * 240 * 3)
// Zwischenspeicher: fertige Patches und der Splash. Damit ist Umschalten nur noch Umbenennen und
// zwei kleine Dateien schreiben, statt die Module aus dem NAND zu lesen, zu entpacken und zu durchsuchen.
// Ein Patch gilt nur, solange Modul (Größe + Ende der .code), Server und Regeln gleich sind.
#define CACHE_DIR APP_DIR "/cache"
#define SPLASH_CACHE CACHE_DIR "/splash-" APP_VERSION ".bin"
#define RULES_VERSION "1"
extern const u8 splash_rle[];
extern const u32 splash_rle_size;
// Eine Ersetzungsregel. Es gibt zwei Arten:
// PREFIX: Text beginnt mit needle -> needle wird zu "http://<server>", der Rest bleibt
// ("https://nasc.nintendowifi.net/ac" -> "http://<server>/ac")
// EXACT: Text ist genau needle -> wird zu "http://<server>" + suffix
// (für Vorlagen mit Platzhaltern, deren Werte der 3DS dann ins Leere schreibt)
typedef enum { RULE_PREFIX, RULE_EXACT } RuleType;
typedef struct {
RuleType type;
const char *needle;
const char *suffix; // nur EXACT
} Rule;
#define MAX_RULES 2
typedef struct {
u64 tid;
const char *dir; // Luma-Ordner für dieses Modul
const char *label; // Name für Meldungen
Rule rules[MAX_RULES];
bool optional; // Anwendung statt Systemmodul: nur patchen, wenn installiert und Adresse gefunden
} Target;
#define TITLES_DIR "sdmc:/luma/titles"
// Umgelenkt wird die normale Nintendo-Umgebung: Alles, was der 3DS sonst zu
// nasc.nintendowifi.net und account.nintendo.net schickt, geht an MBMNET.
static const Target targets[] = {
{ 0x0004013000003202ULL, SYSMODULE_DIR, "Freunde", {
{ RULE_PREFIX, "https://nasc.nintendowifi.net" },
} },
{ 0x0004013000003802ULL, SYSMODULE_DIR, "Konto", {
// Aktuelle Firmware: die beiden %s sind Zusätze für Nintendos Test-Server (sonst leer),
// %u ist die API-Version (immer 1).
{ RULE_EXACT, "https://%s%saccount.nintendo.net/v%u/api/", "/v1/api/" },
{ RULE_PREFIX, "https://account.nintendo.net" },
} },
// 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
// installierte Region wird gepatcht. Luma lädt Patches für Anwendungen aus /luma/titles/<ID>/code.ips (wie Nimbus).
{ 0x000400300000BC02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true },
{ 0x000400300000BD02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true },
{ 0x000400300000BE02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true },
};
#define TARGET_COUNT (sizeof(targets) / sizeof(targets[0]))
typedef struct {
u32 offset;
u32 size; // Anzahl Bytes, die geschrieben werden
u8 data[160];
} Hit;
typedef struct {
Hit hits[MAX_HITS];
int count;
} Patch;
// --- Dateien ---------------------------------------------------------------------
static bool file_exists(const char *path)
{
struct stat st;
return stat(path, &st) == 0;
}
// Luma3DS lädt IPS-Patches für Systemmodule aus /luma/sysmodules/<Titel-ID>.ips
// (nur wenn "Enable loading external FIRMs and system modules" an ist).
// Daneben legen wir einen Merker <ID>.mbmnet und ggf. die Sicherung eines fremden Patches ab.
static void path_in(char *out, size_t len, const Target *t, const char *file)
{
// Anwendungen: /luma/titles/<ID>/code.ips (+ mbmnet.txt, code.ips.vor-mbmnet)
if (strcmp(t->dir, TITLES_DIR) == 0) {
snprintf(out, len, "%s/%016llX/%s", t->dir, t->tid, file);
return;
}
const char *ext = strcmp(file, "code.ips") == 0 ? ".ips"
: strcmp(file, "mbmnet.txt") == 0 ? ".mbmnet"
: ".ips.vor-mbmnet";
snprintf(out, len, "%s/%016llX%s", t->dir, t->tid, ext);
}
// Bis v2.2.0 lagen die Patches unter /luma/titles/<ID>/code.ips - dort wendet Luma sie für
// Systemmodule aber nicht an. Alte Dateien entfernen und gesicherte fremde Patches zurückholen.
bool patch_migrate(void)
{
bool migrated = false;
for (size_t i = 0; i < TARGET_COUNT; i++) {
if (targets[i].optional) continue; // Anwendungen gehören nach /luma/titles
char dir[64], ips[96], marker[96], backup[96];
snprintf(dir, sizeof(dir), "sdmc:/luma/titles/%016llX", targets[i].tid);
snprintf(ips, sizeof(ips), "%s/code.ips", dir);
snprintf(marker, sizeof(marker), "%s/mbmnet.txt", dir);
snprintf(backup, sizeof(backup), "%s/code.ips.vor-mbmnet", dir);
if (!file_exists(marker)) continue;
remove(ips);
remove(marker);
if (file_exists(backup)) rename(backup, ips);
migrated = true;
}
return migrated;
}
static void mkdirs(const char *path)
{
char tmp[256];
snprintf(tmp, sizeof(tmp), "%s", path);
for (char *p = tmp + strlen("sdmc:/"); *p; p++) {
if (*p == '/') {
*p = 0;
mkdir(tmp, 0777);
*p = '/';
}
}
mkdir(tmp, 0777);
}
PatchState patch_state(void)
{
char path[128];
bool foreign = false;
for (size_t i = 0; i < TARGET_COUNT; i++) {
path_in(path, sizeof(path), &targets[i], "mbmnet.txt");
if (file_exists(path)) return STATE_MBMNET;
path_in(path, sizeof(path), &targets[i], "code.ips");
if (!targets[i].optional && file_exists(path)) foreign = true;
}
return foreign ? STATE_FOREIGN : STATE_ORIGINAL;
}
// --- .code lesen ------------------------------------------------------------
static Result read_from_nand(u64 tid, u8 **out, u32 *outSize)
{
u32 archPath[4] = { (u32)tid, (u32)(tid >> 32), MEDIATYPE_NAND, 0 };
// ExeFS-Datei ".code"
static const u32 filePath[5] = { 0, 0, 2, 0x646F632E, 0x00000065 };
FS_Path arch = { PATH_BINARY, sizeof(archPath), archPath };
FS_Path file = { PATH_BINARY, sizeof(filePath), filePath };
Handle h;
Result rc = FSUSER_OpenFileDirectly(&h, ARCHIVE_SAVEDATA_AND_CONTENT, arch, file, FS_OPEN_READ, 0);
if (R_FAILED(rc)) return rc;
u64 size = 0;
rc = FSFILE_GetSize(h, &size);
if (R_SUCCEEDED(rc)) {
u8 *buf = malloc(size);
u32 read = 0;
if (!buf) rc = -1;
else rc = FSFILE_Read(h, &read, 0, buf, size);
if (R_SUCCEEDED(rc)) { *out = buf; *outSize = read; }
else free(buf);
}
FSFILE_Close(h);
return rc;
}
// Ersatzweise ein Dump von der SD: /3ds/mbmnet/<TID>.bin
static bool read_from_sd(u64 tid, u8 **out, u32 *outSize)
{
char path[64];
snprintf(path, sizeof(path), APP_DIR "/%016llX.bin", tid);
FILE *f = fopen(path, "rb");
if (!f) return false;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
u8 *buf = size > 0 ? malloc(size) : NULL;
bool ok = buf && fread(buf, 1, size, f) == (size_t)size;
fclose(f);
if (!ok) { free(buf); return false; }
*out = buf;
*outSize = size;
return true;
}
static bool load_code(const Target *t, u8 **out, u32 *outSize, char *msg, size_t msglen)
{
u8 *buf = NULL;
u32 size = 0;
Result rc = read_from_nand(t->tid, &buf, &size);
if (R_FAILED(rc) && t->optional) return false; // nicht installiert (andere Region)
if (R_FAILED(rc) && !read_from_sd(t->tid, &buf, &size)) {
snprintf(msg, msglen,
"%s-Modul nicht lesbar (Fehler %08lX).\n"
"Lege einen Dump als /3ds/mbmnet/%016llX.bin\nab (siehe Anleitung).",
t->label, (unsigned long)rc, t->tid);
return false;
}
if (blz_looks_compressed(buf, size)) {
u32 full = blz_decompressed_size(buf, size);
u8 *big = realloc(buf, full);
if (!big) { free(buf); snprintf(msg, msglen, "Zu wenig Speicher."); return false; }
buf = big;
if (!blz_decompress(buf, size)) {
free(buf);
snprintf(msg, msglen, "%s-Modul konnte nicht entpackt werden.", t->label);
return false;
}
size = full;
}
*out = buf;
*outSize = size;
return true;
}
// --- Zwischenspeicher ------------------------------------------------------------
// Fingerabdruck des Moduls ohne es ganz zu lesen: Größe und die letzten 16 Bytes der .code
// (bei komprimierten Modulen stehen dort die Entpack-Angaben). Leer, wenn nicht lesbar.
static void fingerprint(u64 tid, char *out, size_t len)
{
u32 archPath[4] = { (u32)tid, (u32)(tid >> 32), MEDIATYPE_NAND, 0 };
static const u32 filePath[5] = { 0, 0, 2, 0x646F632E, 0x00000065 };
out[0] = 0;
Handle h;
if (R_FAILED(FSUSER_OpenFileDirectly(&h, ARCHIVE_SAVEDATA_AND_CONTENT, (FS_Path){ PATH_BINARY, sizeof(archPath), archPath },
(FS_Path){ PATH_BINARY, sizeof(filePath), filePath }, FS_OPEN_READ, 0))) return;
u64 size = 0;
static u8 block[0x400];
u32 got = 0;
// Nur ab einer 0x200-Grenze lesen: Ein Lesen mitten im Block (Ende - 16) scheitert bei diesen Dateien
u64 start = size;
if (R_SUCCEEDED(FSFILE_GetSize(h, &size)) && size >= 16) {
start = (size - 16) & ~0x1FFULL;
if (R_SUCCEEDED(FSFILE_Read(h, &got, start, block, (u32)(size - start))) && got == size - start) {
const u8 *tail = block + got - 16;
int n = snprintf(out, len, "%llX-", size);
for (int i = 0; i < 16 && n + 2 < (int)len; i++) n += snprintf(out + n, len - n, "%02X", tail[i]);
}
}
FSFILE_Close(h);
}
static void cache_key(const Target *t, const char *server, char *out, size_t len)
{
char fp[64];
fingerprint(t->tid, fp, sizeof(fp));
if (fp[0]) snprintf(out, len, "%s|%s|" RULES_VERSION, fp, server);
else out[0] = 0;
}
static u32 cache_load(const Target *t, const char *key, u8 *data, u32 cap)
{
char path[96], stored[160] = "";
if (!key[0]) return 0;
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.key", t->tid);
FILE *f = fopen(path, "r");
if (!f) return 0;
if (!fgets(stored, sizeof(stored), f)) stored[0] = 0;
fclose(f);
stored[strcspn(stored, "\r\n")] = 0;
if (strcmp(stored, key)) return 0;
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.ips", t->tid);
f = fopen(path, "rb");
u32 n = f ? fread(data, 1, cap, f) : 0;
if (f) fclose(f);
return n > 8 && !memcmp(data, "PATCH", 5) && !memcmp(data + n - 3, "EOF", 3) ? n : 0;
}
static void cache_store(const Target *t, const char *key, const u8 *data, u32 n)
{
char path[96];
if (!key[0]) return;
mkdir(APP_DIR, 0777);
mkdir(CACHE_DIR, 0777);
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.ips", t->tid);
FILE *f = fopen(path, "wb");
bool ok = f && fwrite(data, 1, n, f) == n;
if (f) fclose(f);
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.key", t->tid);
f = ok ? fopen(path, "w") : NULL;
if (f) { fprintf(f, "%s\n", key); fclose(f); }
}
// --- Patch berechnen und schreiben ----------------------------------------------
static bool build_patch(const Target *t, const char *server, Patch *p, char *msg, size_t msglen)
{
u8 *code;
u32 size;
if (!load_code(t, &code, &size, msg, msglen)) return false;
p->count = 0;
for (u32 i = 0; i < size && p->count < MAX_HITS; i++) {
for (int r = 0; r < MAX_RULES && t->rules[r].needle; r++) {
const Rule *rule = &t->rules[r];
size_t needleLen = strlen(rule->needle);
if (i + needleLen >= size || memcmp(code + i, rule->needle, needleLen) != 0) continue;
u32 len = strnlen((const char *)code + i, size - i);
Hit *h = &p->hits[p->count];
memset(h, 0, sizeof(*h));
if (rule->type == RULE_EXACT && len != needleLen) continue; // Vorlage muss genau passen
{
char repl[128];
if (rule->type == RULE_EXACT)
snprintf(repl, sizeof(repl), "http://%s%s", server, rule->suffix);
else
snprintf(repl, sizeof(repl), "http://%s%.*s",
server, (int)(len - needleLen), (const char *)code + i + needleLen);
if (len + 1 > sizeof(h->data) || strlen(repl) > len) {
free(code);
snprintf(msg, msglen, "Server-Adresse passt nicht in das %s-Modul\n(zu lang).", t->label);
return false;
}
// Ersatz + Nullbytes bis zur Originallänge (inkl. Terminator)
h->offset = i;
h->size = len + 1;
memcpy(h->data, repl, strlen(repl));
}
p->count++;
i += len;
break;
}
}
free(code);
if (p->count == 0 && t->optional) return false;
if (p->count == 0) {
char diag[64];
patch_diagnose(diag, sizeof(diag));
snprintf(msg, msglen, "%s-Modul: Nintendo-Adresse nicht gefunden.\n"
"Diagnose gespeichert: /3ds/mbmnet/diagnose.txt\nBitte an den Entwickler schicken.", t->label);
return false;
}
return true;
}
// Baut den IPS-Patch im Speicher. Gibt die Länge zurück.
static u32 build_ips(const Patch *p, u8 *out, u32 cap)
{
u32 n = 0;
memcpy(out, "PATCH", 5);
n = 5;
for (int i = 0; i < p->count && n + 5 + p->hits[i].size + 3 <= cap; i++) {
const Hit *h = &p->hits[i];
u8 rec[5] = { h->offset >> 16, h->offset >> 8, h->offset, h->size >> 8, h->size };
memcpy(out + n, rec, 5);
memcpy(out + n + 5, h->data, h->size);
n += 5 + h->size;
}
memcpy(out + n, "EOF", 3);
return n + 3;
}
// Schreibt den Patch. *changed wird true, wenn sich die Datei dadurch geändert hat
// (nur dann muss der 3DS neu starten, denn Luma patcht beim Laden der Module).
#define IPS_CAP 2048 // Miiverse-Patch mit CA: ca. 1100 Bytes
static bool write_ips(const char *path, const u8 *data, u32 n, bool *changed)
{
static u8 old[IPS_CAP];
FILE *f = fopen(path, "rb");
size_t oldLen = f ? fread(old, 1, sizeof(old), f) : 0;
if (f) fclose(f);
if (oldLen == n && memcmp(old, data, n) == 0) return true; // schon genau so vorhanden
f = fopen(path, "wb");
bool ok = f && fwrite(data, 1, n, f) == n;
if (f) fclose(f);
if (ok) *changed = true;
return ok;
}
// Miiverse-Patch (olvpatch.c): eigene Code-Änderungen + MBMNET-CA statt einfacher Text-Regeln.
// 0 = nicht installiert, andere Version oder CA nicht vom Server zu holen (dann bleibt ein alter Patch stehen).
static u32 build_miiverse(const Target *t, const char *server, u8 *out, u32 cap)
{
static u8 der[1024];
static u32 derLen;
static int caState; // 0 = noch nicht geholt, 1 = da, -1 = nicht zu holen
char fp[64];
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 (caState < 0) return 0;
u32 sum = 2166136261u; // FNV-1a über die CA: neue CA -> neuer Patch
for (u32 i = 0; i < derLen; i++) sum = (sum ^ der[i]) * 16777619u;
char key[160];
if (fp[0]) snprintf(key, sizeof(key), "%s|%s|olv1|%08lX", fp, server, (unsigned long)sum);
else key[0] = 0;
u32 n = cache_load(t, key, out, cap);
if (n) return n;
u8 *code;
u32 size;
char ignore[8];
if (!load_code(t, &code, &size, ignore, sizeof(ignore))) return 0;
bool supported = olvpatch_supported(code, size);
free(code);
if (!supported) return 0;
n = olvpatch_build(server, der, derLen, out, cap);
if (n) cache_store(t, key, out, n);
return n;
}
// --- Fremde Patches, die MBMNET stören ---------------------------------------------
// Pretendos Patch für das http-Modul fragt bei jeder Verbindung den Freunde-Dienst nach dem
// aktiven Konto. Stellt der Freunde-Dienst selbst die Anfrage, warten beide aufeinander und
// keine Anfrage kommt bei MBMNET an. Er wird beim Aktivieren beiseitegelegt und beim
// Zurücksetzen zurückgeholt.
static const u64 conflicting[] = {
0x0004013000002902ULL, // http
};
static int park_conflicting(void)
{
int parked = 0;
for (size_t i = 0; i < sizeof(conflicting) / sizeof(conflicting[0]); i++) {
char ips[96], backup[112];
snprintf(ips, sizeof(ips), SYSMODULE_DIR "/%016llX.ips", conflicting[i]);
snprintf(backup, sizeof(backup), "%s.vor-mbmnet", ips);
// Liegt hier ein Patch, ist er fremd (MBMNET legt hier keinen an): immer den aktuellen sichern
if (file_exists(ips)) {
remove(backup);
if (rename(ips, backup) == 0) parked++;
}
}
return parked;
}
static void restore_conflicting(void)
{
for (size_t i = 0; i < sizeof(conflicting) / sizeof(conflicting[0]); i++) {
char ips[96], backup[112];
snprintf(ips, sizeof(ips), SYSMODULE_DIR "/%016llX.ips", conflicting[i]);
snprintf(backup, sizeof(backup), "%s.vor-mbmnet", ips);
if (file_exists(backup) && !file_exists(ips)) rename(backup, ips);
}
}
// --- Boot-Splash --------------------------------------------------------------------
static bool install_splash(bool alreadyOurs)
{
// Eigenen Splash des Nutzers sichern (nur beim ersten Aktivieren)
if (!alreadyOurs && file_exists(SPLASH_FILE) && !file_exists(SPLASH_BACKUP)) rename(SPLASH_FILE, SPLASH_BACKUP);
if (alreadyOurs && file_exists(SPLASH_FILE)) return true;
// Beim Deaktivieren geparkt: nur zurückschieben
if (!file_exists(SPLASH_FILE) && file_exists(SPLASH_CACHE) && rename(SPLASH_CACHE, SPLASH_FILE) == 0) return true;
u8 *buf = malloc(SPLASH_SIZE);
if (!buf) return false;
u32 out = 0;
for (u32 i = 0; i + 3 < splash_rle_size && out < SPLASH_SIZE; i += 4) {
for (u8 n = splash_rle[i]; n > 0 && out + 3 <= SPLASH_SIZE; n--) {
memcpy(buf + out, &splash_rle[i + 1], 3);
out += 3;
}
}
FILE *f = out == SPLASH_SIZE ? fopen(SPLASH_FILE, "wb") : NULL;
bool ok = f && fwrite(buf, 1, SPLASH_SIZE, f) == SPLASH_SIZE;
if (f) fclose(f);
free(buf);
return ok;
}
static void remove_splash(void)
{
// Parken statt löschen, damit das nächste Aktivieren ihn nicht neu schreiben muss
mkdir(APP_DIR, 0777);
mkdir(CACHE_DIR, 0777);
remove(SPLASH_CACHE);
if (rename(SPLASH_FILE, SPLASH_CACHE) != 0) remove(SPLASH_FILE);
if (file_exists(SPLASH_BACKUP)) rename(SPLASH_BACKUP, SPLASH_FILE);
}
// --- Aktivieren / Deaktivieren -----------------------------------------------
bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen)
{
*changed = false;
if (!server[0]) {
snprintf(msg, msglen, "Bitte zuerst die Server-Adresse eintragen.");
return false;
}
bool alreadyOurs = patch_state() == STATE_MBMNET;
// Erst alle Patches bereitlegen (aus dem Zwischenspeicher oder neu berechnet):
// Klappt eines nicht, wird gar nichts geschrieben.
static u8 ips[TARGET_COUNT][IPS_CAP];
u32 ipsLen[TARGET_COUNT];
for (size_t i = 0; i < TARGET_COUNT; i++) {
if (targets[i].optional) { // Miiverse
ipsLen[i] = build_miiverse(&targets[i], server, ips[i], IPS_CAP);
continue;
}
char key[160];
cache_key(&targets[i], server, key, sizeof(key));
ipsLen[i] = cache_load(&targets[i], key, ips[i], IPS_CAP);
if (ipsLen[i]) continue;
static Patch patch;
if (!build_patch(&targets[i], server, &patch, msg, msglen)) {
if (targets[i].optional) { ipsLen[i] = 0; continue; } // nicht installiert: auslassen
return false;
}
ipsLen[i] = build_ips(&patch, ips[i], IPS_CAP);
cache_store(&targets[i], key, ips[i], ipsLen[i]);
}
bool backedUp = false, appPatched = false;
for (size_t i = 0; i < TARGET_COUNT; i++) {
const Target *t = &targets[i];
if (!ipsLen[i]) continue;
char path[128], marker[128], backup[128], dir[96];
if (t->optional) {
snprintf(dir, sizeof(dir), "%s/%016llX", t->dir, t->tid);
mkdirs(dir);
appPatched = true;
}
path_in(path, sizeof(path), t, "code.ips");
path_in(marker, sizeof(marker), t, "mbmnet.txt");
path_in(backup, sizeof(backup), t, "code.ips.vor-mbmnet");
mkdirs(t->dir);
// Fremden Patch (z. B. Pretendo) sichern, nicht überschreiben. Immer den aktuellen: Eine ältere Sicherung
// (z. B. von vor einem Nimbus-Update) würde sonst beim Zurückschalten veraltete Patches herstellen
// (bei Pretendo Fehler 002-0122).
if (file_exists(path) && !file_exists(marker)) {
remove(backup);
rename(path, backup);
backedUp = true;
}
if (!write_ips(path, ips[i], ipsLen[i], changed)) {
snprintf(msg, msglen, "Patch konnte nicht auf die SD-Karte geschrieben werden.");
return false;
}
FILE *m = fopen(marker, "w");
if (m) {
fprintf(m, "MBMNET-Patch aktiv\nServer: %s\n", server);
fclose(m);
}
}
if (park_conflicting() > 0) *changed = true; // Neustart nötig, damit das http-Modul neu lädt
bool splash = install_splash(alreadyOurs) && lumaconfig_enable_splash();
lumaconfig_enable_modules();
if (appPatched && lumaconfig_enable_game_patching()) *changed = true;
snprintf(msg, msglen, "Patches geschrieben.%s%s",
backedUp ? "\nAlter Patch wurde gesichert." : "",
splash ? "\nLuma zeigt beim Start \"MBMNET aktiv\"." : "");
return true;
}
// --- Diagnose ---------------------------------------------------------------------
static bool has_ci(const char *s, u32 len, const char *word)
{
size_t wl = strlen(word);
for (u32 i = 0; i + wl <= len; i++) {
size_t k = 0;
while (k < wl && (s[i + k] | 0x20) == word[k]) k++;
if (k == wl) return true;
}
return false;
}
static bool interesting(const char *s, u32 len)
{
return has_ci(s, len, "nintendo") || has_ci(s, len, "http") || has_ci(s, len, "nasc")
|| has_ci(s, len, "account") || has_ci(s, len, "%s") || has_ci(s, len, ".net");
}
static void dump_strings(FILE *f, const u8 *code, u32 size)
{
int lines = 0, total = 0;
char buf[256];
// ASCII-Texte
for (u32 i = 0; i < size;) {
u32 j = i;
while (j < size && code[j] >= 0x20 && code[j] < 0x7F) j++;
u32 len = j - i;
if (len >= 6) total++;
if (len >= 4 && interesting((const char *)code + i, len) && lines < 400) {
fprintf(f, " %06lX %.*s\n", (unsigned long)i, (int)(len > 200 ? 200 : len), (const char *)code + i);
lines++;
}
i = j + 1;
}
// UTF-16-Texte (ein Zeichen + Nullbyte)
for (u32 i = 0; i + 1 < size;) {
u32 j = i, n = 0;
while (j + 1 < size && code[j] >= 0x20 && code[j] < 0x7F && code[j + 1] == 0 && n < sizeof(buf) - 1) {
buf[n++] = code[j];
j += 2;
}
if (n >= 6 && interesting(buf, n) && lines < 500) {
fprintf(f, " %06lX [UTF-16] %.*s\n", (unsigned long)i, (int)n, buf);
lines++;
}
i = (j > i) ? j + 2 : i + 1;
}
fprintf(f, " (%d Texte insgesamt, %d passende gezeigt)\n", total, lines);
}
// Sucht den laufenden Prozess eines Titels (Luma liefert die Titel-ID über Info-Typ 0x10001)
bool patch_find_process(u64 tid, u32 *pidOut)
{
static u32 pids[0x60];
s32 n = 0;
if (R_FAILED(svcGetProcessList(&n, pids, 0x60))) return false;
for (s32 i = 0; i < n; i++) {
Handle h;
if (R_FAILED(svcOpenProcess(&h, pids[i]))) continue;
s64 t = 0;
Result r = svcGetProcessInfo(&t, h, 0x10001);
svcCloseHandle(h);
if (R_SUCCEEDED(r) && (u64)t == tid) { *pidOut = pids[i]; return true; }
}
return false;
}
// Liest aus dem laufenden Modul, welche Adresse dort wirklich im Speicher steht.
// So sieht man, ob Luma den Patch beim Laden angewendet hat.
static void dump_runtime(FILE *f, const char *label, u64 tid, u32 offset)
{
u32 pid = 0;
if (!patch_find_process(tid, &pid)) { fprintf(f, " %s: Prozess nicht gefunden\n", label); return; }
Handle dbg;
Result r = svcDebugActiveProcess(&dbg, pid);
if (R_FAILED(r)) { fprintf(f, " %s: kein Zugriff (%08lX)\n", label, (unsigned long)r); return; }
DebugEventInfo info;
while (R_SUCCEEDED(svcGetProcessDebugEvent(&info, dbg)))
if (info.flags & 1) svcContinueDebugEvent(dbg, 0);
char buf[64] = { 0 };
r = svcReadProcessMemory(buf, dbg, 0x100000 + offset, 48);
svcCloseHandle(dbg);
if (R_FAILED(r)) { fprintf(f, " %s: Lesen fehlgeschlagen (%08lX)\n", label, (unsigned long)r); return; }
for (int i = 0; i < 48 && buf[i]; i++) if (buf[i] < 0x20 || buf[i] > 0x7E) buf[i] = '.';
fprintf(f, " %s (PID %lu) bei %06lX: %s\n", label, (unsigned long)pid, (unsigned long)offset, buf);
}
bool patch_diagnose(char *msg, size_t msglen)
{
mkdirs(APP_DIR);
FILE *f = fopen(DIAG_FILE, "w");
if (!f) {
snprintf(msg, msglen, "Diagnose konnte nicht gespeichert werden.");
return false;
}
fprintf(f, "MBMNET-Diagnose (App v%s)\n\n", APP_VERSION);
for (size_t i = 0; i < TARGET_COUNT; i++) {
const Target *t = &targets[i];
u8 *raw = NULL;
u32 rawSize = 0;
Result rc = read_from_nand(t->tid, &raw, &rawSize);
bool fromSd = false;
if (R_FAILED(rc)) fromSd = read_from_sd(t->tid, &raw, &rawSize);
fprintf(f, "== %s-Modul %016llX\n", t->label, t->tid);
if (!raw) {
fprintf(f, " nicht lesbar (Fehler %08lX)\n\n", (unsigned long)rc);
continue;
}
bool compressed = blz_looks_compressed(raw, rawSize);
fprintf(f, " Quelle: %s, Größe %lu, komprimiert: %s\n", fromSd ? "SD-Dump" : "NAND",
(unsigned long)rawSize, compressed ? "ja" : "nein");
free(raw);
char err[160];
u8 *code;
u32 size;
if (!load_code(t, &code, &size, err, sizeof(err))) {
fprintf(f, " Fehler: %s\n\n", err);
continue;
}
fprintf(f, " entpackt: %lu Bytes\n", (unsigned long)size);
dump_strings(f, code, size);
fprintf(f, "\n");
free(code);
}
// Installierte Patches: liegen sie auf der SD, und was steht drin?
fprintf(f, "== Installierte Patches\n");
for (size_t i = 0; i < TARGET_COUNT; i++) {
char ips[128];
path_in(ips, sizeof(ips), &targets[i], "code.ips");
FILE *pf = fopen(ips, "rb");
if (!pf) { fprintf(f, " %s: kein Patch (%s)\n", targets[i].label, ips + 5); continue; }
static u8 buf[4096];
size_t n = fread(buf, 1, sizeof(buf), pf);
fclose(pf);
fprintf(f, " %s: %s, %lu Bytes\n", targets[i].label, ips + 5, (unsigned long)n);
for (size_t o = 5; o + 5 <= n && memcmp(buf + o, "EOF", 3) != 0;) {
u32 off = (buf[o] << 16) | (buf[o + 1] << 8) | buf[o + 2];
u32 len = (buf[o + 3] << 8) | buf[o + 4];
if (o + 5 + len > n) break;
fprintf(f, " bei %06lX: %.*s\n", (unsigned long)off, (int)strnlen((char *)buf + o + 5, len), (char *)buf + o + 5);
o += 5 + len;
}
}
fprintf(f, "\n");
// Alles in /luma/sysmodules (auch fremde Patches, z. B. von Pretendo)
fprintf(f, "== Inhalt von /luma/sysmodules\n");
DIR *dir = opendir(SYSMODULE_DIR);
if (dir) {
struct dirent *e;
while ((e = readdir(dir))) {
char full[300];
struct stat est;
snprintf(full, sizeof(full), "%s/%s", SYSMODULE_DIR, e->d_name);
if (stat(full, &est) == 0) fprintf(f, " %-40s %8ld Bytes\n", e->d_name, (long)est.st_size);
}
closedir(dir);
} else {
fprintf(f, " (Ordner fehlt)\n");
}
fprintf(f, "\n");
// Konto-Plätze des Kontodienstes (act)
fprintf(f, "== Konto-Plätze\n");
nnid_dump(f);
fprintf(f, "\n");
// Was steht im ARBEITSSPEICHER der laufenden Module? (zeigt, ob Luma die Patches angewendet hat)
fprintf(f, "== Laufende Module (Arbeitsspeicher)\n");
dump_runtime(f, "Freunde", 0x0004013000003202ULL, 0x61279);
dump_runtime(f, "Konto ", 0x0004013000003802ULL, 0xE564);
fprintf(f, "\n");
// Verbindungstest: so, wie die Systemmodule eine Verbindung aufbauen (mit Nintendo-Zertifikaten)
fprintf(f, "== Verbindungstest (wie Freunde-/Konto-Modul)\n");
httpcContext c;
Result r = httpcOpenContext(&c, HTTPC_METHOD_GET, "http://mbmnetapi.mbm.wtf:81/app/version", 1);
fprintf(f, " Verbindung öffnen (http) %08lX\n", (unsigned long)r);
if (R_SUCCEEDED(r)) {
fprintf(f, " Nintendo-Root-Zertifikat %08lX\n", (unsigned long)httpcAddDefaultCert(&c, SSLC_DefaultRootCert_Nintendo_CA_G3));
fprintf(f, " Client-Zertifikat %08lX\n", (unsigned long)httpcSetClientCertDefault(&c, SSLC_DefaultClientCert_ClCertA));
r = httpcBeginRequest(&c);
u32 status = 0;
if (R_SUCCEEDED(r)) httpcGetResponseStatusCodeTimeout(&c, &status, 10ULL * 1000 * 1000 * 1000);
fprintf(f, " Anfrage senden %08lX HTTP %lu\n", (unsigned long)r, (unsigned long)status);
httpcCloseContext(&c);
}
fprintf(f, "\n");
// Boot-Splash und Luma-Einstellungen
fprintf(f, "== Boot-Splash / Luma-Einstellungen\n");
struct stat st;
if (stat(SPLASH_FILE, &st) == 0)
fprintf(f, " /luma/splash.bin: %ld Bytes%s\n", (long)st.st_size, st.st_size == SPLASH_SIZE ? " (richtig)" : " (FALSCHE GRÖSSE, Luma erwartet 288000)");
else
fprintf(f, " /luma/splash.bin: fehlt\n");
FILE *cfg = fopen("sdmc:/luma/config.ini", "r");
if (cfg) {
char line[160];
while (fgets(line, sizeof(line), cfg)) {
if (strstr(line, "splash_position") || strstr(line, "splash_duration") || strstr(line, "config_version")
|| strstr(line, "enable_external_firm_and_modules ="))
fprintf(f, " config.ini: %s", line);
}
fclose(cfg);
} else {
fprintf(f, " /luma/config.ini: fehlt (ältere Luma-Version? Splash dann im Luma-Menü einschalten)\n");
}
fprintf(f, "\n");
// Kontospeicher: Darf die App Systemspeicher öffnen? Scheitert nur der eines laufenden Dienstes
// (act, frd), ist er belegt; scheitern alle, fehlt eine Berechtigung.
fprintf(f, "== Systemspeicher öffnen (für \"Anmeldung merken\")\n");
static const struct { u32 id; const char *name; } saves[] = {
{ 0x00010038, "act (Kontodienst, läuft)" },
{ 0x00010032, "frd (Freunde-Dienst, läuft)" },
{ 0x00010017, "cfg (Systemeinstellungen-Modul, läuft)" },
{ 0x00020082, "HOME-Menü (System-App)" },
{ 0x00020086, "Systemeinstellungen (System-App, läuft nicht)" },
};
for (size_t i = 0; i < sizeof(saves) / sizeof(saves[0]); i++) {
u32 path[2] = { MEDIATYPE_NAND, saves[i].id };
FS_Archive arch;
Result r = FSUSER_OpenArchive(&arch, ARCHIVE_SYSTEM_SAVEDATA, (FS_Path){ PATH_BINARY, sizeof(path), path });
int files = -1;
if (R_SUCCEEDED(r)) {
Handle dir;
if (R_SUCCEEDED(FSUSER_OpenDirectory(&dir, arch, fsMakePath(PATH_ASCII, "/")))) {
FS_DirectoryEntry e[16];
u32 n = 0;
files = R_SUCCEEDED(FSDIR_Read(dir, &n, 16, e)) ? (int)n : -2;
FSDIR_Close(dir);
}
FSUSER_CloseArchive(arch);
}
fprintf(f, " %08lX %-44s %08lX%s", (unsigned long)saves[i].id, saves[i].name, (unsigned long)r, R_SUCCEEDED(r) ? " OK" : "");
if (files >= 0) fprintf(f, ", %d Einträge", files);
fprintf(f, "\n");
}
fprintf(f, " Als %s gestartet\n", envIsHomebrew() ? "3dsx (Homebrew Launcher)" : "CIA");
fprintf(f, "\n");
fclose(f);
snprintf(msg, msglen, "Diagnose gespeichert:\n/3ds/mbmnet/diagnose.txt\nBitte an den Entwickler schicken.");
return true;
}
bool patch_disable(char *msg, size_t msglen)
{
PatchState st = patch_state();
if (st != STATE_MBMNET) {
snprintf(msg, msglen, st == STATE_FOREIGN
? "MBMNET ist nicht aktiv.\nDer vorhandene fremde Patch bleibt unverändert."
: "Schon im Originalzustand.");
return false;
}
remove_splash();
restore_conflicting();
lumaconfig_restore_splash();
lumaconfig_restore_modules();
lumaconfig_restore_game_patching();
bool restored = false;
for (size_t i = 0; i < TARGET_COUNT; i++) {
const Target *t = &targets[i];
char ips[128], marker[128], backup[128];
path_in(ips, sizeof(ips), t, "code.ips");
path_in(marker, sizeof(marker), t, "mbmnet.txt");
path_in(backup, sizeof(backup), t, "code.ips.vor-mbmnet");
if (!file_exists(marker)) continue;
remove(ips);
remove(marker);
if (file_exists(backup)) {
rename(backup, ips);
restored = true;
}
}
snprintf(msg, msglen, restored ? "Vorheriger Patch wiederhergestellt." : "Patches entfernt.");
return true;
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <3ds/types.h>
// Offizieller MBMNET-Server. Über "Server-Adresse" lässt sich ein anderer eintragen (z. B. zum Testen).
// Fester MBMNET-Server: einfaches HTTP über den NetBird-TCP-Service auf Port 81.
// Höchstens 21 Zeichen passen in den Patch.
#define MBMNET_SERVER "mbmnetapi.mbm.wtf:81"
#define APP_DIR "sdmc:/3ds/mbmnet"
#define DIAG_FILE APP_DIR "/diagnose.txt"
typedef enum {
STATE_ORIGINAL, // kein Patch: Nintendo-Originalzustand
STATE_MBMNET, // unser Patch ist aktiv
STATE_FOREIGN, // fremder Patch (z. B. Pretendo/Nimbus) ist aktiv
} PatchState;
PatchState patch_state(void);
// Räumt Patches alter App-Versionen (/luma/titles/...) weg. true, wenn etwas entfernt wurde.
bool patch_migrate(void);
// 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).
bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen);
// Schreibt alle Adress-Texte aus den Modulen nach DIAG_FILE (zur Fehlersuche).
bool patch_diagnose(char *msg, size_t msglen);
// Entfernt unsere Patches und stellt eventuell gesicherte fremde Patches wieder her.
bool patch_disable(char *msg, size_t msglen);
// Läuft ein Prozess mit dieser Titel-ID? (über Lumas Prozess-Info), PID in pidOut
bool patch_find_process(u64 tid, u32 *pidOut);
+48
View File
@@ -0,0 +1,48 @@
// Startet die App nach einem Update selbst neu.
//
// CIA: aptSetChainloaderToSelf() - das System startet beim Beenden denselben Titel
// (jetzt die neue Version) wieder.
// 3DSX: Die App läuft im Homebrew-Titel von Luma (Rosalina). Über dessen Dienst "hb:ldr"
// legen wir fest, welche .3dsx beim nächsten Start geladen wird - genau so macht es
// auch der Homebrew Launcher. Danach den Homebrew-Titel neu starten lassen.
#include <3ds.h>
#include <string.h>
#include "relaunch.h"
static Result hbldr_call(Handle h, u32 cmd, const void *buf, u32 size, int staticId)
{
u32 *cmdbuf = getThreadCommandBuffer();
cmdbuf[0] = IPC_MakeHeader(cmd, 0, 2);
cmdbuf[1] = IPC_Desc_StaticBuffer(size, staticId);
cmdbuf[2] = (u32)buf;
Result rc = svcSendSyncRequest(h);
return R_SUCCEEDED(rc) ? (Result)cmdbuf[1] : rc;
}
static bool set_homebrew_target(const char *selfPath)
{
Handle h;
if (R_FAILED(svcConnectToPort(&h, "hb:ldr"))) return false;
// hb:ldr erwartet den Pfad ohne "sdmc:"
const char *path = strncmp(selfPath, "sdmc:/", 6) == 0 ? selfPath + 5 : selfPath;
Result rc = hbldr_call(h, 2, path, strlen(path) + 1, 0); // SetTarget
// argv wie beim Homebrew Launcher: u32 Anzahl, dann die Texte
static u32 argBuf[0x400 / 4];
memset(argBuf, 0, sizeof(argBuf));
argBuf[0] = 1;
strncpy((char *)&argBuf[1], selfPath, sizeof(argBuf) - 5);
if (R_SUCCEEDED(rc)) rc = hbldr_call(h, 3, argBuf, sizeof(argBuf), 1); // SetArgv
svcCloseHandle(h);
return R_SUCCEEDED(rc);
}
bool relaunch_prepare(const char *selfPath)
{
if (envIsHomebrew() && !set_homebrew_target(selfPath)) return false;
aptSetChainloaderToSelf();
return true;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <3ds/types.h>
// Bereitet den Neustart der App vor. Danach main() verlassen, dann startet das System
// die (neue) App wieder. false: Neustart nicht möglich, Nutzer muss selbst neu starten.
bool relaunch_prepare(const char *selfPath);
+1985
View File
File diff suppressed because it is too large Load Diff
+249
View File
@@ -0,0 +1,249 @@
// SpotPass (BOSS) für MBMNET-Mitteilungen, siehe spotpass.h.
//
// Wie in der libctru-Vorlage (examples/network/boss): eigene Aufgabe mit HTTP-URL. Der Server liefert einen
// BOSS-Container, dessen Inhalt an das News-Modul (0004013000003502) geht. Die Nintendo-Signatur darin ist
// leer, Luma schaltet ihre Prüfung ab.
#include <3ds.h>
#include <stdio.h>
#include <string.h>
#include "spotpass.h"
#define TASK_NEWS "mbmnews"
#define TASK_TEST "mbmtest"
#define NEWS_INTERVAL (2 * 60) // Sekunden zwischen zwei Abrufen
// 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.
#define EXTDATA_ID 0xFB4E0ULL // = Unique ID der App (cia.rsf: ExtSaveDataId)
#define BOSS_STORAGE 0x100000 // 1 MB
// Policy-Liste: SpotPass lädt sie vor jeder Aufgabe von Nintendo (nppl.c.app.nintendowifi.net). Der Server
// antwortet nicht mehr, ohne Liste bricht jede Aufgabe ab. Darum auf MBMNET umstellen (wie Pretendo).
// boss:P-Befehl 0x0427 SetPolicyListUrl, leerer Text = wieder Nintendos Adresse. Klappt nur mit boss:P.
static Result set_policy_url(const char *url)
{
u32 size = strlen(url) + 1;
u32 *cmdbuf = getThreadCommandBuffer();
cmdbuf[0] = IPC_MakeHeader(0x427, 1, 2); // 0x04270042
cmdbuf[1] = size;
cmdbuf[2] = IPC_Desc_Buffer(size, IPC_BUFFER_R);
cmdbuf[3] = (u32)url;
Result rc = svcSendSyncRequest(bossGetSessionHandle());
return R_SUCCEEDED(rc) ? (Result)cmdbuf[1] : rc;
}
static Result use_mbmnet_policy(const char *server)
{
char url[128];
snprintf(url, sizeof(url), "http://%s/p01/policylist", server);
return set_policy_url(url);
}
// Gibt es die Extdata? Sonst anlegen (mit Ordner /boss, in den SpotPass schreibt). bossSetStorageInfo
// prüft das nicht - ohne Extdata scheitert erst die Aufgabe (Fehler 004-6006).
static Result ensure_extdata(void)
{
u32 lowHigh[3] = { MEDIATYPE_SD, (u32)EXTDATA_ID, (u32)(EXTDATA_ID >> 32) };
FS_Path path = { PATH_BINARY, sizeof(lowHigh), lowHigh };
FS_Archive arch;
Result rc = fsInit();
if (R_FAILED(rc)) return rc;
rc = FSUSER_OpenArchive(&arch, ARCHIVE_EXTDATA, path);
if (R_FAILED(rc)) {
FS_ExtSaveDataInfo info = { .mediaType = MEDIATYPE_SD, .saveId = EXTDATA_ID };
rc = FSUSER_CreateExtSaveData(info, 32, 256, 0, 0, NULL);
if (R_FAILED(rc)) {
// Manche Systemversionen wollen ein Icon (SMDH) dazu: leeres mit Kennung reicht
static u8 smdh[0x36C0];
memcpy(smdh, "SMDH", 4);
rc = FSUSER_CreateExtSaveData(info, 32, 256, 0, sizeof(smdh), smdh);
}
if (R_SUCCEEDED(rc)) rc = FSUSER_OpenArchive(&arch, ARCHIVE_EXTDATA, path);
}
if (R_SUCCEEDED(rc)) {
FSUSER_CreateDirectory(arch, fsMakePath(PATH_ASCII, "/boss"), 0); // gibt es ihn schon: egal
FSUSER_CloseArchive(arch);
}
fsExit();
return rc;
}
static Result ensure_storage(void)
{
return bossSetStorageInfo(EXTDATA_ID, BOSS_STORAGE, MEDIATYPE_SD);
}
static bool url_for(const char *server, const char *file, char *url, size_t len)
{
u32 device = 0;
bool ok = R_SUCCEEDED(amInit());
if (ok) {
s32 internal = 0;
ok = R_SUCCEEDED(AM_GetDeviceId(&internal, &device)) && internal >= 0 && device;
amExit();
}
if (ok) snprintf(url, len, "http://%s/spotpass/%lu/%s", server, (unsigned long)device, file);
return ok;
}
// step bekommt den Namen des Schritts, der fehlschlug
static Result setup_task(const char *server, const char *task, const char *url, u32 interval, const char **step)
{
bool once = strcmp(task, TASK_TEST) == 0;
*step = "Policy-Liste umstellen";
Result rc = use_mbmnet_policy(server);
if (R_FAILED(rc)) return rc;
*step = "Speicher anlegen";
rc = ensure_extdata();
if (R_FAILED(rc)) return rc;
*step = "Speicher anmelden";
rc = ensure_storage();
if (R_FAILED(rc)) return rc;
bossDeleteTask(task, 0);
bossContext ctx;
bossSetupContextDefault(&ctx, interval, url);
ctx.property[0x4] = once ? 1 : 0; // Count: 0 = dauerhaft (Standard wären 100 Läufe), Test nur einmal
*step = "Einstellungen senden";
rc = bossSendContextConfig(&ctx);
if (R_FAILED(rc)) return rc;
*step = "Aufgabe anmelden";
return bossRegisterTask(task, 0, 0);
}
bool spotpass_register(const char *server, char *msg, size_t msglen)
{
char url[256];
if (!url_for(server, "news.boss", url, sizeof(url))) {
snprintf(msg, msglen, "SpotPass: Geräte-ID nicht lesbar");
return false;
}
const char *step = "BOSS starten";
Result rc = bossInit(0, false);
if (R_SUCCEEDED(rc)) {
rc = setup_task(server, TASK_NEWS, url, NEWS_INTERVAL, &step);
if (R_SUCCEEDED(rc)) bossStartBgImmediate(TASK_NEWS); // gleich einmal abholen
bossExit();
}
if (R_FAILED(rc)) snprintf(msg, msglen, "SpotPass anmelden fehlgeschlagen bei: %s (%08lX)", step, (unsigned long)rc);
return R_SUCCEEDED(rc);
}
void spotpass_unregister(void)
{
if (R_FAILED(bossInit(0, false))) return;
bossDeleteTask(TASK_NEWS, 0);
set_policy_url(""); // wieder Nintendos Policy-Liste
bossExit();
}
// --- Fehlerauswertung (BOSSU-Befehle, die libctru nicht hat) ---------------------------------------
static Result boss_send(u32 *cmdbuf)
{
Result rc = svcSendSyncRequest(bossGetSessionHandle());
return R_SUCCEEDED(rc) ? (Result)cmdbuf[1] : rc;
}
// Befehl mit Task-ID-Puffer; extra = Werte zwischen Größe und Puffer
static Result boss_task_cmd(u16 id, const char *task, int nextra, u32 e1, u32 e2, u32 *out)
{
u32 size = strlen(task) + 1;
u32 *cmd = getThreadCommandBuffer();
cmd[0] = IPC_MakeHeader(id, 1 + nextra, 2);
cmd[1] = size;
if (nextra > 0) cmd[2] = e1;
if (nextra > 1) cmd[3] = e2;
cmd[2 + nextra] = IPC_Desc_Buffer(size, IPC_BUFFER_R);
cmd[3 + nextra] = (u32)task;
Result rc = boss_send(cmd);
if (out) *out = cmd[2];
return rc;
}
static Result boss_receive_property(u16 property, void *buf, u32 size)
{
u32 *cmd = getThreadCommandBuffer();
cmd[0] = IPC_MakeHeader(0x16, 2, 2); // ReceiveProperty
cmd[1] = property;
cmd[2] = size;
cmd[3] = IPC_Desc_Buffer(size, IPC_BUFFER_W);
cmd[4] = (u32)buf;
return boss_send(cmd);
}
// Fehler einer Aufgabe als Text: Nintendo-Fehlercode 004-XXXX, Ergebnis, Dienststatus, HTTP-Code
static void task_error_text(const char *task, char *out, size_t len)
{
u8 result = 0, service = 0, taskErr = 0;
u32 http = 0, code = 0;
boss_task_cmd(0x23, task, 2, 0, 0, NULL); // GetTaskStatus: lädt die Status-Eigenschaften
boss_receive_property(0x20, &result, 1); // TaskResultCode
boss_receive_property(0x21, &service, 1); // TaskServiceStatus
boss_task_cmd(0x24, task, 1, 0, 0, NULL); // GetTaskError: lädt Eigenschaft 0x30
boss_receive_property(0x30, &taskErr, 1);
boss_task_cmd(0x22, task, 0, 0, 0, &http); // GetTaskCommErrorCode (HTTP-Status)
u32 *cmd = getThreadCommandBuffer();
cmd[0] = IPC_MakeHeader(0x2E, 1, 0); // GetErrorCode: Ergebnis -> 004-XXXX
cmd[1] = result;
if (R_SUCCEEDED(boss_send(cmd))) code = cmd[2];
snprintf(out, len, "Fehler %03lu-%04lu (Ergebnis 0x%02X, Dienst 0x%02X, Aufgabe 0x%02X, HTTP %lu)",
(unsigned long)(code / 10000), (unsigned long)(code % 10000), result, service, taskErr, (unsigned long)http);
}
static char testUrl[256];
static bool testSeenRunning;
bool spotpass_test_start(const char *server, char *msg, size_t msglen)
{
if (!url_for(server, "test.boss", testUrl, sizeof(testUrl))) {
snprintf(msg, msglen, "SpotPass-Test: Geräte-ID nicht lesbar");
return false;
}
Result rc = bossInit(0, false);
if (R_FAILED(rc)) {
snprintf(msg, msglen, "SpotPass-Test: BOSS nicht verfügbar (%08lX)", (unsigned long)rc);
return false;
}
const char *step = "";
rc = setup_task(server, TASK_TEST, testUrl, 60, &step);
if (R_SUCCEEDED(rc)) { step = "Aufgabe starten"; rc = bossStartTaskImmediate(TASK_TEST); }
bossExit();
if (R_FAILED(rc)) {
snprintf(msg, msglen, "SpotPass-Test fehlgeschlagen bei: %s (%08lX)", step, (unsigned long)rc);
return false;
}
testSeenRunning = false;
snprintf(msg, msglen, "SpotPass-Test gestartet ...");
return true;
}
bool spotpass_test_poll(int seconds, char *msg, size_t msglen)
{
if (R_FAILED(bossInit(0, false))) return true;
u8 status = 0, out2 = 0;
u32 out1 = 0;
Result rc = bossGetTaskState(TASK_TEST, 0, &status, &out1, &out2);
bool running = R_SUCCEEDED(rc) && status != BOSSTASKSTATUS_ERROR && !(testSeenRunning && status != BOSSTASKSTATUS_STARTED);
if (status == BOSSTASKSTATUS_STARTED) testSeenRunning = true;
if (running && seconds < 180) {
snprintf(msg, msglen, "SpotPass-Test läuft ... %d s (Status %u)", seconds, status);
bossExit();
return true;
}
if (R_FAILED(rc))
snprintf(msg, msglen, "SpotPass-Test: Status nicht lesbar (%08lX)", (unsigned long)rc);
else if (running)
snprintf(msg, msglen, "SpotPass-Test nach 3 Min. noch nicht fertig (Status %u).\n"
"Er läuft im Hintergrund weiter. Später in die Mitteilungen schauen.", status);
else if (status == BOSSTASKSTATUS_ERROR) {
char why[128];
task_error_text(TASK_TEST, why, sizeof(why));
snprintf(msg, msglen, "SpotPass-Test: Abruf fehlgeschlagen.\n%s\n%s", why, testUrl);
}
else
snprintf(msg, msglen, "SpotPass-Test abgeholt (Status %u, %lu, %u).\nSteht \"MBMNET SpotPass-Test\" in den Mitteilungen?",
status, (unsigned long)out1, out2);
if (!running) bossDeleteTask(TASK_TEST, 0); // nur löschen, wenn wirklich fertig
bossExit();
return false;
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <3ds/types.h>
#include <stddef.h>
// 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
// http://<server>/spotpass/<Geräte-ID>/news.boss ab (MBMNET Accounting, spotpass.js).
// Hintergrund-Aufgabe anmelden (bei aktivem MBMNET). false + msg bei Fehler.
bool spotpass_register(const char *server, char *msg, size_t msglen);
// Hintergrund-Aufgabe abmelden (beim Wechsel zu Nintendo)
void spotpass_unregister(void);
// Test für die Werkzeuge: startet den sofortigen Abruf einer Test-Mitteilung. true = läuft,
// danach jede Sekunde spotpass_test_poll aufrufen.
bool spotpass_test_start(const char *server, char *msg, size_t msglen);
// true = Test noch nicht fertig. msg bekommt den Stand bzw. das Ergebnis.
bool spotpass_test_poll(int seconds, char *msg, size_t msglen);
+277
View File
@@ -0,0 +1,277 @@
// Auto-Update. Die App fragt den MBMNET-Server (per HTTP, das kann der 3DS zuverlässig):
// GET /app/version -> z. B. "1.2.0"
// GET /app/mbmnet.3dsx -> die neue App (Homebrew-Launcher-Version)
// GET /app/mbmnet.cia -> die neue App (installierte CIA-Version)
// Der Server holt beides von deinem Git-Server (siehe config.json / README).
#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <sys/stat.h>
#include "update.h"
#define MAX_DOWNLOAD (8 * 1024 * 1024)
static volatile UpdateState state = UPD_IDLE;
static char server[64];
static char selfPath[256];
static char newVersion[32];
static Thread thread;
static bool runningAsCia;
// Fehlersuche: Schritt und Code des letzten Fehlers, dazu ein Protokoll auf der SD
#define LOG_FILE "sdmc:/3ds/mbmnet/update.log"
static char errStep[48];
static Result errCode;
static volatile int downloadAttempt; // laufender Versuch (für die Anzeige)
static void ulog(const char *fmt, ...)
{
FILE *f = fopen(LOG_FILE, "a");
if (!f) return;
va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
fputc('\n', f);
fclose(f);
}
// Merkt sich den Fehler. Den Zustand setzt erst der Worker, wenn alle Versuche erfolglos waren.
static void fail(const char *stepName, Result rc)
{
snprintf(errStep, sizeof(errStep), "%s", stepName);
errCode = rc;
ulog("FEHLER bei %s: %08lX", stepName, (unsigned long)rc);
}
#define ATTEMPTS 3
static void pause_seconds(int s) { svcSleepThread((s64)s * 1000 * 1000 * 1000); }
int update_attempt(void) { return downloadAttempt; }
void update_error(char *out, size_t len)
{
snprintf(out, len, "%s (Fehler %08lX)", errStep[0] ? errStep : "?", (unsigned long)errCode);
}
UpdateState update_state(void) { return state; }
const char *update_new_version(void) { return newVersion; }
// Lädt eine URL komplett in den Speicher. Gibt den HTTP-Status zurück (0 bei Netzwerkfehler).
static Result lastHttpRc; // Fehlercode, falls http_get 0 zurückgibt
static u32 http_get(const char *url, u8 **out, u32 *outSize)
{
httpcContext ctx;
u32 status = 0;
*out = NULL;
*outSize = 0;
lastHttpRc = httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1);
if (R_FAILED(lastHttpRc)) return 0;
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
httpcSetKeepAlive(&ctx, HTTPC_KEEPALIVE_DISABLED);
if (R_SUCCEEDED(lastHttpRc = httpcBeginRequest(&ctx)) &&
R_SUCCEEDED(lastHttpRc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 10ULL * 1000 * 1000 * 1000)) &&
status == 200) {
u32 cap = 64 * 1024, size = 0, got;
u8 *buf = malloc(cap);
Result rc;
do {
if (!buf) break;
if (size == cap) {
if (cap >= MAX_DOWNLOAD) { free(buf); buf = NULL; break; }
u8 *bigger = realloc(buf, cap * 2);
if (!bigger) { free(buf); buf = NULL; break; }
buf = bigger;
cap *= 2;
}
got = 0;
rc = httpcDownloadData(&ctx, buf + size, cap - size, &got);
size += got;
} while (rc == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING);
if (buf && R_SUCCEEDED(rc)) {
*out = buf;
*outSize = size;
} else {
lastHttpRc = buf ? rc : -2; // -2 = zu wenig Speicher
*outSize = size;
free(buf);
status = 0;
}
}
httpcCloseContext(&ctx);
return status;
}
// Installiert eine CIA über den Systemdienst am:net (wie FBI)
static bool install_cia(const u8 *data, u32 size)
{
Handle h;
Result rc = AM_StartCiaInstall(MEDIATYPE_SD, &h);
ulog("CIA-Installation starten: %08lX", (unsigned long)rc);
if (R_FAILED(rc)) { fail("CIA-Installation starten", rc); return false; }
for (u32 off = 0; off < size;) {
u32 chunk = size - off > 0x10000 ? 0x10000 : size - off;
u32 written = 0;
rc = FSFILE_Write(h, &written, off, data + off, chunk, 0);
if (R_FAILED(rc) || written != chunk) {
ulog("Schreiben bei %lu: %08lX (%lu von %lu Bytes)", (unsigned long)off, (unsigned long)rc,
(unsigned long)written, (unsigned long)chunk);
AM_CancelCIAInstall(h);
fail("CIA schreiben", rc);
return false;
}
off += chunk;
}
rc = AM_FinishCiaInstall(h);
ulog("CIA-Installation abschließen: %08lX", (unsigned long)rc);
if (R_FAILED(rc)) { fail("CIA-Installation abschließen", rc); return false; }
return true;
}
// Schreibt die neue .3dsx über die laufende
static bool install_3dsx(const u8 *data, u32 size)
{
char tmpPath[sizeof(selfPath) + 8];
snprintf(tmpPath, sizeof(tmpPath), "%s.neu", selfPath);
FILE *f = fopen(tmpPath, "wb");
bool ok = f && fwrite(data, 1, size, f) == size;
if (f) fclose(f);
ulog("3DSX schreiben nach %s: %s", tmpPath, ok ? "OK" : "FEHLER");
if (!ok) { fail("3DSX schreiben", -1); return false; }
// Erst komplett schreiben, dann austauschen: so bleibt bei einem Abbruch die alte App heil.
remove(selfPath);
ok = rename(tmpPath, selfPath) == 0;
ulog("3DSX austauschen (%s): %s", selfPath, ok ? "OK" : "FEHLER");
if (!ok) fail("3DSX austauschen", -1);
return ok;
}
static bool is_newer(const char *remote, const char *local)
{
int r[3] = { 0 }, l[3] = { 0 };
sscanf(remote, "%d.%d.%d", &r[0], &r[1], &r[2]);
sscanf(local, "%d.%d.%d", &l[0], &l[1], &l[2]);
for (int i = 0; i < 3; i++) {
if (r[i] != l[i]) return r[i] > l[i];
}
return false;
}
// Schritt 1: gibt es eine neuere Version? (bei Netzwerkfehlern bis zu 3 Versuche)
static void check_worker(void *arg)
{
char url[128];
u8 *data;
u32 size;
snprintf(url, sizeof(url), "http://%s/app/version", server);
for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
u32 status = http_get(url, &data, &size);
if (status == 404) { state = UPD_NONE; return; }
if (status == 200) {
snprintf(newVersion, sizeof(newVersion), "%.*s", (int)(size < 31 ? size : 31), (char *)data);
newVersion[strcspn(newVersion, " \r\n")] = 0;
if (newVersion[0] == 'v') memmove(newVersion, newVersion + 1, strlen(newVersion));
free(data);
state = is_newer(newVersion, APP_VERSION) ? UPD_AVAILABLE : UPD_LATEST;
return;
}
free(data);
if (attempt < ATTEMPTS) pause_seconds(2);
}
state = UPD_ERROR;
}
// Ein Versuch: laden, prüfen, installieren
static bool download_and_install(const char *url)
{
u8 *data;
u32 size;
u32 status = http_get(url, &data, &size);
ulog("Download: HTTP %lu, %lu Bytes", (unsigned long)status, (unsigned long)size);
if (status != 200) {
free(data);
fail(status ? "Herunterladen (HTTP-Status)" : "Herunterladen", status ? (Result)status : lastHttpRc);
return false;
}
// Nur echte Dateien installieren: CIA beginnt mit Headergröße 0x2020, 3DSX mit "3DSX"
bool valid = size >= 32 && (runningAsCia
? data[0] == 0x20 && data[1] == 0x20 && data[2] == 0 && data[3] == 0
: memcmp(data, "3DSX", 4) == 0);
if (!valid) { free(data); fail("Datei prüfen", -1); return false; }
bool ok = runningAsCia ? install_cia(data, size) : install_3dsx(data, size);
free(data);
return ok;
}
// Schritt 2: neue Version laden und installieren (bei Fehlern bis zu 3 Versuche)
static void download_worker(void *arg)
{
char url[128];
mkdir("sdmc:/3ds/mbmnet", 0777);
remove(LOG_FILE);
errStep[0] = 0;
snprintf(url, sizeof(url), "http://%s/app/%s", server, runningAsCia ? "mbmnet.cia" : "mbmnet.3dsx");
ulog("MBMNET-Update v%s -> v%s (%s)", APP_VERSION, newVersion, runningAsCia ? "CIA" : "3DSX");
ulog("Lade %s", url);
for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
if (attempt > 1) ulog("--- Versuch %d von %d", attempt, ATTEMPTS);
downloadAttempt = attempt;
if (download_and_install(url)) {
ulog("Ergebnis: OK");
state = UPD_INSTALLED;
return;
}
if (attempt < ATTEMPTS) pause_seconds(3);
}
ulog("Ergebnis: FEHLER nach %d Versuchen", ATTEMPTS);
state = UPD_ERROR;
}
static void start_thread(ThreadFunc fn)
{
if (thread) { threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; }
s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
thread = threadCreate(fn, NULL, 32 * 1024, prio + 1, -2, false);
if (!thread) state = UPD_ERROR;
}
void update_start(const char *srv, const char *self)
{
if (!srv[0] || state == UPD_CHECKING || state == UPD_DOWNLOADING) return;
snprintf(server, sizeof(server), "%s", srv);
snprintf(selfPath, sizeof(selfPath), "%s", self);
runningAsCia = !envIsHomebrew();
state = UPD_CHECKING;
start_thread(check_worker);
}
void update_download(void)
{
if (state != UPD_AVAILABLE) return;
state = UPD_DOWNLOADING;
start_thread(download_worker);
}
bool update_pending(void)
{
return state == UPD_AVAILABLE || state == UPD_DOWNLOADING || state == UPD_INSTALLED;
}
void update_finish(void)
{
if (!thread) return;
// Läuft der Download noch, nicht freigeben: Die App beendet sich ohnehin.
if (R_SUCCEEDED(threadJoin(thread, 3ULL * 1000 * 1000 * 1000))) threadFree(thread);
thread = NULL;
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <3ds/types.h>
#include <stddef.h>
typedef enum {
UPD_IDLE, // noch nicht geprüft
UPD_CHECKING, // fragt den Server
UPD_LATEST, // schon die neueste Version
UPD_AVAILABLE, // neue Version gefunden, wartet auf update_download()
UPD_DOWNLOADING, // lädt neue Version
UPD_INSTALLED, // neue Version liegt auf der SD, App neu starten
UPD_NONE, // Server bietet keine Updates an
UPD_ERROR, // Server nicht erreichbar o. Ä.
} UpdateState;
// Startet die Prüfung im Hintergrund. selfPath = Pfad der laufenden .3dsx.
void update_start(const char *server, const char *selfPath);
// Lädt und installiert die gefundene neue Version (nur im Zustand UPD_AVAILABLE).
void update_download(void);
// true, solange ein Update ansteht, lädt oder auf den Neustart wartet
bool update_pending(void);
UpdateState update_state(void);
const char *update_new_version(void);
void update_finish(void);
// Laufender Download-Versuch (1-3)
int update_attempt(void);
// Beschreibung des letzten Fehlers (Schritt und Code)
void update_error(char *out, size_t len);