diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4892c90..f66d999 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,9 +18,14 @@ jobs: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Build - run: make + run: | + patch -d library/borealis -p1 -N < patches/borealis-swkbd-libnx.patch || true + patch -d library/borealis -p1 -N < patches/borealis-applet-frame-hints.patch || true + make - name: Upload NRO uses: actions/upload-artifact@v3 diff --git a/.gitignore b/.gitignore index 66a569b..c0ed8fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +resources/shaders/ *.nro *.nacp *.elf diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1a8224e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "library/borealis"] + path = library/borealis + url = https://github.com/natinusala/borealis.git diff --git a/Makefile b/Makefile index 61cb807..c359254 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,10 @@ SOURCES := source DATA := data INCLUDES := include +ROMFS := resources +BOREALIS_PATH := library/borealis +OUT_SHADERS := shaders + #--------------------------------------------------------------------------------- # options for code generation #--------------------------------------------------------------------------------- @@ -31,7 +35,7 @@ CFLAGS := -g -Wall -O2 -ffunction-sections \ CFLAGS += $(INCLUDE) -D__SWITCH__ -CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +CXXFLAGS := $(CFLAGS) -std=c++1z -O2 -Wno-volatile -include optional ASFLAGS := -g $(ARCH) LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) @@ -41,10 +45,14 @@ LIBS := -lminizip -lz -lnx #--------------------------------------------------------------------------------- LIBDIRS := $(PORTLIBS) $(LIBNX) +include $(TOPDIR)/$(BOREALIS_PATH)/library/borealis.mk + #--------------------------------------------------------------------------------- ifneq ($(BUILD),$(notdir $(CURDIR))) #--------------------------------------------------------------------------------- +.DEFAULT_GOAL := all + export OUTPUT := $(CURDIR)/$(TARGET) export TOPDIR := $(CURDIR) @@ -56,6 +64,7 @@ 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))) +GLSLFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.glsl))) BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) #--------------------------------------------------------------------------------- @@ -75,6 +84,18 @@ 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))) +ifneq ($(strip $(ROMFS)),) + ROMFS_TARGETS := + ROMFS_FOLDERS := + ifneq ($(strip $(OUT_SHADERS)),) + ROMFS_SHADERS := $(ROMFS)/$(OUT_SHADERS) + ROMFS_TARGETS += $(patsubst %.glsl, $(ROMFS_SHADERS)/%.dksh, $(GLSLFILES)) + ROMFS_FOLDERS += $(ROMFS_SHADERS) + endif + + export ROMFS_DEPS := $(foreach file,$(ROMFS_TARGETS),$(CURDIR)/$(file)) +endif + export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ -I$(CURDIR)/$(BUILD) @@ -123,22 +144,63 @@ ifneq ($(ROMFS),) export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) endif -.PHONY: $(BUILD) clean all +.PHONY: $(BUILD) clean all patch-borealis #--------------------------------------------------------------------------------- -all: $(BUILD) +patch-borealis: + @if ! grep -q 'SWKBD_CONFIG_SET_STRING_LEN_MAX_EXT' $(BOREALIS_PATH)/library/lib/platforms/switch/swkbd.cpp 2>/dev/null; then \ + patch -d $(BOREALIS_PATH) -p1 -N -i $(CURDIR)/patches/borealis-swkbd-libnx.patch >/dev/null 2>&1 || true; \ + fi + @if ! grep -q 'footer_hint_right' $(BOREALIS_PATH)/library/lib/views/applet_frame.cpp 2>/dev/null; then \ + patch -d $(BOREALIS_PATH) -p1 -N -i $(CURDIR)/patches/borealis-applet-frame-hints.patch >/dev/null 2>&1 || true; \ + fi -$(BUILD): +all: $(ROMFS_TARGETS) | $(BUILD) + +$(BUILD): patch-borealis @[ -d $@ ] || mkdir -p $@ - @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + @MSYS2_ARG_CONV_EXCL="-D;$(MSYS2_ARG_CONV_EXCL)" $(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +ifneq ($(strip $(ROMFS_TARGETS)),) + +$(ROMFS_TARGETS): | $(ROMFS_FOLDERS) + +$(ROMFS_FOLDERS): + @mkdir -p $@ + +$(ROMFS_SHADERS)/%_vsh.dksh: %_vsh.glsl + @echo {vert} $(notdir $<) + @uam -s vert -o $@ $< + +$(ROMFS_SHADERS)/%_tcsh.dksh: %_tcsh.glsl + @echo {tess_ctrl} $(notdir $<) + @uam -s tess_ctrl -o $@ $< + +$(ROMFS_SHADERS)/%_tesh.dksh: %_tesh.glsl + @echo {tess_eval} $(notdir $<) + @uam -s tess_eval -o $@ $< + +$(ROMFS_SHADERS)/%_gsh.dksh: %_gsh.glsl + @echo {geom} $(notdir $<) + @uam -s geom -o $@ $< + +$(ROMFS_SHADERS)/%_fsh.dksh: %_fsh.glsl + @echo {frag} $(notdir $<) + @uam -s frag -o $@ $< + +$(ROMFS_SHADERS)/%.dksh: %.glsl + @echo {comp} $(notdir $<) + @uam -s comp -o $@ $< + +endif #--------------------------------------------------------------------------------- clean: @echo clean ... ifeq ($(strip $(APP_JSON)),) - @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf + @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nro $(TARGET).nacp $(TARGET).elf else - @rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf + @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf endif @@ -154,9 +216,9 @@ ifeq ($(strip $(APP_JSON)),) all : $(OUTPUT).nro ifeq ($(strip $(NO_NACP)),) -$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp +$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp $(ROMFS_DEPS) else -$(OUTPUT).nro : $(OUTPUT).elf +$(OUTPUT).nro : $(OUTPUT).elf $(ROMFS_DEPS) endif else diff --git a/README.md b/README.md index 742ab09..a8bc5f3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Nintendo Switch Homebrew App für [OmniNX OC](https://git.niklascfw.de/OmniNX/Om 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`) +4. Löscht `sd:/switch/PatchExtractor.nro` beim Beenden (nach `romfsExit`, da eingebettetes ROMFS die Datei sonst offen hält) ## Nutzung @@ -15,14 +15,17 @@ Die App ist bereits in OmniNX OC enthalten und kann direkt aus Sphaira (hbmenu) ## Selber bauen -Benötigt [devkitPro](https://devkitpro.org/) mit libnx, zlib und minizip. +Benötigt [devkitPro](https://devkitpro.org/) mit libnx, zlib, minizip und [borealis](https://github.com/natinusala/borealis) (als Git-Submodul). ```bash +git submodule update --init --recursive export DEVKITPRO=/opt/devkitpro export PATH=$DEVKITPRO/devkitA64/bin:$DEVKITPRO/tools/bin:$PATH make ``` +Die Oberfläche nutzt die Borealis UI-Bibliothek (Switch-Systemdesign). + ## Lizenz MIT diff --git a/library/borealis b/library/borealis new file mode 160000 index 0000000..20e2d33 --- /dev/null +++ b/library/borealis @@ -0,0 +1 @@ +Subproject commit 20e2d33b6c4ffce139ce304c503c04f5b94da920 diff --git a/patches/borealis-applet-frame-hints.patch b/patches/borealis-applet-frame-hints.patch new file mode 100644 index 0000000..83f49cb --- /dev/null +++ b/patches/borealis-applet-frame-hints.patch @@ -0,0 +1,383 @@ +diff --git a/library/include/borealis/views/applet_frame.hpp b/library/include/borealis/views/applet_frame.hpp +index 1402928..beacd1a 100644 +--- a/library/include/borealis/views/applet_frame.hpp ++++ b/library/include/borealis/views/applet_frame.hpp +@@ -19,6 +19,7 @@ + + #include + #include ++#include + #include + #include + +@@ -30,6 +31,7 @@ class AppletFrame : public Box + { + public: + AppletFrame(); ++ ~AppletFrame() override; + + void handleXMLElement(tinyxml2::XMLElement* element) override; + +@@ -48,8 +50,20 @@ class AppletFrame : public Box + static View* create(); + + private: ++ void updateFooterHints(); ++ void clearHintBox(Box* box); ++ void addHintItem(Box* box, const Action& action, float spacingAfter); ++ + BRLS_BIND(Label, title, "brls/applet_frame/title_label"); + BRLS_BIND(Image, icon, "brls/applet_frame/title_icon"); ++ BRLS_BIND(Box, footerHintRight, "brls/applet_frame/footer_hint_right"); ++ BRLS_BIND(Box, footerHintLeft, "brls/applet_frame/footer_hint_left"); ++ ++ GenericEvent::Subscription focusSubscription; ++ VoidEvent::Subscription hintsSubscription; ++ ++ int switchFont = FONT_INVALID; ++ int regularFont = FONT_INVALID; + + protected: + View* contentView = nullptr; +diff --git a/library/include/borealis/views/label.hpp b/library/include/borealis/views/label.hpp +index 3e1ccef..797d328 100644 +--- a/library/include/borealis/views/label.hpp ++++ b/library/include/borealis/views/label.hpp +@@ -85,6 +85,7 @@ class Label : public View + void setVerticalAlign(VerticalAlign align); + + void setFontSize(float value); ++ void setFontFace(int fontFace); + void setLineHeight(float value); + void setTextColor(NVGcolor color); + +diff --git a/library/lib/views/applet_frame.cpp b/library/lib/views/applet_frame.cpp +index f9f47dc..11b8cd6 100644 +--- a/library/lib/views/applet_frame.cpp ++++ b/library/lib/views/applet_frame.cpp +@@ -15,6 +15,11 @@ + limitations under the License. + */ + ++#include ++#include ++ ++#include ++#include + #include + #include + #include +@@ -22,6 +27,73 @@ + namespace brls + { + ++namespace ++{ ++ ++constexpr float HINT_FONT_SIZE = 22.0f; ++constexpr float HINT_SPACING = 30.0f; ++constexpr float HINT_ICON_GAP = 4.0f; ++ ++const char* getButtonIcon(ControllerButton button) ++{ ++ switch (button) ++ { ++ case BUTTON_A: ++ return "\uE0E0"; ++ case BUTTON_B: ++ return "\uE0E1"; ++ case BUTTON_X: ++ return "\uE0E2"; ++ case BUTTON_Y: ++ return "\uE0E3"; ++ case BUTTON_START: ++ return "\uE0EF"; ++ case BUTTON_BACK: ++ return "\uE0F0"; ++ case BUTTON_UP: ++ return "\uE0EB"; ++ case BUTTON_DOWN: ++ return "\uE0EC"; ++ case BUTTON_LEFT: ++ return "\uE0ED"; ++ case BUTTON_RIGHT: ++ return "\uE0EE"; ++ default: ++ return "\uE152"; ++ } ++} ++ ++std::string getButtonHintText(const Action& action) ++{ ++ if (!action.hintText.empty()) ++ return action.hintText; ++ ++ switch (action.button) ++ { ++ case BUTTON_A: ++ return "Ok"; ++ case BUTTON_B: ++ return "Zurück"; ++ case BUTTON_START: ++ return "Beenden"; ++ default: ++ return ""; ++ } ++} ++ ++bool actionsSort(const Action& a, const Action& b) ++{ ++ if (a.button == BUTTON_START) ++ return true; ++ if (b.button == BUTTON_A) ++ return true; ++ if (b.button == BUTTON_B && a.button != BUTTON_A) ++ return true; ++ return false; ++} ++ ++} // namespace ++ + const std::string appletFrameXML = R"xml( + +@@ -61,16 +133,12 @@ const std::string appletFrameXML = R"xml( + + + +- ++ + + +- ++ axis="row" ++ direction="leftToRight" ++ justifyContent="flexStart" ++ alignItems="center" ++ visibility="gone" /> + +- ++ grow="1.0" ++ axis="row" ++ direction="leftToRight" ++ justifyContent="flexEnd" ++ alignItems="center" /> + + + +@@ -109,6 +187,141 @@ AppletFrame::AppletFrame() + }); + + this->forwardXMLAttribute("iconInterpolation", this->icon, "interpolation"); ++ ++ this->switchFont = Application::getFont(FONT_SWITCH_ICONS); ++ this->regularFont = Application::getFont(FONT_REGULAR); ++ ++ this->focusSubscription = Application::getGlobalFocusChangeEvent()->subscribe([this](View*) { ++ this->updateFooterHints(); ++ }); ++ ++ this->hintsSubscription = Application::getGlobalHintsUpdateEvent()->subscribe([this]() { ++ this->updateFooterHints(); ++ }); ++ ++ this->updateFooterHints(); ++} ++ ++AppletFrame::~AppletFrame() ++{ ++ Application::getGlobalFocusChangeEvent()->unsubscribe(this->focusSubscription); ++ Application::getGlobalHintsUpdateEvent()->unsubscribe(this->hintsSubscription); ++} ++ ++void AppletFrame::clearHintBox(Box* box) ++{ ++ if (!box) ++ return; ++ ++ std::vector children = box->getChildren(); ++ while (!children.empty()) ++ { ++ View* child = children.back(); ++ box->removeView(child); ++ children.pop_back(); ++ } ++} ++ ++void AppletFrame::addHintItem(Box* box, const Action& action, float spacingAfter) ++{ ++ const std::string hintText = getButtonHintText(action); ++ if (hintText.empty()) ++ return; ++ ++ Box* item = new Box(Axis::ROW); ++ item->setDirection(Direction::LEFT_TO_RIGHT); ++ item->setAlignItems(AlignItems::CENTER); ++ if (spacingAfter > 0.0f) ++ item->setMarginRight(spacingAfter); ++ ++ Label* icon = new Label(); ++ icon->setText(getButtonIcon(action.button)); ++ icon->setFontSize(HINT_FONT_SIZE); ++ icon->setSingleLine(true); ++ if (this->switchFont != FONT_INVALID) ++ icon->setFontFace(this->switchFont); ++ ++ Label* text = new Label(); ++ text->setText(hintText); ++ text->setFontSize(HINT_FONT_SIZE); ++ text->setSingleLine(true); ++ text->setMarginLeft(HINT_ICON_GAP); ++ if (this->regularFont != FONT_INVALID) ++ text->setFontFace(this->regularFont); ++ ++ item->addView(icon); ++ item->addView(text); ++ box->addView(item); ++} ++ ++void AppletFrame::updateFooterHints() ++{ ++ this->clearHintBox(this->footerHintRight); ++ this->clearHintBox(this->footerHintLeft); ++ ++ std::set addedKeys; ++ std::vector rightActions; ++ std::vector leftActions; ++ ++ View* focusParent = Application::getCurrentFocus(); ++ if (!focusParent) ++ focusParent = this->contentView; ++ ++ while (focusParent) ++ { ++ for (const Action& action : focusParent->getActions()) ++ { ++ if (action.hidden || !action.available) ++ continue; ++ ++ if (addedKeys.find(action.button) != addedKeys.end()) ++ continue; ++ ++ addedKeys.insert(action.button); ++ ++ if (action.button == BUTTON_START) ++ leftActions.push_back(action); ++ else ++ rightActions.push_back(action); ++ } ++ ++ focusParent = focusParent->getParent(); ++ } ++ ++ std::stable_sort(rightActions.begin(), rightActions.end(), actionsSort); ++ std::stable_sort(leftActions.begin(), leftActions.end(), actionsSort); ++ ++ for (size_t i = 0; i < rightActions.size(); i++) ++ { ++ float spacing = (i + 1 < rightActions.size()) ? HINT_SPACING : 0.0f; ++ this->addHintItem(this->footerHintRight, rightActions[i], spacing); ++ } ++ ++ for (size_t i = 0; i < leftActions.size(); i++) ++ { ++ float spacing = (i + 1 < leftActions.size()) ? HINT_SPACING : 0.0f; ++ this->addHintItem(this->footerHintLeft, leftActions[i], spacing); ++ } ++ ++ std::string* commonFooter = Application::getCommonFooter(); ++ if (commonFooter && !commonFooter->empty()) ++ { ++ Label* text = new Label(); ++ text->setText(*commonFooter); ++ text->setFontSize(HINT_FONT_SIZE); ++ text->setSingleLine(true); ++ if (!leftActions.empty()) ++ text->setMarginLeft(HINT_SPACING); ++ if (this->regularFont != FONT_INVALID) ++ text->setFontFace(this->regularFont); ++ this->footerHintLeft->addView(text); ++ } ++ ++ const bool hasRight = !this->footerHintRight->getChildren().empty(); ++ const bool hasLeft = !this->footerHintLeft->getChildren().empty(); ++ ++ this->footerHintRight->setVisibility(hasRight ? Visibility::VISIBLE : Visibility::GONE); ++ this->footerHintLeft->setVisibility(hasLeft ? Visibility::VISIBLE : Visibility::GONE); + } + + void AppletFrame::setIconFromRes(std::string name) +@@ -132,7 +345,6 @@ void AppletFrame::setContentView(View* view) + { + if (this->contentView) + { +- // Remove the node + this->removeView(this->contentView); + this->contentView = nullptr; + } +@@ -146,6 +358,7 @@ void AppletFrame::setContentView(View* view) + this->contentView->setGrow(1.0f); + + this->addView(this->contentView, 1); ++ this->updateFooterHints(); + } + + void AppletFrame::handleXMLElement(tinyxml2::XMLElement* element) +diff --git a/library/lib/views/label.cpp b/library/lib/views/label.cpp +index c31d904..25e4bdf 100644 +--- a/library/lib/views/label.cpp ++++ b/library/lib/views/label.cpp +@@ -316,6 +316,14 @@ void Label::setFontSize(float value) + this->invalidate(); + } + ++void Label::setFontFace(int fontFace) ++{ ++ if (fontFace != FONT_INVALID) ++ this->font = fontFace; ++ ++ this->invalidate(); ++} ++ + void Label::setLineHeight(float value) + { + this->lineHeight = value; diff --git a/patches/borealis-swkbd-libnx.patch b/patches/borealis-swkbd-libnx.patch new file mode 100644 index 0000000..904cfb9 --- /dev/null +++ b/patches/borealis-swkbd-libnx.patch @@ -0,0 +1,12 @@ +--- a/library/lib/platforms/switch/swkbd.cpp ++++ b/library/lib/platforms/switch/swkbd.cpp +@@ -39,7 +39,9 @@ static SwkbdConfig createSwkbdBaseConfig(std::string headerText, std::string sub + swkbdConfigSetSubText(&config, subText.c_str()); + swkbdConfigSetStringLenMax(&config, maxStringLength); + swkbdConfigSetInitialText(&config, initialText.c_str()); ++#if defined(SWKBD_CONFIG_SET_STRING_LEN_MAX_EXT) + swkbdConfigSetStringLenMaxExt(&config, 1); ++#endif + swkbdConfigSetBlurBackground(&config, true); + + return config; diff --git a/resources/i18n/de/brls.json b/resources/i18n/de/brls.json new file mode 100644 index 0000000..d93ac85 --- /dev/null +++ b/resources/i18n/de/brls.json @@ -0,0 +1,15 @@ +{ + "hints": { + "ok": "Ok", + "back": "Zurück", + "exit": "Beenden" + }, + + "crash_frame": { + "button": "Ok" + }, + + "thumbnail_sidebar": { + "save": "Speichern" + } +} diff --git a/resources/i18n/en-US/brls.json b/resources/i18n/en-US/brls.json new file mode 100644 index 0000000..15069d6 --- /dev/null +++ b/resources/i18n/en-US/brls.json @@ -0,0 +1,15 @@ +{ + "hints": { + "ok": "Ok", + "back": "Back", + "exit": "Exit" + }, + + "crash_frame": { + "button": "Ok" + }, + + "thumbnail_sidebar": { + "save": "Save" + } +} diff --git a/resources/img/omninx.png b/resources/img/omninx.png new file mode 100644 index 0000000..e65eca5 Binary files /dev/null and b/resources/img/omninx.png differ diff --git a/resources/inter b/resources/inter new file mode 120000 index 0000000..87e4e67 --- /dev/null +++ b/resources/inter @@ -0,0 +1 @@ +../library/borealis/resources/inter \ No newline at end of file diff --git a/resources/material b/resources/material new file mode 120000 index 0000000..08a2f79 --- /dev/null +++ b/resources/material @@ -0,0 +1 @@ +../library/borealis/resources/material \ No newline at end of file diff --git a/source/extractor.cpp b/source/extractor.cpp new file mode 100644 index 0000000..8aaceab --- /dev/null +++ b/source/extractor.cpp @@ -0,0 +1,134 @@ +#include "extractor.hpp" + +#include +#include +#include + +#ifdef __SWITCH__ +#include +#endif + +static constexpr size_t READ_BUF_SIZE = 8192; + +int PatchExtractor::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); +} + +bool PatchExtractor::open() { + zip = unzOpen(PATCHES_ZIP); + if (!zip) return false; + + unz_global_info gi{}; + if (unzGetGlobalInfo(zip, &gi) != UNZ_OK) { + close(); + return false; + } + + total = gi.number_entry; + extracted = 0; + skipped = 0; + finished = false; + err = unzGoToFirstFile(zip); + return true; +} + +void PatchExtractor::close() { + if (zip) { + unzClose(zip); + zip = nullptr; + } +} + +int PatchExtractor::getProgressPercent() const { + if (total == 0) return 100; + return static_cast((extracted * 100) / total); +} + +bool PatchExtractor::step() { + if (!zip || finished) return false; + + if (err != UNZ_OK) { + finished = true; + return false; + } + + char filename[512]; + char fullpath[1024]; + unz_file_info fi{}; + + unzGetCurrentFileInfo(zip, &fi, filename, sizeof(filename), nullptr, 0, nullptr, 0); + snprintf(fullpath, sizeof(fullpath), "%s%s", EXTRACT_DIR, filename); + currentFile = filename; + + size_t flen = strlen(filename); + if (flen > 0 && filename[flen - 1] == '/') { + mkdirs(fullpath); + } 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) { + FILE* out = fopen(fullpath, "wb"); + if (out) { + unsigned char buf[READ_BUF_SIZE]; + int bytes; + while ((bytes = unzReadCurrentFile(zip, buf, READ_BUF_SIZE)) > 0) { + fwrite(buf, 1, static_cast(bytes), out); + } + fclose(out); + } else { + skipped++; + } + unzCloseCurrentFile(zip); + } else { + skipped++; + } + } + + extracted++; + err = unzGoToNextFile(zip); + if (err != UNZ_OK) finished = true; + + return !finished; +} + +bool PatchExtractor::cleanup() { + cleanupOk_ = true; + + if (remove(PATCHES_ZIP) != 0) + cleanupOk_ = false; + + // Cannot delete our own NRO while the app is still running; try anyway for edge cases. + remove(SELF_NRO); + + remove("sdmc:/switch/.PatchExtractor.nro.star"); + remove("sdmc:/switch/.packages/boot_package.ini"); + + return cleanupOk_; +} + +void PatchExtractor::tryDeleteSelfOnExit() { +#ifdef __SWITCH__ + // Embedded ROMFS keeps the .nro open on SD; unmount before unlink (console build had no ROMFS). + romfsExit(); +#endif + remove(SELF_NRO); +} diff --git a/source/extractor.hpp b/source/extractor.hpp new file mode 100644 index 0000000..ffd6726 --- /dev/null +++ b/source/extractor.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +class PatchExtractor { +public: + static constexpr const char* PATCHES_ZIP = "sdmc:/SaltySD/plugins/FPSLocker/patches.zip"; + static constexpr const char* EXTRACT_DIR = "sdmc:/SaltySD/plugins/FPSLocker/"; + static constexpr const char* SELF_NRO = "sdmc:/switch/PatchExtractor.nro"; + + bool open(); + void close(); + + /** Process one zip entry. Returns false when finished or on fatal error. */ + bool step(); + + bool isOpen() const { return zip != nullptr; } + unsigned long getTotal() const { return total; } + unsigned long getExtracted() const { return extracted; } + unsigned long getSkipped() const { return skipped; } + int getProgressPercent() const; + const std::string& getCurrentFile() const { return currentFile; } + bool isFinished() const { return finished; } + + bool cleanup(); + bool cleanupOk() const { return cleanupOk_; } + + /** Call after the UI has shut down; releases romfs so the NRO file can be unlinked. */ + static void tryDeleteSelfOnExit(); + +private: + static int mkdirs(const char* path); + + unzFile zip = nullptr; + unsigned long total = 0; + unsigned long extracted = 0; + unsigned long skipped = 0; + int err = UNZ_OK; + bool finished = false; + bool cleanupOk_ = true; + std::string currentFile; +}; diff --git a/source/main.c b/source/main.c deleted file mode 100644 index 984e61a..0000000 --- a/source/main.c +++ /dev/null @@ -1,202 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#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; - } - - remove("sdmc:/switch/.PatchExtractor.nro.star"); - remove("sdmc:/switch/.packages/boot_package.ini"); - - 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; -} diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..dfe2d8e --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,25 @@ +#include + +#include "extractor.hpp" +#include "patch_activity.hpp" + +int main(int argc, char* argv[]) { + (void)argc; + (void)argv; + + brls::Logger::setLogLevel(brls::LogLevel::WARNING); + + if (!brls::Application::init()) { + brls::Logger::error("Borealis init failed"); + return 1; + } + + brls::Application::createWindow("PatchExtractor"); + brls::Application::setGlobalQuit(true); + brls::Application::pushActivity(new PatchActivity()); + + while (brls::Application::mainLoop()) {} + + PatchExtractor::tryDeleteSelfOnExit(); + return 0; +} diff --git a/source/patch_activity.cpp b/source/patch_activity.cpp new file mode 100644 index 0000000..e82b0e4 --- /dev/null +++ b/source/patch_activity.cpp @@ -0,0 +1,201 @@ +#include "patch_activity.hpp" + +#include + +PatchActivity::PatchActivity() { + extractor = std::make_unique(); + + extractTimer.setCallback([this]() { onExtractTick(); }); + extractTimer.setPeriod(0); +} + +PatchActivity::~PatchActivity() { + extractTimer.stop(); + if (extractor) extractor->close(); +} + +brls::View* PatchActivity::createContentView() { + frame = new brls::AppletFrame(); + frame->setTitle("PatchExtractor"); + frame->setIconFromRes("img/omninx.png"); + + contentBox = new brls::Box(brls::Axis::COLUMN); + contentBox->setAlignItems(brls::AlignItems::CENTER); + contentBox->setJustifyContent(brls::JustifyContent::CENTER); + contentBox->setMargins(40, 40, 40, 40); + contentBox->setGrow(1.0f); + + titleLabel = new brls::Label(); + titleLabel->setText("PatchExtractor"); + titleLabel->setFontSize(28); + titleLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER); + titleLabel->setMarginBottom(16); + + messageLabel = new brls::Label(); + messageLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER); + messageLabel->setMarginBottom(12); + + detailLabel = new brls::Label(); + detailLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER); + detailLabel->setFontSize(14); + detailLabel->setMarginBottom(24); + detailLabel->setVisibility(brls::Visibility::GONE); + + progressLabel = new brls::Label(); + progressLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER); + progressLabel->setFontSize(16); + progressLabel->setMarginBottom(8); + progressLabel->setVisibility(brls::Visibility::GONE); + + progressTrack = new brls::Box(brls::Axis::ROW); + progressTrack->setWidth(600); + progressTrack->setHeight(12); + progressTrack->setMarginBottom(24); + progressTrack->setVisibility(brls::Visibility::GONE); + + progressFill = new brls::Rectangle(nvgRGB(0, 190, 80)); + progressFill->setHeight(12); + progressFill->setWidth(0); + progressTrack->addView(progressFill); + + actionButton = new brls::Button(); + actionButton->setStyle(&brls::BUTTONSTYLE_PRIMARY); + actionButton->setWidth(320); + actionButton->registerClickAction([this](brls::View* view) { return onExtractClicked(view); }); + + contentBox->addView(titleLabel); + contentBox->addView(messageLabel); + contentBox->addView(detailLabel); + contentBox->addView(progressLabel); + contentBox->addView(progressTrack); + contentBox->addView(actionButton); + + frame->setContentView(contentBox); + return frame; +} + +void PatchActivity::onContentAvailable() { + registerExitAction(brls::BUTTON_START); + + if (extractor->open()) { + char detail[128]; + snprintf(detail, sizeof(detail), "%lu Eintraege in patches.zip", extractor->getTotal()); + detailLabel->setText(detail); + showScreen(Screen::Ready); + } else { + detailLabel->setText(PatchExtractor::PATCHES_ZIP); + detailLabel->setVisibility(brls::Visibility::VISIBLE); + showScreen(Screen::Error); + } +} + +void PatchActivity::showScreen(Screen screen) { + currentScreen = screen; + + progressLabel->setVisibility(brls::Visibility::GONE); + progressTrack->setVisibility(brls::Visibility::GONE); + actionButton->setVisibility(brls::Visibility::VISIBLE); + + switch (screen) { + case Screen::Error: + titleLabel->setText("Fehler"); + messageLabel->setText("patches.zip nicht gefunden!"); + detailLabel->setVisibility(brls::Visibility::VISIBLE); + actionButton->setText("Beenden"); + actionButton->registerClickAction([](brls::View*) { + brls::Application::quit(); + return true; + }); + brls::Application::giveFocus(actionButton); + break; + + case Screen::Ready: + titleLabel->setText("OmniNX OC"); + messageLabel->setText("SaltyNX / FPSLocker Patch Installer"); + detailLabel->setVisibility(brls::Visibility::VISIBLE); + actionButton->setText("Entpacken"); + actionButton->registerClickAction([this](brls::View* view) { return onExtractClicked(view); }); + brls::Application::giveFocus(actionButton); + break; + + case Screen::Extracting: + titleLabel->setText("Entpacken..."); + messageLabel->setText("Bitte nicht beenden, bis der Vorgang abgeschlossen ist!"); + detailLabel->setVisibility(brls::Visibility::VISIBLE); + progressLabel->setVisibility(brls::Visibility::VISIBLE); + progressTrack->setVisibility(brls::Visibility::VISIBLE); + actionButton->setVisibility(brls::Visibility::GONE); + break; + + case Screen::Done: { + titleLabel->setText("Fertig"); + const unsigned long ok = extractor->getExtracted() - extractor->getSkipped(); + char msg[128]; + if (extractor->getSkipped() == 0 && extractor->cleanupOk()) + snprintf(msg, sizeof(msg), "Alles erledigt! %lu / %lu entpackt.", ok, extractor->getTotal()); + else + snprintf(msg, sizeof(msg), "Entpacken abgeschlossen. %lu / %lu entpackt.", ok, extractor->getTotal()); + messageLabel->setText(msg); + + if (extractor->getSkipped() > 0) { + char err[64]; + snprintf(err, sizeof(err), "%lu Eintraege fehlgeschlagen.", extractor->getSkipped()); + detailLabel->setText(err); + detailLabel->setVisibility(brls::Visibility::VISIBLE); + } else if (!extractor->cleanupOk()) { + detailLabel->setText("Aufräumen teilweise fehlgeschlagen."); + detailLabel->setVisibility(brls::Visibility::VISIBLE); + } else { + detailLabel->setVisibility(brls::Visibility::GONE); + } + + actionButton->setText("Beenden"); + actionButton->setVisibility(brls::Visibility::VISIBLE); + actionButton->registerClickAction([](brls::View*) { + brls::Application::quit(); + return true; + }); + brls::Application::unblockInputs(); + brls::Application::giveFocus(actionButton); + break; + } + } +} + +bool PatchActivity::onExtractClicked(brls::View* view) { + (void)view; + if (currentScreen != Screen::Ready) return false; + startExtraction(); + return true; +} + +void PatchActivity::startExtraction() { + showScreen(Screen::Extracting); + brls::Application::blockInputs(); + updateProgressUi(); + extractTimer.start(); +} + +void PatchActivity::onExtractTick() { + if (!extractor->step()) { + extractTimer.stop(); + extractor->close(); + extractor->cleanup(); + updateProgressUi(); + showScreen(Screen::Done); + return; + } + updateProgressUi(); +} + +void PatchActivity::updateProgressUi() { + const int pct = extractor->getProgressPercent(); + progressLabel->setText(std::to_string(pct) + "%"); + + constexpr float trackWidth = 600.0f; + progressFill->setWidth(trackWidth * static_cast(pct) / 100.0f); + + const std::string& file = extractor->getCurrentFile(); + if (!file.empty()) + detailLabel->setText(file); +} diff --git a/source/patch_activity.hpp b/source/patch_activity.hpp new file mode 100644 index 0000000..a2c61d9 --- /dev/null +++ b/source/patch_activity.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "extractor.hpp" + +class PatchActivity : public brls::Activity { +public: + PatchActivity(); + ~PatchActivity() override; + + brls::View* createContentView() override; + void onContentAvailable() override; + +private: + enum class Screen { Error, Ready, Extracting, Done }; + + void showScreen(Screen screen); + void startExtraction(); + void onExtractTick(); + void updateProgressUi(); + bool onExtractClicked(brls::View* view); + + brls::AppletFrame* frame = nullptr; + brls::Box* contentBox = nullptr; + + brls::Label* titleLabel = nullptr; + brls::Label* messageLabel = nullptr; + brls::Label* detailLabel = nullptr; + brls::Label* progressLabel = nullptr; + brls::Box* progressTrack = nullptr; + brls::Rectangle* progressFill = nullptr; + brls::Button* actionButton = nullptr; + + std::unique_ptr extractor; + brls::RepeatingTimer extractTimer; + Screen currentScreen = Screen::Ready; +};