v2.9.0: MBMNET Shop (eigene App, auf Wunsch installierbar)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-10-18 17:46:42 +02:00
co-authored by Claude Opus 5.5
parent d264cd93cd
commit ca77952a9d
16 changed files with 1710 additions and 19 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ SOURCES := source
INCLUDES := source INCLUDES := source
# Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer # Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer
VERSION := 2.8.2 VERSION := 2.9.0
APP_TITLE := MBMNET APP_TITLE := MBMNET
APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk (v$(VERSION)) APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk (v$(VERSION))
@@ -35,7 +35,7 @@ TITLE_MAJOR := $(shell echo $$(( $(word 1,$(VERSION_PARTS)) * 10 + $(word 2
VERSION_NUM := $(shell echo $$(( ($(TITLE_MAJOR) << 10) | $(word 3,$(VERSION_PARTS)) ))) VERSION_NUM := $(shell echo $$(( ($(TITLE_MAJOR) << 10) | $(word 3,$(VERSION_PARTS)) )))
ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft
CFLAGS := -g -Wall -O2 -mword-relocations -ffunction-sections $(ARCH) $(INCLUDE) -D__3DS__ -DAPP_VERSION=\"$(VERSION)\" CFLAGS := -g -Wall -O2 -mword-relocations -ffunction-sections $(ARCH) $(INCLUDE) -D__3DS__ -DAPP_VERSION=\"$(VERSION)\" -DAPP_TITLE_VERSION=$(VERSION_NUM)
ASFLAGS := -g $(ARCH) ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lcitro2d -lcitro3d -lctru -lm LIBS := -lcitro2d -lcitro3d -lctru -lm
+3 -1
View File
@@ -14,7 +14,7 @@ docker build -t mbmnet-3ds-build tools
docker run --rm -v "$PWD":/app -w /app mbmnet-3ds-build bash -c 'make && make cia' docker run --rm -v "$PWD":/app -w /app mbmnet-3ds-build bash -c 'make && make cia'
``` ```
Ergebnis: `mbmnet.3dsx` und `mbmnet.cia`. Version steht in `Makefile` (`VERSION`). Ergebnis: `mbmnet.3dsx` und `mbmnet.cia`. Den Shop baut `make -C shop && make -C shop cia`. Version steht in `Makefile` (`VERSION`).
`release.sh` baut, legt die Dateien ins Repo `MBMNET` und erstellt dort ein Gitea-Release. `release.sh` baut, legt die Dateien ins Repo `MBMNET` und erstellt dort ein Gitea-Release.
@@ -32,6 +32,8 @@ Ergebnis: `mbmnet.3dsx` und `mbmnet.cia`. Version steht in `Makefile` (`VERSION`
| `source/update.c` | Selbst-Update aus dem Repo `MBMNET` | | `source/update.c` | Selbst-Update aus dem Repo `MBMNET` |
| `source/admin.c` | Entwickler-Werkzeuge nur für Admins | | `source/admin.c` | Entwickler-Werkzeuge nur für Admins |
| `ftpmodul/` | Kleiner FTP-Server für die Entwickler-Werkzeuge | | `ftpmodul/` | Kleiner FTP-Server für die Entwickler-Werkzeuge |
| `source/shopctl.c` | MBMNET Shop installieren, öffnen, entfernen |
| `shop/` | **MBMNET Shop**: eigene App mit Homebrew aus der Universal-DB (Katalog und Downloads über den MBMNET-Server) |
## Lizenz ## Lizenz
+7 -4
View File
@@ -2,6 +2,7 @@
# Baut die App und lädt sie ins Repo "MBMNET" hoch. Danach aktualisiert sich jede # Baut die App und lädt sie ins Repo "MBMNET" hoch. Danach aktualisiert sich jede
# MBMNET-App beim nächsten Start von selbst. # MBMNET-App beim nächsten Start von selbst.
# Zusätzlich legt es auf Gitea ein Release "vX.Y.Z" mit .cia und .3dsx an (für Universal-Updater). # Zusätzlich legt es auf Gitea ein Release "vX.Y.Z" mit .cia und .3dsx an (für Universal-Updater).
# Der MBMNET Shop (shop/) wird mitgebaut; die App installiert ihn auf Wunsch vom Server.
# #
# Vorher in Makefile die VERSION erhöhen! # Vorher in Makefile die VERSION erhöhen!
# ./release.sh ["Release-Notizen"] bauen, hochladen, Release anlegen # ./release.sh ["Release-Notizen"] bauen, hochladen, Release anlegen
@@ -23,12 +24,13 @@ if [ "$NUR_RELEASE" = 0 ]; then
docker image inspect mbmnet-3ds-build >/dev/null 2>&1 || docker build -t mbmnet-3ds-build tools docker image inspect mbmnet-3ds-build >/dev/null 2>&1 || docker build -t mbmnet-3ds-build tools
echo "Baue MBMNET-App v$VERSION ..." echo "Baue MBMNET-App v$VERSION ..."
docker run --rm -v "$PWD":/app -w /app mbmnet-3ds-build bash -c 'make clean >/dev/null && make && make cia' docker run --rm -v "$PWD":/app -w /app mbmnet-3ds-build bash -c \
'make clean >/dev/null && make && make cia && make -C shop clean >/dev/null && make -C shop && make -C shop cia'
cp mbmnet.3dsx mbmnet.cia "$RELEASE/" cp mbmnet.3dsx mbmnet.cia shop/mbmnet-shop.3dsx shop/mbmnet-shop.cia "$RELEASE/"
echo "$VERSION" > "$RELEASE/version.txt" echo "$VERSION" > "$RELEASE/version.txt"
(cd "$RELEASE" && git add mbmnet.3dsx mbmnet.cia version.txt && git commit -m "MBMNET-App v$VERSION" && git push) (cd "$RELEASE" && git add mbmnet.3dsx mbmnet.cia mbmnet-shop.3dsx mbmnet-shop.cia version.txt && git commit -m "MBMNET-App v$VERSION" && git push)
fi fi
# --- Gitea-Release ------------------------------------------------------------------- # --- Gitea-Release -------------------------------------------------------------------
@@ -55,7 +57,8 @@ else
fi fi
# Vorhandene Dateien gleichen Namens löschen (bei erneutem Lauf), dann hochladen # Vorhandene Dateien gleichen Namens löschen (bei erneutem Lauf), dann hochladen
for f in mbmnet.cia mbmnet.3dsx; do for f in mbmnet.cia mbmnet.3dsx mbmnet-shop.cia mbmnet-shop.3dsx; do
[ -f "$RELEASE/$f" ] || continue
for old in $(api GET "/releases/$ID/assets" | json "' '.join(str(a['id']) for a in d if a['name']=='$f')"); do for old in $(api GET "/releases/$ID/assets" | json "' '.join(str(a['id']) for a in d if a['name']=='$f')"); do
api DELETE "/releases/$ID/assets/$old" >/dev/null api DELETE "/releases/$ID/assets/$old" >/dev/null
done done
+90
View File
@@ -0,0 +1,90 @@
#---------------------------------------------------------------------------------
# MBMNET Shop - Homebrew-Shop für den 3DS (.3dsx / .cia)
# Wird von der MBMNET-App auf Wunsch installiert. Version = Version der MBMNET-App.
#---------------------------------------------------------------------------------
.SUFFIXES:
ifeq ($(strip $(DEVKITARM)),)
$(error "DEVKITARM ist nicht gesetzt. Bitte devkitPro installieren.")
endif
TOPDIR ?= $(CURDIR)
include $(DEVKITARM)/3ds_rules
TARGET := mbmnet-shop
BUILD := build
SOURCES := source
INCLUDES := source
# Gleiche Version wie die MBMNET-App (die vergleicht damit, ob der Shop aktuell ist)
VERSION := $(shell sed -n 's/^VERSION *:= *//p' $(TOPDIR)/../Makefile)
APP_TITLE := MBMNET Shop
APP_DESCRIPTION := Homebrew für deinen 3DS
APP_AUTHOR := MBMNET
ICON := icon.png
# CIA: feste Titel-ID 000400000FB4E100
APP_PRODUCT_CODE := CTR-H-MBMS
APP_UNIQUE_ID := 0xFB4E1
VERSION_PARTS := $(subst ., ,$(VERSION))
# Titelversion wie bei der MBMNET-App: 2.9.0 -> 29.0.0
TITLE_MAJOR := $(shell echo $$(( $(word 1,$(VERSION_PARTS)) * 10 + $(word 2,$(VERSION_PARTS)) )))
VERSION_NUM := $(shell echo $$(( ($(TITLE_MAJOR) << 10) | $(word 3,$(VERSION_PARTS)) )))
ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft
CFLAGS := -g -Wall -O2 -mword-relocations -ffunction-sections $(ARCH) $(INCLUDE) -D__3DS__ -DAPP_VERSION=\"$(VERSION)\"
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lcitro2d -lcitro3d -lctru -lm
LIBDIRS := $(CTRULIB) $(PORTLIBS)
ifneq ($(BUILD),$(notdir $(CURDIR)))
export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
export LD := $(CC)
export OFILES := $(CFILES:.c=.o)
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
export APP_ICON := $(TOPDIR)/$(ICON)
export _3DSXDEPS := $(OUTPUT).smdh
export _3DSXFLAGS += --smdh=$(OUTPUT).smdh
.PHONY: all clean cia $(BUILD)
all: $(BUILD)
cia: $(BUILD)
@bannertool makebanner -i assets/banner.png -a assets/banner.wav -o $(BUILD)/banner.bnr
@bannertool makesmdh -s "$(APP_TITLE)" -l "$(APP_DESCRIPTION)" -p "$(APP_AUTHOR)" -i $(ICON) -o $(BUILD)/icon.icn
@makerom -f cia -o $(TARGET).cia -elf $(TARGET).elf -rsf cia.rsf -target t -exefslogo \
-icon $(BUILD)/icon.icn -banner $(BUILD)/banner.bnr \
-DAPP_TITLE="$(APP_TITLE)" -DAPP_PRODUCT_CODE="$(APP_PRODUCT_CODE)" -DAPP_UNIQUE_ID="$(APP_UNIQUE_ID)" \
-ver $(VERSION_NUM)
@echo built ... $(TARGET).cia
$(BUILD):
@mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).3dsx $(TARGET).smdh $(TARGET).elf $(TARGET).cia
else
DEPENDS := $(OFILES:.o=.d)
$(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS)
$(OUTPUT).elf : $(OFILES)
-include $(DEPENDS)
endif
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.
+268
View File
@@ -0,0 +1,268 @@
# Einstellungen für die CIA des MBMNET Shops (makerom).
# Werte mit $(...) setzt das Makefile.
BasicInfo:
Title : $(APP_TITLE)
ProductCode : $(APP_PRODUCT_CODE)
Logo : Homebrew
TitleInfo:
Category : Application
UniqueId : $(APP_UNIQUE_ID)
Option:
UseOnSD : true
FreeProductCode : true
MediaFootPadding : false
EnableCrypt : false
EnableCompress : true
AccessControlInfo:
CoreVersion : 2
DescVersion : 2
ReleaseKernelMajor : "02"
ReleaseKernelMinor : "33"
# Dateisystem: SD-Karte (Patches, Updates) und Systemtitel lesen (Module patchen)
FileSystemAccess:
- CategorySystemApplication
- CategoryHardwareCheck
- CategoryFileSystemTool
- Debug
- TwlCardBackup
- TwlNandData
- Boss
- DirectSdmc
- Core
- CtrNandRo
- CtrNandRw
- CtrNandRoWrite
- CategorySystemSettings
- CardBoard
- ExportImportIvs
- DirectSdmcWrite
- SwitchCleanup
- SaveDataMove
- Shop
- Shell
- CategoryHomeMenu
- SeedDB
IoAccessControl:
- FsMountNand
- FsMountNandRoWrite
- FsMountTwln
- FsMountWnand
- FsMountCardSpi
- UseSdif3
- CreateSeed
- UseCardSpi
MemoryType : Application
SystemMode : 64MB
IdealProcessor : 0
AffinityMask : 1
Priority : 16
MaxCpu : 0x9E
HandleTableSize : 0x200
DisableDebug : false
EnableForceDebug : false
CanWriteSharedPage : true
CanUsePrivilegedPriority : false
CanUseNonAlphabetAndNumber : true
PermitMainFunctionArgument : true
CanShareDeviceMemory : true
RunnableOnSleep : false
SpecialMemoryArrange : true
SystemModeExt : Legacy
CpuSpeed : 804MHz
EnableL2Cache : true
CanAccessCore2 : true
IORegisterMapping:
- 1ff00000-1ff7ffff
MemoryMapping:
- 1f000000-1f5fffff:r
SystemCallAccess:
ControlMemory: 1
QueryMemory: 2
ExitProcess: 3
GetProcessAffinityMask: 4
SetProcessAffinityMask: 5
GetProcessIdealProcessor: 6
SetProcessIdealProcessor: 7
CreateThread: 8
ExitThread: 9
SleepThread: 10
GetThreadPriority: 11
SetThreadPriority: 12
GetThreadAffinityMask: 13
SetThreadAffinityMask: 14
GetThreadIdealProcessor: 15
SetThreadIdealProcessor: 16
GetCurrentProcessorNumber: 17
Run: 18
CreateMutex: 19
ReleaseMutex: 20
CreateSemaphore: 21
ReleaseSemaphore: 22
CreateEvent: 23
SignalEvent: 24
ClearEvent: 25
CreateTimer: 26
SetTimer: 27
CancelTimer: 28
ClearTimer: 29
CreateMemoryBlock: 30
MapMemoryBlock: 31
UnmapMemoryBlock: 32
CreateAddressArbiter: 33
ArbitrateAddress: 34
CloseHandle: 35
WaitSynchronization1: 36
WaitSynchronizationN: 37
SignalAndWait: 38
DuplicateHandle: 39
GetSystemTick: 40
GetHandleInfo: 41
GetSystemInfo: 42
GetProcessInfo: 43
GetThreadInfo: 44
ConnectToPort: 45
SendSyncRequest1: 46
SendSyncRequest2: 47
SendSyncRequest3: 48
SendSyncRequest4: 49
SendSyncRequest: 50
OpenProcess: 51
OpenThread: 52
GetProcessId: 53
GetProcessIdOfThread: 54
GetThreadId: 55
GetResourceLimit: 56
GetResourceLimitLimitValues: 57
GetResourceLimitCurrentValues: 58
GetThreadContext: 59
Break: 60
OutputDebugString: 61
ControlPerformanceCounter: 62
CreatePort: 71
CreateSessionToPort: 72
CreateSession: 73
AcceptSession: 74
ReplyAndReceive1: 75
ReplyAndReceive2: 76
ReplyAndReceive3: 77
ReplyAndReceive4: 78
ReplyAndReceive: 79
BindInterrupt: 80
UnbindInterrupt: 81
InvalidateProcessDataCache: 82
StoreProcessDataCache: 83
FlushProcessDataCache: 84
StartInterProcessDma: 85
StopDma: 86
GetDmaState: 87
RestartDma: 88
DebugActiveProcess: 96
BreakDebugProcess: 97
TerminateDebugProcess: 98
GetProcessDebugEvent: 99
ContinueDebugEvent: 100
GetProcessList: 101
GetThreadList: 102
GetDebugThreadContext: 103
SetDebugThreadContext: 104
QueryDebugProcessMemory: 105
ReadProcessMemory: 106
WriteProcessMemory: 107
SetHardwareBreakPoint: 108
GetDebugThreadParam: 109
ControlProcessMemory: 112
MapProcessMemory: 113
UnmapProcessMemory: 114
CreateCodeSet: 115
CreateProcess: 117
TerminateProcess: 118
SetProcessResourceLimits: 119
CreateResourceLimit: 120
SetResourceLimitValues: 121
AddCodeSegment: 122
Backdoor: 123
KernelSetState: 124
QueryProcessMemory: 125
# Dienste: u. a. am:net (Apps installieren und entfernen), http:C (Katalog und Downloads)
ServiceAccessControl:
- APT:U
- ac:u
- act:a
- act:u
- am:net
- boss:U
- boss:P
- cecd:u
- cfg:nor
- cfg:u
- csnd:SND
- dsp::DSP
- frd:a
- frd:u
- fs:USER
- gsp::Gpu
- gsp::Lcd
- hid:USER
- http:C
- ir:rst
- ir:u
- ir:USER
- mic:u
- ndm:u
- news:s
- nwm::EXT
- nwm::UDS
- pm:app
- ptm:sysm
- ptm:u
- pxi:dev
- soc:U
- ssl:C
- y2r:u
SystemControlInfo:
SaveDataSize: 0KB
RemasterVersion: 0
StackSize: 0x40000
Dependency:
ac: 0x0004013000002402
am: 0x0004013000001502
boss: 0x0004013000003402
camera: 0x0004013000001602
cecd: 0x0004013000002602
cfg: 0x0004013000001702
codec: 0x0004013000001802
csnd: 0x0004013000002702
dlp: 0x0004013000002802
dsp: 0x0004013000001a02
friends: 0x0004013000003202
gpio: 0x0004013000001b02
gsp: 0x0004013000001c02
hid: 0x0004013000001d02
http: 0x0004013000002902
i2c: 0x0004013000001e02
ir: 0x0004013000003302
mcu: 0x0004013000001f02
mic: 0x0004013000002002
ndm: 0x0004013000002b02
news: 0x0004013000003502
nim: 0x0004013000002c02
nwm: 0x0004013000002d02
pdn: 0x0004013000002102
ps: 0x0004013000003102
ptm: 0x0004013000002202
ro: 0x0004013000003702
socket: 0x0004013000002e02
spi: 0x0004013000002302
ssl: 0x0004013000002f02
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

+477
View File
@@ -0,0 +1,477 @@
// MBMNET Shop - Homebrew für den 3DS, aus der Universal-DB (über den MBMNET-Server).
//
// Oben: die gewählte App (Symbol, Name, Beschreibung). Unten: Reiter (Kategorien) und die Liste.
// A installiert / aktualisiert / startet, Y entfernt, L/R wechselt die Kategorie, X die Sortierung.
#include <3ds.h>
#include <citro2d.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "store.h"
#define TOP_W 400
#define BOT_W 320
#define SCR_H 240
#define COL_RED C2D_Color32(0xE4, 0x00, 0x2B, 0xFF)
#define COL_RED_DARK C2D_Color32(0x9E, 0x00, 0x1E, 0xFF)
#define COL_BG C2D_Color32(0x16, 0x19, 0x1F, 0xFF)
#define COL_BG2 C2D_Color32(0x22, 0x26, 0x2E, 0xFF)
#define COL_CARD C2D_Color32(0x2A, 0x2F, 0x39, 0xFF)
#define COL_CARD_HI C2D_Color32(0x3A, 0x41, 0x4E, 0xFF)
#define COL_WHITE C2D_Color32(0xFF, 0xFF, 0xFF, 0xFF)
#define COL_MUTED C2D_Color32(0x9A, 0xA3, 0xAF, 0xFF)
#define COL_GREEN C2D_Color32(0x22, 0xC5, 0x5E, 0xFF)
#define COL_AMBER C2D_Color32(0xF5, 0x9E, 0x0B, 0xFF)
#define TAB_H 26
#define ROW_H 40
#define LIST_Y (TAB_H + 2)
#define VISIBLE_ROWS ((SCR_H - LIST_Y) / ROW_H + 1)
// Kategorien (Universal-DB) und ihre Reiter
typedef struct { const char *label, *match; } Tab;
static const Tab TABS[] = {
{ "Alle", NULL }, { "Spiele", "game" }, { "Tools", "utility" }, { "Apps", "app" },
{ "Emus", "emulator" }, { "Medien", "media" }, { "Meine", "*installed" },
};
#define TAB_COUNT (int)(sizeof(TABS) / sizeof(TABS[0]))
static const char *SORT_NAMES[] = { "Name", "Beliebt", "Neu" };
static C2D_TextBuf textBuf, measureBuf; // measureBuf nur zum Messen (wird jedes Mal geleert)
static C2D_SpriteSheet sheets[8];
// --- 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 text_wrap(const char *s, float x, float y, float size, u32 color, float width)
{
C2D_Text t;
C2D_TextParse(&t, textBuf, s);
C2D_TextOptimize(&t);
C2D_DrawText(&t, C2D_WithColor | C2D_WordWrap, x, y, 0.5f, size, size, color, width);
}
static float text_width(const char *s, float size)
{
C2D_Text t;
float w = 0;
C2D_TextBufClear(measureBuf);
C2D_TextParse(&t, measureBuf, s);
C2D_TextGetDimensions(&t, size, size, &w, NULL);
return w;
}
// Text auf eine Breite kürzen ("...")
static void fit(char *out, size_t len, const char *s, float size, float width)
{
snprintf(out, len, "%s", s);
if (text_width(out, size) <= width) return;
size_t n = strlen(out);
while (n > 1) {
n--;
while (n > 0 && (out[n] & 0xC0) == 0x80) n--; // nicht mitten in einem UTF-8-Zeichen
snprintf(out + n, len - n, "...");
if (text_width(out, size) <= width) return;
}
}
static void round_rect(float x, float y, float w, float h, float r, u32 color)
{
C2D_DrawRectSolid(x + r, y, 0.5f, w - 2 * r, h, color);
C2D_DrawRectSolid(x, y + r, 0.5f, w, h - 2 * r, color);
C2D_DrawCircleSolid(x + r, y + r, 0.5f, r, color);
C2D_DrawCircleSolid(x + w - r, y + r, 0.5f, r, color);
C2D_DrawCircleSolid(x + r, y + h - r, 0.5f, r, color);
C2D_DrawCircleSolid(x + w - r, y + h - r, 0.5f, r, color);
}
// Einkaufswagen (wie das Symbol der App)
static void draw_cart(float x, float y, float size, u32 bg)
{
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);
float t = 8 * s;
C2D_DrawLine(x + 13 * s, y + 23 * s, COL_WHITE, x + 23 * s, y + 23 * s, COL_WHITE, t, 0.5f);
C2D_DrawLine(x + 23 * s, y + 23 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, t, 0.5f);
C2D_DrawLine(x + 33 * s, y + 61 * s, COL_WHITE, x + 30 * s, y + 70 * s, COL_WHITE, t, 0.5f);
C2D_DrawLine(x + 30 * s, y + 70 * s, COL_WHITE, x + 78 * s, y + 70 * s, COL_WHITE, t, 0.5f);
C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 84 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, 0.5f);
C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, 0.5f);
C2D_DrawCircleSolid(x + 37 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE);
C2D_DrawCircleSolid(x + 72 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE);
(void)bg;
}
static void draw_icon(const App *a, float x, float y, float size)
{
C2D_SpriteSheet sh = a->sheet >= 0 && a->sheet < 8 ? sheets[a->sheet] : NULL;
if (sh && a->icon >= 0 && (size_t)a->icon < C2D_SpriteSheetCount(sh)) {
C2D_Image img = C2D_SpriteSheetGetImage(sh, a->icon);
float sc = size / (img.subtex->width ? img.subtex->width : 48);
C2D_DrawImageAt(img, x, y, 0.5f, NULL, sc, sc);
return;
}
// kein Symbol: Anfangsbuchstabe auf grauem Feld
round_rect(x, y, size, size, size / 6, COL_CARD_HI);
char first[8] = { 0 };
int n = 1;
while (n < 4 && (a->title[n] & 0xC0) == 0x80) n++;
memcpy(first, a->title, n);
text(first, x + size / 2, y + size * 0.2f, size / 48.0f, COL_WHITE, C2D_AlignCenter);
}
static void size_text(char *out, size_t len, u32 bytes)
{
if (bytes >= 1024 * 1024) snprintf(out, len, "%.1f MB", bytes / (1024.0 * 1024.0));
else if (bytes) snprintf(out, len, "%lu KB", (unsigned long)((bytes + 1023) / 1024));
else snprintf(out, len, "?");
}
// --- Liste (gefiltert und sortiert) ---------------------------------------------
static int *view; // Indizes in den Katalog
static int viewCount;
static int sortMode;
static int cmp_name(const void *a, const void *b)
{
return strcasecmp(catalog_app(*(const int *)a)->title, catalog_app(*(const int *)b)->title);
}
static int cmp_stars(const void *a, const void *b)
{
const App *x = catalog_app(*(const int *)a), *y = catalog_app(*(const int *)b);
if (x->stars != y->stars) return x->stars > y->stars ? -1 : 1;
return strcasecmp(x->title, y->title);
}
static int cmp_new(const void *a, const void *b)
{
const App *x = catalog_app(*(const int *)a), *y = catalog_app(*(const int *)b);
int c = strcmp(y->updated, x->updated);
return c ? c : strcasecmp(x->title, y->title);
}
static bool in_tab(const App *a, int tab)
{
const char *m = TABS[tab].match;
if (!m) return true;
if (strcmp(m, "*installed") == 0) return a->installed;
if (strcmp(m, "utility") == 0)
return !strcmp(a->category, "utility") || !strcmp(a->category, "save-tool") || !strcmp(a->category, "plugin");
return strcmp(a->category, m) == 0;
}
static void rebuild_view(int tab)
{
int n = catalog_count();
free(view);
view = malloc(sizeof(int) * (n ? n : 1));
viewCount = 0;
for (int i = 0; i < n; i++)
if (in_tab(catalog_app(i), tab)) view[viewCount++] = i;
qsort(view, viewCount, sizeof(int), sortMode == 1 ? cmp_stars : sortMode == 2 ? cmp_new : cmp_name);
}
// --- Bildschirme ---------------------------------------------------------------
static void draw_status_badge(const App *a, float right, float y)
{
AppStatus s = app_status(a);
if (s == APP_NEW) return;
const char *label = s == APP_UPDATE ? "Update" : "Installiert";
u32 col = s == APP_UPDATE ? COL_AMBER : COL_GREEN;
float w = text_width(label, 0.38f) + 10;
round_rect(right - w, y, w, 15, 7, col);
text(label, right - w / 2, y + 1.5f, 0.38f, COL_BG, C2D_AlignCenter);
}
static void draw_top(const App *a, int tab, const char *msg, bool armed)
{
C2D_DrawRectangle(0, 0, 0, TOP_W, SCR_H, COL_BG2, COL_BG2, COL_BG, COL_BG);
// Kopfzeile
draw_cart(10, 6, 22, COL_BG2);
text("MBMNET Shop", 38, 8, 0.55f, COL_WHITE, 0);
char info[64];
if (catalog_state() == CAT_READY)
snprintf(info, sizeof(info), "%s · %d Apps · %s%s", TABS[tab].label, viewCount, SORT_NAMES[sortMode],
catalog_offline() ? " · offline" : "");
else snprintf(info, sizeof(info), "v" APP_VERSION);
text(info, TOP_W - 10, 10, 0.42f, COL_MUTED, C2D_AlignRight);
C2D_DrawRectSolid(0, 32, 0.5f, TOP_W, 2, COL_RED);
if (!a) {
draw_cart(TOP_W / 2 - 36, 60, 72, COL_BG2);
const char *m = catalog_state() == CAT_READY ? (tab == TAB_COUNT - 1 ? "Noch nichts aus dem Shop installiert."
: "Hier gibt es nichts.")
: catalog_message();
text_wrap(m, 40, 150, 0.5f, COL_MUTED, TOP_W - 80);
return;
}
// App-Kopf: Symbol, Name, Autor, Version, Größe
round_rect(10, 42, 56, 56, 8, COL_CARD);
draw_icon(a, 14, 46, 48);
char line[96], size[16];
fit(line, sizeof(line), a->title, 0.7f, TOP_W - 90);
text(line, 76, 42, 0.7f, COL_WHITE, 0);
fit(line, sizeof(line), a->author, 0.45f, TOP_W - 90);
text(line, 76, 64, 0.45f, COL_MUTED, 0);
size_text(size, sizeof(size), a->size);
snprintf(line, sizeof(line), "%s · %s · %s", a->version[0] ? a->version : "ohne Version",
size, strcmp(a->kind, "cia") == 0 ? "CIA" : "3DSX");
text(line, 76, 82, 0.42f, COL_MUTED, 0);
draw_status_badge(a, TOP_W - 10, 82);
// Beschreibung oder Meldung
round_rect(10, 106, TOP_W - 20, 104, 8, COL_CARD);
if (msg[0]) {
text_wrap(msg, 20, 114, 0.48f, armed ? COL_AMBER : COL_WHITE, TOP_W - 40);
if (job_state() == JOB_RUNNING) {
float p = job_progress();
round_rect(20, 184, TOP_W - 40, 14, 7, COL_BG);
if (p >= 0) round_rect(20, 184, 14 + (TOP_W - 54) * (p > 1 ? 1 : p), 14, 7, COL_RED);
}
} else {
char desc[440];
snprintf(desc, sizeof(desc), "%s", a->description ? a->description : "");
if (strlen(desc) >= sizeof(desc) - 1) {
char *cut = strrchr(desc, ' ');
if (cut) snprintf(cut, sizeof(desc) - (cut - desc), " ...");
}
text_wrap(desc, 20, 114, 0.42f, COL_WHITE, TOP_W - 40);
}
// Tastenhilfe
AppStatus s = app_status(a);
const char *aLabel = s == APP_NEW ? "A: Installieren" : s == APP_UPDATE ? "A: Aktualisieren"
: strcmp(a->kind, "cia") == 0 ? "A: Starten" : "A: Neu laden";
snprintf(line, sizeof(line), "%s%s L/R: Kategorie X: Sortierung START: Ende", aLabel,
s != APP_NEW ? " Y: Entfernen" : "");
text(line, TOP_W / 2, 220, 0.38f, COL_MUTED, C2D_AlignCenter);
}
static void draw_bottom(int tab, int selected, int scroll)
{
C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG);
// Reiter
float w = (float)BOT_W / TAB_COUNT;
for (int i = 0; i < TAB_COUNT; i++) {
bool on = i == tab;
C2D_DrawRectSolid(i * w, 0, 0.5f, w, TAB_H, on ? COL_RED : COL_BG2);
text(TABS[i].label, i * w + w / 2, 6, 0.42f, on ? COL_WHITE : COL_MUTED, C2D_AlignCenter);
}
if (catalog_state() != CAT_READY) {
text_wrap(catalog_state() == CAT_ERROR ? "Kein Katalog. A: nochmal versuchen" : "Einen Moment ...",
20, 110, 0.5f, COL_MUTED, BOT_W - 40);
return;
}
// Liste
for (int r = 0; r < VISIBLE_ROWS; r++) {
int i = scroll + r;
if (i >= viewCount) break;
const App *a = catalog_app(view[i]);
float y = LIST_Y + r * ROW_H;
if (i == selected) {
C2D_DrawRectSolid(0, y, 0.5f, BOT_W, ROW_H, COL_CARD_HI);
C2D_DrawRectSolid(0, y + 6, 0.5f, 4, ROW_H - 12, COL_RED);
} else if (i % 2) {
C2D_DrawRectSolid(0, y, 0.5f, BOT_W, ROW_H, COL_BG2);
}
draw_icon(a, 8, y + 4, 32);
char line[96];
AppStatus s = app_status(a);
float right = s == APP_NEW ? BOT_W - 8 : BOT_W - 78;
fit(line, sizeof(line), a->title, 0.5f, right - 48);
text(line, 48, y + 4, 0.5f, COL_WHITE, 0);
fit(line, sizeof(line), a->author, 0.38f, right - 48);
text(line, 48, y + 22, 0.38f, COL_MUTED, 0);
draw_status_badge(a, BOT_W - 8, y + 12);
}
// Rollbalken
if (viewCount > VISIBLE_ROWS - 1) {
float h = SCR_H - LIST_Y, bar = h * (VISIBLE_ROWS - 1) / viewCount;
if (bar < 12) bar = 12;
float y = LIST_Y + (h - bar) * scroll / (viewCount - (VISIBLE_ROWS - 1));
C2D_DrawRectSolid(BOT_W - 3, y, 0.5f, 3, bar, COL_MUTED);
}
}
// --- Aktionen --------------------------------------------------------------------
// Installierte CIA starten (Sprung in die App, der Shop beendet sich)
static bool launch(const App *a, char *msg, size_t len)
{
if (!a->titleId) { snprintf(msg, len, "Starte %s bitte im HOME-Menü.", a->title); return false; }
Result rc = APT_PrepareToDoApplicationJump(0, a->titleId, MEDIATYPE_SD);
if (R_SUCCEEDED(rc)) {
u8 param[0x300] = { 0 }, hmac[0x20] = { 0 };
rc = APT_DoApplicationJump(param, sizeof(param), hmac);
}
if (R_FAILED(rc)) { snprintf(msg, len, "Starte %s bitte im HOME-Menü (%08lX).", a->title, (unsigned long)rc); return false; }
return true;
}
static void load_sheets(void)
{
for (int n = 0; n < catalog_sheet_count() && n < 8; n++) {
const char *p = catalog_sheet_path(n);
if (p[0] && !sheets[n]) sheets[n] = C2D_SpriteSheetLoad(p);
}
}
int main(int argc, char **argv)
{
(void)argc; (void)argv;
gfxInitDefault();
httpcInit(0);
amInit();
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
C2D_Prepare();
C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);
textBuf = C2D_TextBufNew(8192);
measureBuf = C2D_TextBufNew(512);
hidSetRepeatParameters(18, 4);
catalog_start();
int tab = 0, selected = 0, scroll = 0;
bool ready = false;
char msg[192] = "";
int armed = -1; // Sicherheitsabfrage: 0 = installieren, 1 = entfernen
bool quit = false;
int touchStartY = -1, touchScroll = 0, touchRow = -1;
bool dragged = false;
while (!quit && aptMainLoop()) {
hidScanInput();
u32 down = hidKeysDown(), rep = hidKeysDownRepeat(), held = hidKeysHeld();
bool busy = job_state() == JOB_RUNNING;
if (down & KEY_START && !busy) break;
// Katalog fertig: Symbole laden und Liste aufbauen
if (!ready && catalog_state() == CAT_READY) {
load_sheets();
rebuild_view(tab);
ready = true;
}
if (catalog_state() == CAT_ERROR && down & KEY_A) catalog_start();
// Ergebnis einer Installation: anzeigen, Liste auffrischen (z. B. Reiter "Meine")
JobState js = job_state();
if (js == JOB_DONE || js == JOB_FAILED) {
snprintf(msg, sizeof(msg), "%s", job_message());
job_acknowledge();
const App *cur = viewCount ? catalog_app(view[selected]) : NULL;
rebuild_view(tab);
selected = 0;
for (int i = 0; cur && i < viewCount; i++) if (catalog_app(view[i]) == cur) selected = i;
}
if (ready && !busy) {
int oldSel = selected, oldTab = tab;
if (down & KEY_R) tab = (tab + 1) % TAB_COUNT;
if (down & KEY_L) tab = (tab + TAB_COUNT - 1) % TAB_COUNT;
if (down & KEY_X) { sortMode = (sortMode + 1) % 3; rebuild_view(tab); selected = scroll = 0; }
if (rep & (KEY_DOWN | KEY_CPAD_DOWN)) selected++;
if (rep & (KEY_UP | KEY_CPAD_UP)) selected--;
if (rep & (KEY_RIGHT | KEY_CPAD_RIGHT)) selected += VISIBLE_ROWS - 1;
if (rep & (KEY_LEFT | KEY_CPAD_LEFT)) selected -= VISIBLE_ROWS - 1;
// Touch: Reiter antippen, Liste ziehen, Zeile antippen (zweimal = Aktion)
touchPosition tp;
if (down & KEY_TOUCH) {
hidTouchRead(&tp);
if (tp.py < TAB_H) tab = tp.px * TAB_COUNT / BOT_W;
else { touchStartY = tp.py; touchScroll = scroll; dragged = false; touchRow = scroll + (tp.py - LIST_Y) / ROW_H; }
} else if (held & KEY_TOUCH && touchStartY >= 0) {
hidTouchRead(&tp);
int dy = touchStartY - tp.py;
if (abs(dy) > 6) dragged = true;
if (dragged) {
scroll = touchScroll + dy / ROW_H;
int maxScroll = viewCount - (VISIBLE_ROWS - 1);
if (scroll > maxScroll) scroll = maxScroll;
if (scroll < 0) scroll = 0;
}
} else if (touchStartY >= 0) {
if (!dragged && touchRow >= 0 && touchRow < viewCount) {
if (touchRow == selected) down |= KEY_A;
else selected = touchRow;
}
touchStartY = -1;
}
if (tab != oldTab) { rebuild_view(tab); selected = scroll = 0; }
if (selected >= viewCount) selected = viewCount - 1;
if (selected < 0) selected = 0;
if (selected != oldSel || tab != oldTab) { msg[0] = 0; armed = -1; }
if (selected < scroll) scroll = selected;
if (selected >= scroll + VISIBLE_ROWS - 1) scroll = selected - (VISIBLE_ROWS - 2);
App *a = viewCount ? catalog_app(view[selected]) : NULL;
if (a && down & KEY_A) {
AppStatus s = app_status(a);
if (s == APP_INSTALLED && strcmp(a->kind, "cia") == 0) {
armed = -1;
if (launch(a, msg, sizeof(msg))) quit = true;
} else if (armed == 0) {
armed = -1;
char size[16];
size_text(size, sizeof(size), a->size);
snprintf(msg, sizeof(msg), "Lade %s (%s) ...", a->title, size);
job_install(a);
} else if (catalog_offline()) {
snprintf(msg, sizeof(msg), "Keine Verbindung zum MBMNET-Server.\nInstallieren geht gerade nicht.");
} else {
armed = 0;
snprintf(msg, sizeof(msg), "%s %s?\nZum Bestätigen nochmal A drücken (oder nochmal antippen).",
a->title, s == APP_UPDATE ? "aktualisieren" : "installieren");
}
}
if (a && down & KEY_Y && app_status(a) != APP_NEW) {
if (armed == 1) {
armed = -1;
snprintf(msg, sizeof(msg), "Entferne %s ...", a->title);
job_remove(a);
} else {
armed = 1;
snprintf(msg, sizeof(msg), "%s entfernen?\nZum Bestätigen nochmal Y drücken.", a->title);
}
}
if (down & KEY_B) { msg[0] = 0; armed = -1; }
}
const App *cur = ready && viewCount ? catalog_app(view[selected]) : NULL;
C2D_TextBufClear(textBuf);
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
C2D_TargetClear(top, COL_BG);
C2D_SceneBegin(top);
draw_top(cur, tab, msg, armed >= 0);
C2D_TargetClear(bot, COL_BG);
C2D_SceneBegin(bot);
draw_bottom(tab, selected, scroll);
C3D_FrameEnd(0);
}
store_exit();
for (int n = 0; n < 8; n++) if (sheets[n]) C2D_SpriteSheetFree(sheets[n]);
free(view);
C2D_TextBufDelete(textBuf);
C2D_TextBufDelete(measureBuf);
C2D_Fini();
C3D_Fini();
amExit();
httpcExit();
gfxExit();
return 0;
}
+73
View File
@@ -0,0 +1,73 @@
// HTTP über den Systemdienst http:C. Alles läuft über den MBMNET-Server (einfaches HTTP),
// der Katalog, Symbole und Downloads von Universal-DB und GitHub holt.
#include <3ds.h>
#include <stdlib.h>
#include <string.h>
#include "net.h"
#define CHUNK (128 * 1024)
static Result open_request(httpcContext *ctx, const char *url, u32 *status)
{
Result rc = httpcOpenContext(ctx, HTTPC_METHOD_GET, url, 1);
if (R_FAILED(rc)) return rc;
httpcAddRequestHeaderField(ctx, "User-Agent", "MBMNET-Shop/" APP_VERSION);
httpcSetKeepAlive(ctx, HTTPC_KEEPALIVE_DISABLED);
if (R_SUCCEEDED(rc = httpcBeginRequest(ctx)))
rc = httpcGetResponseStatusCodeTimeout(ctx, status, 30ULL * 1000 * 1000 * 1000);
if (R_FAILED(rc)) httpcCloseContext(ctx);
return rc;
}
u32 net_stream(const char *url, NetSink sink, void *ctx, Result *rcOut)
{
httpcContext http;
u32 status = 0, total = 0, got;
Result rc = open_request(&http, url, &status);
if (rcOut) *rcOut = rc;
if (R_FAILED(rc)) return 0;
if (status != 200) { httpcCloseContext(&http); return status; }
httpcGetDownloadSizeState(&http, NULL, &total);
u8 *buf = malloc(CHUNK);
if (!buf) { httpcCloseContext(&http); if (rcOut) *rcOut = -2; return 0; }
do {
got = 0;
rc = httpcDownloadData(&http, buf, CHUNK, &got);
if (got && !sink(ctx, buf, got, total)) { rc = -3; break; } // -3 = Empfänger hat abgebrochen
} while (rc == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING);
free(buf);
httpcCloseContext(&http);
if (rcOut) *rcOut = rc;
return R_SUCCEEDED(rc) ? 200 : 0;
}
typedef struct { u8 *data; u32 size, cap, max; } MemSink;
static bool to_memory(void *p, const u8 *data, u32 len, u32 total)
{
MemSink *m = p;
if (m->size + len > m->max) return false;
if (m->size + len > m->cap) {
u32 cap = m->cap ? m->cap : (total && total <= m->max ? total : 64 * 1024);
while (cap < m->size + len) cap *= 2;
u8 *bigger = realloc(m->data, cap);
if (!bigger) return false;
m->data = bigger;
m->cap = cap;
}
memcpy(m->data + m->size, data, len);
m->size += len;
return true;
}
u32 net_get(const char *url, u8 **out, u32 *size, u32 max, Result *rc)
{
MemSink m = { NULL, 0, 0, max };
u32 status = net_stream(url, to_memory, &m, rc);
if (status != 200) { free(m.data); m.data = NULL; m.size = 0; }
*out = m.data;
*size = m.size;
return status;
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <3ds/types.h>
#include <stdbool.h>
// Empfänger für net_stream: bekommt die Daten stückweise. total = Gesamtgröße (0, wenn unbekannt).
// false = abbrechen.
typedef bool (*NetSink)(void *ctx, const u8 *data, u32 len, u32 total);
// Lädt eine URL komplett in den Speicher (höchstens max Bytes). Gibt den HTTP-Status zurück,
// 0 bei Netzwerkfehler (Code in *rc). *out mit free() freigeben.
u32 net_get(const char *url, u8 **out, u32 *size, u32 max, Result *rc);
// Lädt eine URL und reicht die Daten an sink weiter. Gibt den HTTP-Status zurück (0 bei Fehler).
u32 net_stream(const char *url, NetSink sink, void *ctx, Result *rc);
+476
View File
@@ -0,0 +1,476 @@
// Katalog, Symbole und installierte Apps des MBMNET Shops.
//
// Der Katalog kommt vom MBMNET-Server (/shop/list, Quelle Universal-DB) und wird auf der SD
// zwischengespeichert, damit der Shop auch ohne Verbindung die installierten Apps zeigt.
// Was der Shop installiert hat, steht in SHOP_DIR/installed.txt (nur dann kann er es wieder entfernen).
#include <3ds.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "net.h"
#include "store.h"
#define LIST_FILE SHOP_DIR "/list.txt"
#define INSTALLED_FILE SHOP_DIR "/installed.txt"
#define MAX_SHEETS 8
// --- Katalog ---------------------------------------------------------------------
static volatile CatalogState catState = CAT_IDLE;
static char catMsg[128];
static bool offline;
static App *apps;
static int appCount;
static int sheetCount;
static char sheetPath[MAX_SHEETS][64];
static char *extraInstalled; // Einträge in installed.txt zu Apps, die es im Katalog nicht (mehr) gibt
static Thread catThread, jobThread;
CatalogState catalog_state(void) { return catState; }
const char *catalog_message(void) { return catMsg; }
bool catalog_offline(void) { return offline; }
int catalog_count(void) { return catState == CAT_READY ? appCount : 0; }
App *catalog_app(int i) { return &apps[i]; }
int catalog_sheet_count(void) { return sheetCount; }
const char *catalog_sheet_path(int n) { return n >= 0 && n < sheetCount ? sheetPath[n] : ""; }
AppStatus app_status(const App *a)
{
if (!a->installed) return APP_NEW;
if (a->version[0] && strcmp(a->version, a->installedVersion) != 0) return APP_UPDATE;
return APP_INSTALLED;
}
static bool file_exists(const char *path)
{
struct stat st;
return stat(path, &st) == 0;
}
static bool write_file(const char *path, const void *data, u32 size)
{
char tmp[96];
snprintf(tmp, sizeof(tmp), "%s.neu", path);
FILE *f = fopen(tmp, "wb");
bool ok = f && fwrite(data, 1, size, f) == size;
if (f) fclose(f);
if (ok) { remove(path); ok = rename(tmp, path) == 0; }
if (!ok) remove(tmp);
return ok;
}
static char *read_file(const char *path, u32 *size)
{
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
char *buf = n >= 0 ? malloc(n + 1) : NULL;
if (buf && fread(buf, 1, n, f) != (size_t)n) { free(buf); buf = NULL; }
fclose(f);
if (buf) { buf[n] = 0; if (size) *size = n; }
return buf;
}
// Zerlegt eine Zeile in Felder (Tab-getrennt). Gibt die Anzahl zurück.
static int split(char *line, char **fields, int max)
{
int n = 0;
while (n < max) {
fields[n++] = line;
char *tab = strchr(line, '\t');
if (!tab) break;
*tab = 0;
line = tab + 1;
}
return n;
}
#define COPY(dst, src) snprintf(dst, sizeof(dst), "%s", src)
static bool parse_list(char *text, int *revision)
{
char *save = NULL, *line = strtok_r(text, "\n", &save);
char *f[16];
if (!line || split(line, f, 16) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false;
*revision = atoi(f[2]);
int count = atoi(f[3]);
sheetCount = atoi(f[4]);
if (sheetCount > MAX_SHEETS) sheetCount = MAX_SHEETS;
if (count <= 0 || count > 5000) return false;
apps = calloc(count, sizeof(App));
if (!apps) return false;
appCount = 0;
while ((line = strtok_r(NULL, "\n", &save)) && appCount < count) {
if (split(line, f, 16) < 13) continue;
App *a = &apps[appCount++];
COPY(a->slug, f[0]);
COPY(a->title, f[1]);
COPY(a->author, f[2]);
COPY(a->version, f[3]);
COPY(a->category, f[4]);
a->sheet = atoi(f[5]);
a->icon = atoi(f[6]);
COPY(a->kind, f[7]);
COPY(a->file, f[8]);
a->size = strtoul(f[9], NULL, 10);
a->stars = strtoul(f[10], NULL, 10);
COPY(a->updated, f[11]);
a->description = strdup(f[12]);
}
return appCount > 0;
}
static App *find_app(const char *slug)
{
for (int i = 0; i < appCount; i++)
if (strcmp(apps[i].slug, slug) == 0) return &apps[i];
return NULL;
}
// installed.txt: slug \t Version \t Art \t Titel-ID (hex) \t Dateiname
static void load_installed(void)
{
char *text = read_file(INSTALLED_FILE, NULL);
if (!text) return;
size_t extraLen = 0;
char *save = NULL, *line, *f[8];
for (line = strtok_r(text, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
char copy[256];
COPY(copy, line);
if (split(line, f, 8) < 5) continue;
App *a = find_app(f[0]);
if (!a) {
// unbekannte App merken, damit sie beim Speichern nicht verloren geht
size_t len = strlen(copy);
char *bigger = realloc(extraInstalled, extraLen + len + 2);
if (!bigger) continue;
extraInstalled = bigger;
memcpy(extraInstalled + extraLen, copy, len);
extraLen += len;
extraInstalled[extraLen++] = '\n';
extraInstalled[extraLen] = 0;
continue;
}
a->installed = true;
COPY(a->installedVersion, f[1]);
a->titleId = strtoull(f[3], NULL, 16);
if (f[4][0]) COPY(a->file, f[4]);
}
free(text);
}
static void save_installed(void)
{
FILE *f = fopen(INSTALLED_FILE, "w");
if (!f) return;
for (int i = 0; i < appCount; i++) {
App *a = &apps[i];
if (a->installed)
fprintf(f, "%s\t%s\t%s\t%016llX\t%s\n", a->slug, a->installedVersion, a->kind, a->titleId, a->file);
}
if (extraInstalled) fputs(extraInstalled, f);
fclose(f);
}
// Alte Symbol-Blätter (andere Revision) löschen
static void remove_old_sheets(int revision)
{
char keep[32];
snprintf(keep, sizeof(keep), "icons-%d-", revision);
DIR *d = opendir(SHOP_DIR);
if (!d) return;
struct dirent *e;
char path[320];
while ((e = readdir(d))) {
if (strncmp(e->d_name, "icons-", 6) == 0 && strncmp(e->d_name, keep, strlen(keep)) != 0) {
snprintf(path, sizeof(path), SHOP_DIR "/%s", e->d_name);
remove(path);
}
}
closedir(d);
}
static void catalog_worker(void *arg)
{
(void)arg;
char url[160];
u8 *data = NULL;
u32 size = 0;
Result rc;
mkdir("sdmc:/3ds", 0777);
mkdir(SHOP_DIR, 0777);
snprintf(catMsg, sizeof(catMsg), "Lade den Katalog ...");
snprintf(url, sizeof(url), "http://%s/shop/list", SHOP_SERVER);
u32 status = net_get(url, &data, &size, 4 * 1024 * 1024, &rc);
char *text = NULL;
if (status == 200 && size > 10) {
write_file(LIST_FILE, data, size);
text = realloc(data, size + 1);
if (text) text[size] = 0; else free(data);
} else {
free(data);
text = read_file(LIST_FILE, &size);
offline = true;
if (!text) {
if (status) snprintf(catMsg, sizeof(catMsg), "Der Shop-Server antwortet nicht (HTTP %lu).", (unsigned long)status);
else snprintf(catMsg, sizeof(catMsg), "Keine Verbindung zum MBMNET-Server (%08lX).\nIst das WLAN an?", (unsigned long)rc);
catState = CAT_ERROR;
return;
}
}
int revision = 0;
bool ok = parse_list(text, &revision);
free(text);
if (!ok) {
snprintf(catMsg, sizeof(catMsg), "Der Katalog ist beschädigt.");
catState = CAT_ERROR;
return;
}
load_installed();
// Symbol-Blätter (einmal pro Katalog-Revision laden)
for (int n = 0; n < sheetCount; n++) {
snprintf(sheetPath[n], sizeof(sheetPath[n]), SHOP_DIR "/icons-%d-%d.t3x", revision, n);
if (file_exists(sheetPath[n])) continue;
if (offline) { sheetPath[n][0] = 0; continue; }
snprintf(catMsg, sizeof(catMsg), "Lade die Symbole (%d von %d) ...", n + 1, sheetCount);
snprintf(url, sizeof(url), "http://%s/shop/sheet/%d", SHOP_SERVER, n);
if (net_get(url, &data, &size, 16 * 1024 * 1024, &rc) != 200 || !write_file(sheetPath[n], data, size))
sheetPath[n][0] = 0;
free(data);
}
if (!offline) remove_old_sheets(revision);
catMsg[0] = 0;
catState = CAT_READY;
}
static void start_thread(Thread *t, ThreadFunc fn)
{
if (*t) { threadJoin(*t, U64_MAX); threadFree(*t); *t = NULL; }
s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
*t = threadCreate(fn, NULL, 32 * 1024, prio + 1, -2, false);
}
void catalog_start(void)
{
if (catState == CAT_LOADING || catState == CAT_READY) return;
catState = CAT_LOADING;
start_thread(&catThread, catalog_worker);
if (!catThread) { snprintf(catMsg, sizeof(catMsg), "Konnte nicht starten."); catState = CAT_ERROR; }
}
// --- Installieren / Entfernen ------------------------------------------------------
static volatile JobState jobState = JOB_IDLE;
static volatile u32 jobDone, jobTotal;
static char jobMsg[160];
static App *jobApp;
static bool jobRemove;
JobState job_state(void) { return jobState; }
float job_progress(void) { return jobTotal ? (float)jobDone / jobTotal : -1.0f; }
const char *job_message(void) { return jobMsg; }
void job_acknowledge(void) { if (jobState != JOB_RUNNING) jobState = JOB_IDLE; }
static inline u32 le32(const u8 *p) { return p[0] | p[1] << 8 | p[2] << 16 | (u32)p[3] << 24; }
static inline u32 be32(const u8 *p) { return (u32)p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]; }
static inline u32 align64(u32 x) { return (x + 63) & ~63u; }
// Titel-ID aus dem Kopf einer CIA (Kopf, Zertifikate, Ticket, dann die TMD mit der Titel-ID)
static u64 cia_title_id(const u8 *d, u32 n)
{
if (n < 0x20) return 0;
u32 off = align64(le32(d)) + align64(le32(d + 8)) + align64(le32(d + 12));
if (off + 4 > n) return 0;
u32 sigType = be32(d + off), sig;
switch (sigType) {
case 0x10003: sig = 0x200 + 0x3C; break;
case 0x10004: sig = 0x100 + 0x3C; break;
case 0x10005: sig = 0x3C + 0x40; break;
default: return 0;
}
u32 hdr = off + 4 + sig;
if (hdr + 0x54 > n) return 0;
return (u64)be32(d + hdr + 0x4C) << 32 | be32(d + hdr + 0x50);
}
#define HEAD_SIZE (64 * 1024)
typedef struct {
bool cia, started;
Handle cia_h;
FILE *file;
u32 offset;
u8 *head; // Anfang der Datei (für Prüfung und Titel-ID)
u32 headLen;
Result rc;
const char *error;
} Install;
static bool write_part(Install *in, const u8 *data, u32 len)
{
if (!len) return true;
if (in->cia) {
u32 w = 0;
in->rc = FSFILE_Write(in->cia_h, &w, in->offset, data, len, 0);
if (R_FAILED(in->rc) || w != len) { in->error = "Schreiben fehlgeschlagen"; return false; }
} else if (fwrite(data, 1, len, in->file) != len) {
in->error = "SD-Karte voll?";
return false;
}
in->offset += len;
jobDone = in->offset;
return true;
}
static bool install_sink(void *p, const u8 *data, u32 len, u32 total)
{
Install *in = p;
jobTotal = total;
// Den Anfang der Datei mitschreiben (Prüfung, Titel-ID)
u32 copied = 0;
if (in->headLen < HEAD_SIZE && len) {
copied = len < HEAD_SIZE - in->headLen ? len : HEAD_SIZE - in->headLen;
memcpy(in->head + in->headLen, data, copied);
in->headLen += copied;
}
if (!in->started) {
if (in->headLen < 8) return true;
if (in->cia ? le32(in->head) != 0x2020 : memcmp(in->head, "3DSX", 4) != 0) {
in->error = "Die Datei ist keine gültige App.";
return false;
}
if (in->cia) {
in->rc = AM_StartCiaInstall(MEDIATYPE_SD, &in->cia_h);
if (R_FAILED(in->rc)) { in->error = "Installation nicht möglich"; return false; }
} else if (!(in->file = fopen(SHOP_DIR "/download.tmp", "wb"))) {
in->error = "Kann nicht auf die SD-Karte schreiben.";
return false;
}
in->started = true;
// Alles bisher Gesammelte schreiben, vom aktuellen Stück dann nur noch den Rest
if (!write_part(in, in->head, in->headLen)) return false;
data += copied;
len -= copied;
}
return write_part(in, data, len);
}
static void install_worker(App *a)
{
char url[192];
Install in = { .cia = strcmp(a->kind, "cia") == 0 };
in.head = malloc(HEAD_SIZE);
if (!in.head) { snprintf(jobMsg, sizeof(jobMsg), "Zu wenig Speicher."); jobState = JOB_FAILED; return; }
snprintf(url, sizeof(url), "http://%s/shop/get/%s", SHOP_SERVER, a->slug);
Result rc = 0;
u32 status = net_stream(url, install_sink, &in, &rc);
if (status == 200 && !in.started && !in.error) in.error = "Die Datei ist keine gültige App.";
bool ok = status == 200 && in.started && !in.error;
if (in.cia) {
if (in.started) {
if (ok) {
in.rc = AM_FinishCiaInstall(in.cia_h);
if (R_FAILED(in.rc)) { ok = false; in.error = "Installation abgelehnt"; }
} else {
AM_CancelCIAInstall(in.cia_h);
}
}
} else if (in.file) {
fclose(in.file);
char dest[128];
snprintf(dest, sizeof(dest), "sdmc:/3ds/%s", a->file);
if (ok) { remove(dest); ok = rename(SHOP_DIR "/download.tmp", dest) == 0; if (!ok) in.error = "Umbenennen fehlgeschlagen"; }
else remove(SHOP_DIR "/download.tmp");
}
if (ok) {
bool update = a->installed;
a->installed = true;
COPY(a->installedVersion, a->version);
if (in.cia) a->titleId = cia_title_id(in.head, in.headLen);
save_installed();
snprintf(jobMsg, sizeof(jobMsg), in.cia ? "%s %s!\nDu findest die App jetzt im HOME-Menü."
: "%s %s!\nStarte sie über den Homebrew Launcher (/3ds/%s).",
a->title, update ? "aktualisiert" : "installiert", a->file);
jobState = JOB_DONE;
} else {
if (in.error && in.rc) snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\n%s (%08lX).", a->title, in.error, (unsigned long)in.rc);
else if (in.error) snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\n%s", a->title, in.error);
else if (status && status != 200) snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\nDer Server meldet HTTP %lu.", a->title, (unsigned long)status);
else snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\nDownload abgebrochen (%08lX).", a->title, (unsigned long)rc);
jobState = JOB_FAILED;
}
free(in.head);
}
static void remove_worker(App *a)
{
bool ok = true;
Result rc = 0;
if (strcmp(a->kind, "cia") == 0) {
if (a->titleId) {
rc = AM_DeleteTitle(MEDIATYPE_SD, a->titleId);
// schon von Hand gelöscht? Dann ist es auch gut.
AM_TitleEntry info;
u64 id = a->titleId;
if (R_FAILED(rc) && R_FAILED(AM_GetTitleInfo(MEDIATYPE_SD, 1, &id, &info))) rc = 0;
ok = R_SUCCEEDED(rc);
}
} else {
char path[128];
snprintf(path, sizeof(path), "sdmc:/3ds/%s", a->file);
remove(path);
}
if (ok) {
a->installed = false;
a->installedVersion[0] = 0;
a->titleId = 0;
save_installed();
snprintf(jobMsg, sizeof(jobMsg), "%s entfernt.", a->title);
jobState = JOB_DONE;
} else {
snprintf(jobMsg, sizeof(jobMsg), "%s ließ sich nicht entfernen (%08lX).", a->title, (unsigned long)rc);
jobState = JOB_FAILED;
}
}
static void job_worker(void *arg)
{
(void)arg;
if (jobRemove) remove_worker(jobApp);
else install_worker(jobApp);
}
static void job_start(App *a, bool remove)
{
if (jobState == JOB_RUNNING) return;
jobApp = a;
jobRemove = remove;
jobDone = jobTotal = 0;
jobMsg[0] = 0;
jobState = JOB_RUNNING;
start_thread(&jobThread, job_worker);
if (!jobThread) { snprintf(jobMsg, sizeof(jobMsg), "Konnte nicht starten."); jobState = JOB_FAILED; }
}
void job_install(App *a) { job_start(a, false); }
void job_remove(App *a) { job_start(a, true); }
void store_exit(void)
{
if (catThread) { threadJoin(catThread, U64_MAX); threadFree(catThread); }
if (jobThread) { threadJoin(jobThread, U64_MAX); threadFree(jobThread); }
for (int i = 0; i < appCount; i++) free(apps[i].description);
free(apps);
free(extraInstalled);
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <3ds/types.h>
#include <stdbool.h>
#include <stddef.h>
#define SHOP_SERVER "mbmnetapi.mbm.wtf:81"
#define SHOP_DIR "sdmc:/3ds/mbmnet-shop"
typedef enum { APP_NEW, APP_INSTALLED, APP_UPDATE } AppStatus;
typedef struct {
char slug[72], title[64], author[44], version[28], category[16], kind[6], file[72], updated[12];
int sheet, icon; // Symbol: Blatt und Nummer darin (-1 = keins)
u32 size, stars;
char *description;
// Installiert (aus SHOP_DIR/installed.txt)
bool installed;
char installedVersion[28];
u64 titleId; // bei CIAs, 0 sonst
} App;
// --- Katalog (läuft im Hintergrund) ---
typedef enum { CAT_IDLE, CAT_LOADING, CAT_READY, CAT_ERROR } CatalogState;
void catalog_start(void);
CatalogState catalog_state(void);
const char *catalog_message(void); // Fortschritt bzw. Fehler
bool catalog_offline(void); // aus dem Zwischenspeicher auf der SD (Server nicht erreichbar)
int catalog_count(void);
App *catalog_app(int i);
int catalog_sheet_count(void);
const char *catalog_sheet_path(int n); // Symbol-Blatt auf der SD (leer, wenn nicht da)
AppStatus app_status(const App *a);
// --- Installieren / Entfernen (läuft im Hintergrund) ---
typedef enum { JOB_IDLE, JOB_RUNNING, JOB_DONE, JOB_FAILED } JobState;
void job_install(App *a);
void job_remove(App *a);
JobState job_state(void);
float job_progress(void); // 0..1, < 0 wenn unbekannt
const char *job_message(void);
void job_acknowledge(void); // Ergebnis gesehen, zurück auf JOB_IDLE
void store_exit(void);
+105 -12
View File
@@ -15,6 +15,7 @@
#include "admin.h" #include "admin.h"
#include "ftpctl.h" #include "ftpctl.h"
#include "spotpass.h" #include "spotpass.h"
#include "shopctl.h"
#define TOP_W 400 #define TOP_W 400
#define BOT_W 320 #define BOT_W 320
@@ -33,18 +34,31 @@
#define COL_AMBER C2D_Color32(0xF5, 0x9E, 0x0B, 0xFF) #define COL_AMBER C2D_Color32(0xF5, 0x9E, 0x0B, 0xFF)
#define COL_NINTENDO C2D_Color32(0xE6, 0x00, 0x12, 0xFF) #define COL_NINTENDO C2D_Color32(0xE6, 0x00, 0x12, 0xFF)
// Bedienelemente auf dem unteren Bildschirm. Die Werkzeuge gibt es nur für Admins. // Bedienelemente auf dem unteren Bildschirm. Die Werkzeuge gibt es nur für Admins (mit SELECT),
enum { BTN_ENABLE, BTN_DISABLE, BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_REBOOT, BTN_COUNT }; // die Shop-Leiste nur ohne Werkzeuge.
enum { BTN_ENABLE, BTN_DISABLE, BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_REBOOT, BTN_SHOP, BTN_SHOP_DEL, BTN_COUNT };
typedef struct { float x, y, w, h; } Box; typedef struct { float x, y, w, h; } Box;
#define TILE_Y 14 #define TILE_Y 14
#define TILE_W 142 #define TILE_W 142
#define TILE_H_FULL 210 #define TILE_H_FULL 166
#define TILE_H_ADMIN 160 #define TILE_H_ADMIN 160
#define TOOL_COUNT 5 #define TOOL_COUNT 5
static const Box toolBoxes[TOOL_COUNT] = { { 12, 184, 56, 42 }, { 72, 184, 56, 42 }, { 132, 184, 56, 42 }, { 192, 184, 56, 42 }, { 252, 184, 56, 42 } }; static const Box toolBoxes[TOOL_COUNT] = { { 12, 184, 56, 42 }, { 72, 184, 56, 42 }, { 132, 184, 56, 42 }, { 192, 184, 56, 42 }, { 252, 184, 56, 42 } };
static const Box shopBox = { 12, 188, 216, 40 }, shopDelBox = { 232, 188, 76, 40 };
static int shopVersion = -1; // installierte Version des MBMNET Shops (-1 = nicht installiert)
// Welche Knöpfe sind gerade da?
static bool visible(int btn, bool admin)
{
if (btn <= BTN_DISABLE) return true;
if (btn <= BTN_REBOOT) return admin;
if (btn == BTN_SHOP) return !admin;
return !admin && shopVersion >= 0; // Entfernen nur, wenn installiert
}
static Box box_of(int btn, bool admin) static Box box_of(int btn, bool admin)
{ {
float h = admin ? TILE_H_ADMIN : TILE_H_FULL; float h = admin ? TILE_H_ADMIN : TILE_H_FULL;
@@ -55,6 +69,8 @@ static Box box_of(int btn, bool admin)
case BTN_FTP: return toolBoxes[1]; case BTN_FTP: return toolBoxes[1];
case BTN_UNLINK: return toolBoxes[2]; case BTN_UNLINK: return toolBoxes[2];
case BTN_SPOTPASS: return toolBoxes[3]; case BTN_SPOTPASS: return toolBoxes[3];
case BTN_SHOP: return shopBox;
case BTN_SHOP_DEL: return shopDelBox;
default: return toolBoxes[4]; default: return toolBoxes[4];
} }
} }
@@ -189,12 +205,47 @@ static void draw_tile(int btn, bool sel, bool active, bool admin)
} }
} }
// Kleiner Einkaufswagen (wie das Symbol des MBMNET Shops)
static void draw_cart(float x, float y, float size)
{
float s = size / 100.0f, t = 9 * s;
round_rect(x, y, size, size, 22 * s, COL_RED);
C2D_DrawLine(x + 13 * s, y + 23 * s, COL_WHITE, x + 23 * s, y + 23 * s, COL_WHITE, t, 0.5f);
C2D_DrawLine(x + 23 * s, y + 23 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, t, 0.5f);
C2D_DrawLine(x + 33 * s, y + 61 * s, COL_WHITE, x + 30 * s, y + 70 * s, COL_WHITE, t, 0.5f);
C2D_DrawLine(x + 30 * s, y + 70 * s, COL_WHITE, x + 78 * s, y + 70 * s, COL_WHITE, t, 0.5f);
C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 84 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, 0.5f);
C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, 0.5f);
C2D_DrawCircleSolid(x + 37 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE);
C2D_DrawCircleSolid(x + 72 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE);
}
static void draw_shop_bar(int selected)
{
bool busy = shopctl_job() == SHOPJOB_RUNNING;
bool outdated = shopVersion >= 0 && shopVersion < APP_TITLE_VERSION;
Box b = shopBox;
bool sel = selected == BTN_SHOP;
round_rect(b.x, b.y, b.w, b.h, 10, sel ? COL_CARD_HI : COL_CARD);
if (sel) C2D_DrawRectSolid(b.x, b.y + 10, 0.5f, 4, b.h - 20, COL_RED);
draw_cart(b.x + 10, b.y + 7, 26);
const char *label = busy ? "Shop wird installiert ..." : shopVersion < 0 ? "MBMNET Shop installieren"
: outdated ? "Shop aktualisieren" : "MBMNET Shop öffnen";
text(label, b.x + 44, b.y + 11, 0.5f, busy ? COL_AMBER : COL_WHITE, 0);
if (shopVersion < 0) return;
b = shopDelBox;
sel = selected == BTN_SHOP_DEL;
round_rect(b.x, b.y, b.w, b.h, 10, sel ? COL_CARD_HI : COL_CARD);
if (sel) C2D_DrawRectSolid(b.x, b.y + 10, 0.5f, 4, b.h - 20, COL_RED);
text("Entfernen", b.x + b.w / 2, b.y + 12, 0.42f, COL_MUTED, C2D_AlignCenter);
}
static void draw_bottom(int selected, PatchState st, bool admin) static void draw_bottom(int selected, PatchState st, bool admin)
{ {
C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG); C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG);
draw_tile(BTN_ENABLE, selected == BTN_ENABLE, st == STATE_MBMNET, admin); draw_tile(BTN_ENABLE, selected == BTN_ENABLE, st == STATE_MBMNET, admin);
draw_tile(BTN_DISABLE, selected == BTN_DISABLE, st == STATE_ORIGINAL, admin); draw_tile(BTN_DISABLE, selected == BTN_DISABLE, st == STATE_ORIGINAL, admin);
if (!admin) return; if (!admin) { draw_shop_bar(selected); return; }
const char *labels[TOOL_COUNT] = { "Diagnose", ftpOn ? "FTP aus" : "FTP an", "ID lösen", "SpotPass", "Neustart" }; const char *labels[TOOL_COUNT] = { "Diagnose", ftpOn ? "FTP aus" : "FTP an", "ID lösen", "SpotPass", "Neustart" };
for (int i = 0; i < TOOL_COUNT; i++) { for (int i = 0; i < TOOL_COUNT; i++) {
@@ -219,6 +270,7 @@ static void draw_bottom(int selected, PatchState st, bool admin)
static bool old_mbmnet_link(void) { FILE *f = fopen(UNLINKED_MARKER, "r"); if (f) fclose(f); return f != NULL; } static bool old_mbmnet_link(void) { FILE *f = fopen(UNLINKED_MARKER, "r"); if (f) fclose(f); return f != NULL; }
static int armed = -1; // Sicherheitsabfrage (zweimal antippen) static int armed = -1; // Sicherheitsabfrage (zweimal antippen)
static bool launchShop; // Sprung in den MBMNET Shop vorbereitet: App beenden
// Name der MBMNET-ID, gemerkt beim Wechsel zu Nintendo. Der 3DS erlaubt dieselbe ID nicht in zwei Konto-Plätzen // 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. // (022-2571): Heißt die Pretendo-ID genauso, muss sie beim Wechsel zurück zu MBMNET dort gelöst werden.
@@ -354,7 +406,7 @@ static bool run(int action, char *msg, size_t msglen)
bool confirmed = armed == action; bool confirmed = armed == action;
armed = -1; armed = -1;
// Während ein Update lädt oder installiert wird, nicht umschalten (der 3DS würde mittendrin neu starten) // Während ein Update lädt oder installiert wird, nicht umschalten (der 3DS würde mittendrin neu starten)
if (action != BTN_DIAG && action != BTN_FTP && action != BTN_SPOTPASS && update_pending()) { if (action != BTN_DIAG && action != BTN_FTP && action != BTN_SPOTPASS && action < BTN_SHOP && update_pending()) {
snprintf(msg, msglen, "Moment: Das Update auf v%s wird gerade installiert.\nDanach startet die App neu.", update_new_version()); snprintf(msg, msglen, "Moment: Das Update auf v%s wird gerade installiert.\nDanach startet die App neu.", update_new_version());
return false; return false;
} }
@@ -386,6 +438,30 @@ static bool run(int action, char *msg, size_t msglen)
if (spotpass_test_start(MBMNET_SERVER, msg, msglen)) spotpassTestFrames = 0; if (spotpass_test_start(MBMNET_SERVER, msg, msglen)) spotpassTestFrames = 0;
return false; return false;
case BTN_REBOOT: return true; case BTN_REBOOT: return true;
case BTN_SHOP:
if (shopctl_job() == SHOPJOB_RUNNING) { snprintf(msg, msglen, "Der Shop wird gerade installiert ..."); return false; }
if (shopVersion >= 0 && shopVersion >= APP_TITLE_VERSION) {
if (shopctl_launch(msg, msglen)) launchShop = true;
return false;
}
if (shopVersion < 0 && !confirmed) {
armed = BTN_SHOP;
snprintf(msg, msglen, "MBMNET Shop installieren?\nEine eigene App mit Homebrew aus der Universal-DB\n"
"(Spiele, Tools, Emulatoren). Zum Bestätigen nochmal antippen.");
return false;
}
shopctl_install(MBMNET_SERVER);
snprintf(msg, msglen, shopVersion < 0 ? "Installiere den MBMNET Shop ..." : "Aktualisiere den MBMNET Shop ...");
return false;
case BTN_SHOP_DEL:
if (shopVersion < 0) return false;
if (!confirmed) {
armed = BTN_SHOP_DEL;
snprintf(msg, msglen, "MBMNET Shop entfernen?\nApps, die du darüber installiert hast, bleiben.\nZum Bestätigen nochmal antippen.");
return false;
}
if (shopctl_remove(msg, msglen)) shopVersion = -1;
return false;
} }
return false; return false;
} }
@@ -396,8 +472,8 @@ int main(int argc, char **argv)
{ {
gfxInitDefault(); gfxInitDefault();
httpcInit(0); httpcInit(0);
// Als installierte CIA: am:net zum Installieren von Updates // am:net: Updates (als CIA) und den MBMNET Shop installieren. Unter Luma auch als .3dsx erlaubt.
bool am = !envIsHomebrew() && R_SUCCEEDED(amInit()); bool am = R_SUCCEEDED(amInit());
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE); C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
C2D_Init(C2D_DEFAULT_MAX_OBJECTS); C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
C2D_Prepare(); C2D_Prepare();
@@ -444,6 +520,9 @@ int main(int argc, char **argv)
} }
// Das Hintergrund-Modul ist abgeschaltet (Kernel-Absturz): Reste wegräumen, nie wieder starten // Das Hintergrund-Modul ist abgeschaltet (Kernel-Absturz): Reste wegräumen, nie wieder starten
ftpctl_cleanup_module(); ftpctl_cleanup_module();
// MBMNET Shop: installiert? Ältere Version gleich im Hintergrund aktualisieren.
shopVersion = am ? shopctl_version() : -1;
if (shopVersion >= 0 && shopVersion < APP_TITLE_VERSION) shopctl_install(server);
ftpOn = ftpctl_running(); ftpOn = ftpctl_running();
int ftpCheckIn = 120; int ftpCheckIn = 120;
@@ -459,22 +538,26 @@ int main(int argc, char **argv)
if (down & KEY_SELECT) toolsToggled = !toolsToggled; if (down & KEY_SELECT) toolsToggled = !toolsToggled;
// Werkzeuge nur mit SELECT, beim Öffnen immer aus // Werkzeuge nur mit SELECT, beim Öffnen immer aus
bool admin = toolsToggled; bool admin = toolsToggled;
if (!admin && selected >= BTN_DIAG) selected = BTN_ENABLE; if (!visible(selected, admin)) selected = admin || selected <= BTN_REBOOT ? BTN_ENABLE : BTN_SHOP;
int action = -1; int action = -1;
// Oben die beiden Kacheln, unten (nur mit Werkzeugen) Diagnose / FTP / ID lösen / SpotPass / Neustart // Oben die beiden Kacheln, unten (nur mit Werkzeugen) Diagnose / FTP / ID lösen / SpotPass / Neustart
bool left = down & (KEY_LEFT | KEY_CPAD_LEFT), right = down & (KEY_RIGHT | KEY_CPAD_RIGHT); bool left = down & (KEY_LEFT | KEY_CPAD_LEFT), right = down & (KEY_RIGHT | KEY_CPAD_RIGHT);
bool up = down & (KEY_UP | KEY_CPAD_UP), dn = down & (KEY_DOWN | KEY_CPAD_DOWN);
if (selected < BTN_DIAG) { if (left || right) selected ^= 1; } if (selected < BTN_DIAG) { if (left || right) selected ^= 1; }
else if (selected >= BTN_SHOP) { if ((left || right) && shopVersion >= 0) selected = selected == BTN_SHOP ? BTN_SHOP_DEL : BTN_SHOP; }
else if (left) selected = selected == BTN_DIAG ? BTN_REBOOT : selected - 1; else if (left) selected = selected == BTN_DIAG ? BTN_REBOOT : selected - 1;
else if (right) selected = selected == BTN_REBOOT ? BTN_DIAG : 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 (dn && selected < BTN_DIAG) selected = admin ? (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; : (selected == BTN_DISABLE && shopVersion >= 0 ? BTN_SHOP_DEL : BTN_SHOP);
else if (up && selected >= BTN_SHOP) selected = selected == BTN_SHOP_DEL ? BTN_DISABLE : BTN_ENABLE;
else if (up && selected >= BTN_DIAG) selected = selected == BTN_REBOOT ? BTN_DISABLE : BTN_ENABLE;
if (down & KEY_A) action = selected; if (down & KEY_A) action = selected;
if (down & KEY_TOUCH) { if (down & KEY_TOUCH) {
touchPosition tp; touchPosition tp;
hidTouchRead(&tp); hidTouchRead(&tp);
for (int i = 0; i < (admin ? BTN_COUNT : BTN_DIAG); i++) for (int i = 0; i < BTN_COUNT; i++)
if (inside(box_of(i, admin), tp)) selected = action = i; if (visible(i, admin) && inside(box_of(i, admin), tp)) selected = action = i;
} }
if (queued >= 0) { if (queued >= 0) {
int a = queued; int a = queued;
@@ -506,6 +589,15 @@ int main(int argc, char **argv)
} }
lastUpd = upd; lastUpd = upd;
// MBMNET Shop fertig installiert (oder fehlgeschlagen)
ShopJob sj = shopctl_job();
if (sj == SHOPJOB_DONE || sj == SHOPJOB_FAILED) {
snprintf(msg, sizeof(msg), "%s", shopctl_message());
shopctl_acknowledge();
shopVersion = shopctl_version();
}
if (launchShop) break;
// SpotPass-Test: jede Sekunde nachsehen // SpotPass-Test: jede Sekunde nachsehen
if (spotpassTestFrames >= 0 && ++spotpassTestFrames % 60 == 0 && if (spotpassTestFrames >= 0 && ++spotpassTestFrames % 60 == 0 &&
!spotpass_test_poll(spotpassTestFrames / 60, msg, sizeof(msg))) !spotpass_test_poll(spotpassTestFrames / 60, msg, sizeof(msg)))
@@ -533,6 +625,7 @@ int main(int argc, char **argv)
} }
ftpctl_stop(NULL, 0); ftpctl_stop(NULL, 0);
shopctl_exit();
admin_finish(); admin_finish();
update_finish(); update_finish();
frdswitch_exit(); frdswitch_exit();
+131
View File
@@ -0,0 +1,131 @@
// MBMNET Shop installieren, starten und entfernen (der Shop selbst steckt in shop/).
// Die CIA kommt vom MBMNET-Server (/app/mbmnet-shop.cia, aus dem Repo MBMNET wie die App-Updates).
#include <3ds.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "shopctl.h"
static volatile ShopJob job = SHOPJOB_IDLE;
static char jobMsg[160];
static char server[64];
static Thread thread;
ShopJob shopctl_job(void) { return job; }
const char *shopctl_message(void) { return jobMsg; }
void shopctl_acknowledge(void) { if (job != SHOPJOB_RUNNING) job = SHOPJOB_IDLE; }
int shopctl_version(void)
{
u64 id = SHOP_TITLE_ID;
AM_TitleEntry info;
if (R_FAILED(AM_GetTitleInfo(MEDIATYPE_SD, 1, &id, &info))) return -1;
return info.version;
}
static void fail(const char *what, Result rc)
{
snprintf(jobMsg, sizeof(jobMsg), "MBMNET Shop: %s (%08lX).", what, (unsigned long)rc);
job = SHOPJOB_FAILED;
}
static void install_worker(void *arg)
{
(void)arg;
char url[128];
snprintf(url, sizeof(url), "http://%s/app/mbmnet-shop.cia", server);
httpcContext ctx;
u32 status = 0;
Result rc = httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1);
if (R_FAILED(rc)) return fail("keine Verbindung", rc);
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
httpcSetKeepAlive(&ctx, HTTPC_KEEPALIVE_DISABLED);
if (R_FAILED(rc = httpcBeginRequest(&ctx)) ||
R_FAILED(rc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 15ULL * 1000 * 1000 * 1000))) {
httpcCloseContext(&ctx);
return fail("keine Verbindung", rc);
}
if (status != 200) {
httpcCloseContext(&ctx);
snprintf(jobMsg, sizeof(jobMsg), "MBMNET Shop: Der Server hat ihn gerade nicht (HTTP %lu).", (unsigned long)status);
job = SHOPJOB_FAILED;
return;
}
// Direkt in die Installation schreiben (wie FBI)
Handle cia;
bool started = false;
u32 offset = 0, got;
u8 *buf = malloc(64 * 1024);
if (!buf) { httpcCloseContext(&ctx); return fail("zu wenig Speicher", -1); }
do {
got = 0;
rc = httpcDownloadData(&ctx, buf, 64 * 1024, &got);
if (!got) continue;
if (!started) {
// Eine CIA beginnt mit der Kopfgröße 0x2020
if (got < 4 || buf[0] != 0x20 || buf[1] != 0x20 || buf[2] || buf[3]) { rc = -1; break; }
Result arc = AM_StartCiaInstall(MEDIATYPE_SD, &cia);
if (R_FAILED(arc)) { rc = arc; break; }
started = true;
}
u32 written = 0;
Result wrc = FSFILE_Write(cia, &written, offset, buf, got, 0);
if (R_FAILED(wrc) || written != got) { rc = R_FAILED(wrc) ? wrc : -1; break; }
offset += got;
} while (rc == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING);
free(buf);
httpcCloseContext(&ctx);
if (R_FAILED(rc) || !started) {
if (started) AM_CancelCIAInstall(cia);
return fail(started ? "Installation abgebrochen" : "Download fehlgeschlagen", rc);
}
if (R_FAILED(rc = AM_FinishCiaInstall(cia))) return fail("Installation abgelehnt", rc);
snprintf(jobMsg, sizeof(jobMsg), "MBMNET Shop installiert!\nTippe unten auf \"Shop öffnen\" oder starte ihn im HOME-Menü.");
job = SHOPJOB_DONE;
}
void shopctl_install(const char *srv)
{
if (job == SHOPJOB_RUNNING) return;
snprintf(server, sizeof(server), "%s", srv);
if (thread) { threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; }
job = SHOPJOB_RUNNING;
jobMsg[0] = 0;
s32 prio = 0x30;
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
thread = threadCreate(install_worker, NULL, 32 * 1024, prio + 1, -2, false);
if (!thread) fail("konnte nicht starten", -1);
}
bool shopctl_remove(char *msg, size_t len)
{
Result rc = AM_DeleteTitle(MEDIATYPE_SD, SHOP_TITLE_ID);
if (R_FAILED(rc) && shopctl_version() >= 0) {
snprintf(msg, len, "Der Shop ließ sich nicht entfernen (%08lX).", (unsigned long)rc);
return false;
}
snprintf(msg, len, "MBMNET Shop entfernt.\nApps, die du darüber installiert hast, bleiben auf dem 3DS.");
return true;
}
bool shopctl_launch(char *msg, size_t len)
{
Result rc = APT_PrepareToDoApplicationJump(0, SHOP_TITLE_ID, MEDIATYPE_SD);
if (R_SUCCEEDED(rc)) {
static u8 param[0x300], hmac[0x20];
rc = APT_DoApplicationJump(param, sizeof(param), hmac);
}
if (R_FAILED(rc)) {
snprintf(msg, len, "Starte den MBMNET Shop bitte im HOME-Menü (%08lX).", (unsigned long)rc);
return false;
}
return true;
}
void shopctl_exit(void)
{
if (thread) { threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; }
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <3ds/types.h>
#include <stdbool.h>
#include <stddef.h>
// Der MBMNET Shop ist eine eigene App (shop/). Die MBMNET-App installiert ihn nur, wenn man das will.
#define SHOP_TITLE_ID 0x000400000FB4E100ULL
typedef enum { SHOPJOB_IDLE, SHOPJOB_RUNNING, SHOPJOB_DONE, SHOPJOB_FAILED } ShopJob;
// Installierte Version (Titelversion) oder -1, wenn der Shop nicht installiert ist
int shopctl_version(void);
// Lädt den Shop vom MBMNET-Server und installiert ihn (im Hintergrund, auch zum Aktualisieren)
void shopctl_install(const char *server);
bool shopctl_remove(char *msg, size_t len);
// Startet den Shop (die MBMNET-App beendet sich danach). false = ging nicht, Grund in msg.
bool shopctl_launch(char *msg, size_t len);
ShopJob shopctl_job(void);
const char *shopctl_message(void);
void shopctl_acknowledge(void);
void shopctl_exit(void);