MBMNET-App v2.8.2: Quellcode (GPLv3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
@@ -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 ..."; }
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(¤t, 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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user