Initial Commit
This commit is contained in:
24
.github/workflows/build.yml
vendored
Normal file
24
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Build NRO
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: devkitpro/devkita64:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: make
|
||||
|
||||
- name: Upload NRO
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PatchExtractor-nro
|
||||
path: "*.nro"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 NiklasCFW
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
186
Makefile
Normal file
186
Makefile
Normal file
@@ -0,0 +1,186 @@
|
||||
#---------------------------------------------------------------------------------
|
||||
.SUFFIXES:
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
ifeq ($(strip $(DEVKITPRO)),)
|
||||
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro")
|
||||
endif
|
||||
|
||||
TOPDIR ?= $(CURDIR)
|
||||
include $(DEVKITPRO)/libnx/switch_rules
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
APP_TITLE := PatchExtractor
|
||||
APP_AUTHOR := NiklasCFW
|
||||
APP_VERSION := 1.0.0
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
TARGET := $(notdir $(CURDIR))
|
||||
BUILD := build
|
||||
SOURCES := source
|
||||
DATA := data
|
||||
INCLUDES := include
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# options for code generation
|
||||
#---------------------------------------------------------------------------------
|
||||
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
|
||||
|
||||
CFLAGS := -g -Wall -O2 -ffunction-sections \
|
||||
$(ARCH) $(DEFINES)
|
||||
|
||||
CFLAGS += $(INCLUDE) -D__SWITCH__
|
||||
|
||||
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
|
||||
|
||||
ASFLAGS := -g $(ARCH)
|
||||
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
|
||||
|
||||
LIBS := -lminizip -lz -lnx
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
LIBDIRS := $(PORTLIBS) $(LIBNX)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
ifneq ($(BUILD),$(notdir $(CURDIR)))
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
export OUTPUT := $(CURDIR)/$(TARGET)
|
||||
export TOPDIR := $(CURDIR)
|
||||
|
||||
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
|
||||
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
|
||||
|
||||
export DEPSDIR := $(CURDIR)/$(BUILD)
|
||||
|
||||
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
|
||||
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
|
||||
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
|
||||
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
ifeq ($(strip $(CPPFILES)),)
|
||||
#---------------------------------------------------------------------------------
|
||||
export LD := $(CC)
|
||||
#---------------------------------------------------------------------------------
|
||||
else
|
||||
#---------------------------------------------------------------------------------
|
||||
export LD := $(CXX)
|
||||
#---------------------------------------------------------------------------------
|
||||
endif
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
|
||||
export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
|
||||
export OFILES := $(OFILES_BIN) $(OFILES_SRC)
|
||||
export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES)))
|
||||
|
||||
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)
|
||||
|
||||
ifeq ($(strip $(CONFIG_JSON)),)
|
||||
jsons := $(wildcard *.json)
|
||||
ifneq (,$(findstring $(TARGET).json,$(jsons)))
|
||||
export APP_JSON := $(TOPDIR)/$(TARGET).json
|
||||
else
|
||||
ifneq (,$(findstring config.json,$(jsons)))
|
||||
export APP_JSON := $(TOPDIR)/config.json
|
||||
endif
|
||||
endif
|
||||
else
|
||||
export APP_JSON := $(TOPDIR)/$(CONFIG_JSON)
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(ICON)),)
|
||||
icons := $(wildcard *.jpg)
|
||||
ifneq (,$(findstring $(TARGET).jpg,$(icons)))
|
||||
export APP_ICON := $(TOPDIR)/$(TARGET).jpg
|
||||
else
|
||||
ifneq (,$(findstring icon.jpg,$(icons)))
|
||||
export APP_ICON := $(TOPDIR)/icon.jpg
|
||||
endif
|
||||
endif
|
||||
else
|
||||
export APP_ICON := $(TOPDIR)/$(ICON)
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(NO_ICON)),)
|
||||
export NROFLAGS += --icon=$(APP_ICON)
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(NO_NACP)),)
|
||||
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
|
||||
endif
|
||||
|
||||
ifneq ($(APP_TITLEID),)
|
||||
export NACPFLAGS += --titleid=$(APP_TITLEID)
|
||||
endif
|
||||
|
||||
ifneq ($(ROMFS),)
|
||||
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
|
||||
endif
|
||||
|
||||
.PHONY: $(BUILD) clean all
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
all: $(BUILD)
|
||||
|
||||
$(BUILD):
|
||||
@[ -d $@ ] || mkdir -p $@
|
||||
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
clean:
|
||||
@echo clean ...
|
||||
ifeq ($(strip $(APP_JSON)),)
|
||||
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf
|
||||
else
|
||||
@rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf
|
||||
endif
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
else
|
||||
.PHONY: all
|
||||
|
||||
DEPENDS := $(OFILES:.o=.d)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
ifeq ($(strip $(APP_JSON)),)
|
||||
|
||||
all : $(OUTPUT).nro
|
||||
|
||||
ifeq ($(strip $(NO_NACP)),)
|
||||
$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp
|
||||
else
|
||||
$(OUTPUT).nro : $(OUTPUT).elf
|
||||
endif
|
||||
|
||||
else
|
||||
|
||||
all : $(OUTPUT).nsp
|
||||
|
||||
$(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm
|
||||
|
||||
$(OUTPUT).nso : $(OUTPUT).elf
|
||||
|
||||
endif
|
||||
|
||||
$(OUTPUT).elf : $(OFILES)
|
||||
|
||||
$(OFILES_SRC) : $(HFILES_BIN)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
%.bin.o %_bin.h : %.bin
|
||||
#---------------------------------------------------------------------------------
|
||||
@echo $(notdir $<)
|
||||
@$(bin2o)
|
||||
|
||||
-include $(DEPENDS)
|
||||
|
||||
#---------------------------------------------------------------------------------------
|
||||
endif
|
||||
#---------------------------------------------------------------------------------------
|
||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# PatchExtractor
|
||||
|
||||
Nintendo Switch Homebrew App für [OmniNX OC](https://git.niklascfw.de/OmniNX/OmniNX) - entpackt SaltyNX/FPSLocker Patches automatisch auf die SD-Karte.
|
||||
|
||||
## Was macht die App?
|
||||
|
||||
1. Sucht nach `sd:/SaltySD/plugins/FPSLocker/patches.zip`
|
||||
2. Entpackt alle Patches in das gleiche Verzeichnis (überschreibt existierende Dateien)
|
||||
3. Löscht `patches.zip` nach erfolgreichem Entpacken
|
||||
4. Löscht sich selbst (`sd:/switch/PatchExtractor.nro`)
|
||||
|
||||
## Nutzung
|
||||
|
||||
Die App ist bereits in OmniNX OC enthalten und kann direkt aus Sphaira (hbmenu) gestartet werden.
|
||||
|
||||
## Selber bauen
|
||||
|
||||
Benötigt [devkitPro](https://devkitpro.org/) mit libnx, zlib und minizip.
|
||||
|
||||
```bash
|
||||
export DEVKITPRO=/opt/devkitpro
|
||||
export PATH=$DEVKITPRO/devkitA64/bin:$DEVKITPRO/tools/bin:$PATH
|
||||
make
|
||||
```
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
199
source/main.c
Normal file
199
source/main.c
Normal file
@@ -0,0 +1,199 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <switch.h>
|
||||
#include <minizip/unzip.h>
|
||||
|
||||
#define PATCHES_ZIP "sdmc:/SaltySD/plugins/FPSLocker/patches.zip"
|
||||
#define EXTRACT_DIR "sdmc:/SaltySD/plugins/FPSLocker/"
|
||||
#define SELF_NRO "sdmc:/switch/PatchExtractor.nro"
|
||||
#define READ_BUF_SIZE 8192
|
||||
|
||||
#define STATUS_ROW 11
|
||||
|
||||
static PrintConsole *con;
|
||||
|
||||
static void status(const char *fmt, ...) {
|
||||
printf("\x1b[%d;0H\x1b[2K", STATUS_ROW);
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static int mkdirs(const char *path) {
|
||||
char tmp[512];
|
||||
snprintf(tmp, sizeof(tmp), "%s", path);
|
||||
size_t len = strlen(tmp);
|
||||
if (len == 0) return 0;
|
||||
if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
|
||||
|
||||
for (char *p = tmp + 1; *p; p++) {
|
||||
if (*p == '/') {
|
||||
*p = '\0';
|
||||
mkdir(tmp, 0755);
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
return mkdir(tmp, 0755);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
con = consoleInit(NULL);
|
||||
|
||||
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
|
||||
PadState pad;
|
||||
padInitializeDefault(&pad);
|
||||
|
||||
printf("\n");
|
||||
printf(" ========================================\n");
|
||||
printf(" PatchExtractor - OmniNX OC\n");
|
||||
printf(" SaltyNX / FPSLocker Patch Installer\n");
|
||||
printf(" ========================================\n\n");
|
||||
|
||||
unzFile zip = unzOpen(PATCHES_ZIP);
|
||||
if (!zip) {
|
||||
printf(" \x1b[31mFEHLER:\x1b[0m patches.zip nicht gefunden!\n\n");
|
||||
printf(" Pfad: %s\n\n", PATCHES_ZIP);
|
||||
printf(" Bist du sicher, dass du OmniNX OC installiert hast?\n\n");
|
||||
printf(" Die Datei patches.zip muss unter\n");
|
||||
printf(" sd:/SaltySD/plugins/FPSLocker/ liegen.\n\n");
|
||||
printf(" ----------------------------------------\n");
|
||||
printf(" Druecke + zum Beenden.\n");
|
||||
|
||||
while (appletMainLoop()) {
|
||||
padUpdate(&pad);
|
||||
if (padGetButtonsDown(&pad) & HidNpadButton_Plus) break;
|
||||
consoleUpdate(NULL);
|
||||
}
|
||||
consoleExit(NULL);
|
||||
return 1;
|
||||
}
|
||||
|
||||
unz_global_info gi;
|
||||
unzGetGlobalInfo(zip, &gi);
|
||||
unsigned long total = gi.number_entry;
|
||||
printf(" patches.zip gefunden! (%lu Eintraege)\n", total);
|
||||
printf(" Druecke A zum Entpacken, + zum Abbrechen.\n\n");
|
||||
consoleUpdate(NULL);
|
||||
|
||||
while (appletMainLoop()) {
|
||||
padUpdate(&pad);
|
||||
u64 kDown = padGetButtonsDown(&pad);
|
||||
if (kDown & HidNpadButton_A) break;
|
||||
if (kDown & HidNpadButton_Plus) {
|
||||
unzClose(zip);
|
||||
consoleExit(NULL);
|
||||
return 0;
|
||||
}
|
||||
consoleUpdate(NULL);
|
||||
}
|
||||
|
||||
printf("\x1b[10;0H \x1b[93mBitte nicht beenden, bis der Vorgang abgeschlossen ist!\x1b[0m\n");
|
||||
consoleUpdate(NULL);
|
||||
|
||||
char filename[512];
|
||||
char fullpath[1024];
|
||||
unsigned char buf[READ_BUF_SIZE];
|
||||
unsigned long extracted = 0;
|
||||
unsigned long skipped = 0;
|
||||
int err = unzGoToFirstFile(zip);
|
||||
|
||||
while (err == UNZ_OK) {
|
||||
unz_file_info fi;
|
||||
unzGetCurrentFileInfo(zip, &fi, filename, sizeof(filename), NULL, 0, NULL, 0);
|
||||
|
||||
snprintf(fullpath, sizeof(fullpath), "%s%s", EXTRACT_DIR, filename);
|
||||
unsigned long progress = (unsigned long)((extracted * 100) / total);
|
||||
|
||||
size_t flen = strlen(filename);
|
||||
if (flen > 0 && filename[flen - 1] == '/') {
|
||||
mkdirs(fullpath);
|
||||
status(" [%3lu%%] DIR %s", progress, filename);
|
||||
} else {
|
||||
char dirpart[1024];
|
||||
snprintf(dirpart, sizeof(dirpart), "%s", fullpath);
|
||||
char *last_slash = strrchr(dirpart, '/');
|
||||
if (last_slash) {
|
||||
*last_slash = '\0';
|
||||
mkdirs(dirpart);
|
||||
}
|
||||
|
||||
if (unzOpenCurrentFile(zip) != UNZ_OK) {
|
||||
status(" [%3lu%%] \x1b[31mERR\x1b[0m %s", progress, filename);
|
||||
skipped++;
|
||||
extracted++;
|
||||
err = unzGoToNextFile(zip);
|
||||
continue;
|
||||
}
|
||||
|
||||
FILE *out = fopen(fullpath, "wb");
|
||||
if (!out) {
|
||||
status(" [%3lu%%] \x1b[31mERR\x1b[0m %s", progress, filename);
|
||||
unzCloseCurrentFile(zip);
|
||||
skipped++;
|
||||
extracted++;
|
||||
err = unzGoToNextFile(zip);
|
||||
continue;
|
||||
}
|
||||
|
||||
int bytes;
|
||||
while ((bytes = unzReadCurrentFile(zip, buf, READ_BUF_SIZE)) > 0) {
|
||||
fwrite(buf, 1, bytes, out);
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
unzCloseCurrentFile(zip);
|
||||
status(" [%3lu%%] FILE %s", progress, filename);
|
||||
}
|
||||
|
||||
extracted++;
|
||||
consoleUpdate(NULL);
|
||||
err = unzGoToNextFile(zip);
|
||||
}
|
||||
|
||||
unzClose(zip);
|
||||
|
||||
status(" [100%%] Fertig! %lu / %lu entpackt.", extracted - skipped, total);
|
||||
consoleUpdate(NULL);
|
||||
|
||||
int cleanup_ok = 1;
|
||||
|
||||
printf("\x1b[%d;0H", STATUS_ROW + 2);
|
||||
if (remove(PATCHES_ZIP) == 0) {
|
||||
printf(" patches.zip geloescht.\n");
|
||||
} else {
|
||||
printf(" \x1b[31mpatches.zip konnte nicht geloescht werden.\x1b[0m\n");
|
||||
cleanup_ok = 0;
|
||||
}
|
||||
|
||||
if (remove(SELF_NRO) == 0) {
|
||||
printf(" PatchExtractor.nro geloescht (Selbstreinigung).\n");
|
||||
} else {
|
||||
printf(" \x1b[31mPatchExtractor.nro konnte nicht geloescht werden.\x1b[0m\n");
|
||||
cleanup_ok = 0;
|
||||
}
|
||||
|
||||
printf("\n ========================================\n");
|
||||
if (skipped == 0 && cleanup_ok) {
|
||||
printf(" \x1b[32mAlles erledigt!\x1b[0m\n");
|
||||
} else {
|
||||
printf(" \x1b[32mEntpacken abgeschlossen.\x1b[0m\n");
|
||||
if (skipped > 0)
|
||||
printf(" \x1b[31m%lu Eintraege fehlgeschlagen.\x1b[0m\n", skipped);
|
||||
}
|
||||
printf(" ========================================\n\n");
|
||||
printf(" Druecke + zum Beenden.\n");
|
||||
|
||||
while (appletMainLoop()) {
|
||||
padUpdate(&pad);
|
||||
if (padGetButtonsDown(&pad) & HidNpadButton_Plus) break;
|
||||
consoleUpdate(NULL);
|
||||
}
|
||||
|
||||
consoleExit(NULL);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user