diff --git a/arm9/src/bgMenu.cpp b/arm9/src/bgMenu.cpp new file mode 100644 index 0000000..44114ce --- /dev/null +++ b/arm9/src/bgMenu.cpp @@ -0,0 +1,75 @@ +#include "bgMenu.h" +#include "main.h" +#include "menu.h" + +#include +#include +#include +#include + +static const auto& getBackgroundList() +{ + static auto bgs = []{ + std::vector> bgs; + static const std::string bgstr{"nitro:/backgrounds/"}; + auto* pdir = opendir("nitro:/backgrounds"); + if(!pdir) return bgs; + dirent* pent; + while((pent = readdir(pdir))) { + if(pent->d_type == DT_DIR) + continue; + std::string name{pent->d_name}; + if(!name.ends_with(".gif") && !name.ends_with(".GIF")) + continue; + bgs.emplace_back(name.substr(0, name.size() - 4), bgstr + name); + } + closedir(pdir); + return bgs; + }(); + return bgs; +} + +const char* backgroundMenu() +{ + //top screen + clearScreen(&topScreen); + + //menu + Menu* m = newMenu(); + setMenuHeader(m, "BACKGROUNDS"); + + const auto& bgs = getBackgroundList(); + + for(const auto& [bgName, bgPath] : bgs) + { + addMenuItem(m, bgName.data(), nullptr, 0); + } + addMenuItem(m, "Default", nullptr, 0); + addMenuItem(m, "Cancel", nullptr, 0); + + m->cursor = 0; + + //bottom screen + printMenu(m); + + while (!programEnd) + { + swiWaitForVBlank(); + scanKeys(); + + if (moveCursor(m)) + printMenu(m); + + if (keysDown() & KEY_A) + break; + } + + const char* result = nullptr; + if(static_cast(m->cursor) < bgs.size()) + result = bgs[m->cursor].second.data(); + else if(static_cast(m->cursor) == bgs.size()) + result = "default"; + freeMenu(m); + + return result; +} \ No newline at end of file diff --git a/arm9/src/bgMenu.h b/arm9/src/bgMenu.h new file mode 100644 index 0000000..1ad40d9 --- /dev/null +++ b/arm9/src/bgMenu.h @@ -0,0 +1,14 @@ +#ifndef BGMENU_H +#define BGMENU_H + +#ifdef __cplusplus +extern "C" { +#endif + +const char* backgroundMenu(); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/arm9/src/main.c b/arm9/src/main.c index 65db340..b43ba5b 100644 --- a/arm9/src/main.c +++ b/arm9/src/main.c @@ -1,3 +1,4 @@ +#include "bgMenu.h" #include "main.h" #include "menu.h" #include "message.h" @@ -17,6 +18,7 @@ static UNLAUNCH_VERSION foundUnlaunchInstallerVersion = INVALID; static bool disableAllPatches = false; static bool enableSoundAndSplash = false; static const char* splashSoundBinaryPatchPath = NULL; +static const char* customBgPath = NULL; bool charging = false; u8 batteryLevel = 0; @@ -25,6 +27,7 @@ PrintConsole bottomScreen; enum { MAIN_MENU_SAFE_UNLAUNCH_UNINSTALL, + MAIN_MENU_CUSTOM_BG, MAIN_MENU_TID_PATCHES, MAIN_MENU_SOUND_SPLASH_PATCHES, MAIN_MENU_SAFE_UNLAUNCH_INSTALL, @@ -69,8 +72,9 @@ static int mainMenu(int cursor) Menu* m = newMenu(); setMenuHeader(m, "MAIN MENU"); - char uninstallStr[32], installStr[32], soundPatchesStr[64], tidPatchesStr[32]; + char uninstallStr[32], installStr[32], soundPatchesStr[64], tidPatchesStr[32], customBgStr[32]; sprintf(uninstallStr, "\x1B[%02omUninstall unlaunch", unlaunchFound ? 047 : 037); + sprintf(customBgStr, "\x1B[%02omCustom background", (foundUnlaunchInstallerVersion != INVALID) ? 047 : 037); sprintf(tidPatchesStr, "\x1B[%02omDisable all patches: %s", (foundUnlaunchInstallerVersion == v1_9 || foundUnlaunchInstallerVersion == v2_0) ? 047 : 037, disableAllPatches ? "On" : "Off"); @@ -79,6 +83,7 @@ static int mainMenu(int cursor) enableSoundAndSplash ? "On" : "Off"); sprintf(installStr, "\x1B[%02omInstall unlaunch", (foundUnlaunchInstallerVersion != INVALID && !unlaunchFound) ? 047 : 037); addMenuItem(m, uninstallStr, NULL, 0); + addMenuItem(m, customBgStr, NULL, true); addMenuItem(m, tidPatchesStr, NULL, 0); addMenuItem(m, soundPatchesStr, NULL, 0); addMenuItem(m, installStr, NULL, 0); @@ -257,6 +262,22 @@ int main(int argc, char **argv) } break; + case MAIN_MENU_CUSTOM_BG: + if(foundUnlaunchInstallerVersion != INVALID) { + const char* customBg = backgroundMenu(); + if(!customBg) + break; + if(strcmp(customBg, "default") == 0) + { + customBgPath = NULL; + } + else + { + customBgPath = customBg; + } + } + break; + case MAIN_MENU_TID_PATCHES: if(foundUnlaunchInstallerVersion == v1_9 || foundUnlaunchInstallerVersion == v2_0) { disableAllPatches = !disableAllPatches; @@ -279,7 +300,8 @@ int main(int argc, char **argv) if(installUnlaunch(retailConsole, retailLauncherTmdPresentAndToBePatched ? retailLauncherTmdPath : NULL, disableAllPatches, - enableSoundAndSplash ? splashSoundBinaryPatchPath : NULL)) + enableSoundAndSplash ? splashSoundBinaryPatchPath : NULL, + customBgPath)) { messageBox("Install successful!\n"); unlaunchFound = true; diff --git a/arm9/src/menu.c b/arm9/src/menu.c index 026fc3e..9a2d422 100644 --- a/arm9/src/menu.c +++ b/arm9/src/menu.c @@ -77,7 +77,7 @@ void sortMenuItems(Menu* m) qsort(m->items, m->itemCount, sizeof(Item), alphabeticalCompare); } -void setMenuHeader(Menu* m, char* str) +void setMenuHeader(Menu* m, const char* str) { if (!m) return; @@ -87,7 +87,7 @@ void setMenuHeader(Menu* m, char* str) return; } - char* strPtr = str; + const char* strPtr = str; if (strlen(strPtr) > 30) strPtr = str + (strlen(strPtr) - 30); diff --git a/arm9/src/menu.h b/arm9/src/menu.h index 2942394..b6cb591 100644 --- a/arm9/src/menu.h +++ b/arm9/src/menu.h @@ -30,7 +30,7 @@ void freeMenu(Menu* m); void addMenuItem(Menu* m, char const* label, char const* value, bool directory); void sortMenuItems(Menu* m); -void setMenuHeader(Menu* m, char* str); +void setMenuHeader(Menu* m, const char* str); void resetMenu(Menu* m); void clearMenu(Menu* m); diff --git a/arm9/src/tonccpy.h b/arm9/src/tonccpy.h new file mode 100644 index 0000000..dd4267d --- /dev/null +++ b/arm9/src/tonccpy.h @@ -0,0 +1,43 @@ +//# Stuff you may not have yet. + +#ifndef TONCCPY_H +#define TONCCPY_H + + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef unsigned int uint; +#define BIT_MASK(len) ( (1<<(len))-1 ) +static inline u32 quad8(u16 x) { x |= x<<8; return x | x<<16; } + + +//# Declarations and inlines. + +void tonccpy(void *dst, const void *src, uint size); + +void __toncset(void *dst, u32 fill, uint size); +static inline void toncset(void *dst, u8 src, uint size); +static inline void toncset16(void *dst, u16 src, uint size); +static inline void toncset32(void *dst, u32 src, uint size); + + +//! VRAM-safe memset, byte version. Size in bytes. +static inline void toncset(void *dst, u8 src, uint size) +{ __toncset(dst, quad8(src), size); } + +//! VRAM-safe memset, halfword version. Size in hwords. +static inline void toncset16(void *dst, u16 src, uint size) +{ __toncset(dst, src|src<<16, size*2); } + +//! VRAM-safe memset, word version. Size in words. +static inline void toncset32(void *dst, u32 src, uint size) +{ __toncset(dst, src, size*4); } + +#ifdef __cplusplus +} +#endif +#endif diff --git a/arm9/src/tonccpy.itcm.c b/arm9/src/tonccpy.itcm.c new file mode 100644 index 0000000..a51437e --- /dev/null +++ b/arm9/src/tonccpy.itcm.c @@ -0,0 +1,136 @@ +#include "tonccpy.h" +//# tonccpy.c + +//! VRAM-safe cpy. +/*! This version mimics memcpy in functionality, with + the benefit of working for VRAM as well. It is also + slightly faster than the original memcpy, but faster + implementations can be made. + \param dst Destination pointer. + \param src Source pointer. + \param size Fill-length in bytes. + \note The pointers and size need not be word-aligned. +*/ +void tonccpy(void *dst, const void *src, uint size) +{ + if(size==0 || dst==0 || src==0) + return; + + uint count; + u16 *dst16; // hword destination + u8 *src8; // byte source + + // Ideal case: copy by 4x words. Leaves tail for later. + if( ((u32)src|(u32)dst)%4==0 && size>=4) + { + u32 *src32= (u32*)src, *dst32= (u32*)dst; + + count= size/4; + uint tmp= count&3; + count /= 4; + + // Duff's Device, good friend! + switch(tmp) { + do { *dst32++ = *src32++; + case 3: *dst32++ = *src32++; + case 2: *dst32++ = *src32++; + case 1: *dst32++ = *src32++; + case 0: ; } while(count--); + } + + // Check for tail + size &= 3; + if(size == 0) + return; + + src8= (u8*)src32; + dst16= (u16*)dst32; + } + else // Unaligned. + { + uint dstOfs= (u32)dst&1; + src8= (u8*)src; + dst16= (u16*)(dst-dstOfs); + + // Head: 1 byte. + if(dstOfs != 0) + { + *dst16= (*dst16 & 0xFF) | *src8++<<8; + dst16++; + if(--size==0) + return; + } + } + + // Unaligned main: copy by 2x byte. + count= size/2; + while(count--) + { + *dst16++ = src8[0] | src8[1]<<8; + src8 += 2; + } + + // Tail: 1 byte. + if(size&1) + *dst16= (*dst16 &~ 0xFF) | *src8; +} +//# toncset.c + +//! VRAM-safe memset, internal routine. +/*! This version mimics memset in functionality, with + the benefit of working for VRAM as well. It is also + slightly faster than the original memset. + \param dst Destination pointer. + \param fill Word to fill with. + \param size Fill-length in bytes. + \note The \a dst pointer and \a size need not be + word-aligned. In the case of unaligned fills, \a fill + will be masked off to match the situation. +*/ +void __toncset(void *dst, u32 fill, uint size) +{ + if(size==0 || dst==0) + return; + + uint left= (u32)dst&3; + u32 *dst32= (u32*)(dst-left); + u32 count, mask; + + // Unaligned head. + if(left != 0) + { + // Adjust for very small stint. + if(left+size<4) + { + mask= BIT_MASK(size*8)<<(left*8); + *dst32= (*dst32 &~ mask) | (fill & mask); + return; + } + + mask= BIT_MASK(left*8); + *dst32= (*dst32 & mask) | (fill&~mask); + dst32++; + size -= 4-left; + } + + // Main stint. + count= size/4; + uint tmp= count&3; + count /= 4; + + switch(tmp) { + do { *dst32++ = fill; + case 3: *dst32++ = fill; + case 2: *dst32++ = fill; + case 1: *dst32++ = fill; + case 0: ; } while(count--); + } + + // Tail + size &= 3; + if(size) + { + mask= BIT_MASK(size*8); + *dst32= (*dst32 &~ mask) | (fill & mask); + } +} diff --git a/arm9/src/unlaunch.cpp b/arm9/src/unlaunch.cpp index 8d99d7f..e90cfa8 100644 --- a/arm9/src/unlaunch.cpp +++ b/arm9/src/unlaunch.cpp @@ -1,15 +1,16 @@ #include "message.h" #include "sha1digest.h" #include "storage.h" +#include "tonccpy.h" #include "unlaunch.h" #include -#include #include #include #include #include static char unlaunchInstallerBuffer[0x30000]; +static char ogUnlaunchInstallerBuffer[0x30000]; static const char* hnaaTmdPath = "nand:/title/00030017/484e4141/content/title.tmd"; static const char* hnaaBackupTmdPath = "nand:/title/00030017/484e4141/content/title.tmd.bak"; @@ -32,6 +33,12 @@ constexpr std::array knownUnlaunchHashes{ "15f4a36251d1408d71114019b2825fe8f5b4c8cc"_sha1, // v2.0 }; +constexpr std::array gifOffsets{ + std::make_pair(0x48d4, 0x8540), /* 1.8 */ + std::make_pair(0x48c8, 0x8534), /* 1.9 */ + std::make_pair(0x48f0, 0x855c), /* 2.0 */ +}; + constexpr std::array blockAllPatchesOffset{ 0xae74, /* 1.9 */ 0xae91, /* 2.0 */ @@ -366,12 +373,12 @@ static bool readUnlaunchInstaller(const char* path) } int toRead = unlaunchInstallerSize; - char* buff = unlaunchInstallerBuffer; + auto* buff = unlaunchInstallerBuffer; // Pad the installer with 520 bytes, those being the size of a valid tmd buff += 520; size_t n = 0; - while (toRead != 0 && (n = fread(buff, sizeof(char), toRead, unlaunchInstaller)) > 0) + while (toRead != 0 && (n = fread(buff, sizeof(uint8_t), toRead, unlaunchInstaller)) > 0) { toRead -= n; buff += n; @@ -402,8 +409,64 @@ static bool verifyUnlaunchInstaller(void) return true; } -static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath) +static bool patchCustomBackground(const char* customBackgroundPath) { + if(!customBackgroundPath) + { + return true; + } + auto bgGif = fopen(customBackgroundPath, "rb"); + if(!bgGif) + { + messageBox("\x1B[31mError:\x1B[33m Failed to open custom bg gif.\n"); + return false; + } + auto size = getFileSize(bgGif); + if(size < 7 || size > 0x3C70) + { + messageBox("\x1B[31mError:\x1B[33m Invalid gif file.\n"); + fclose(bgGif); + return false; + } + u16 gifWidth; + u16 gifHeight; + if((fseek(bgGif, 6, SEEK_SET) != 0) || (fread(&gifWidth, 1, sizeof(u16), bgGif) != sizeof(u16)) || (fread(&gifHeight, 1, sizeof(u16), bgGif) != sizeof(u16))) + { + messageBox("\x1B[31mError:\x1B[33m Failed to parse gif file.\n"); + fclose(bgGif); + return false; + } + if (gifWidth != 256 || gifHeight != 192) { + messageBox("\x1B[31mError:\x1B[33m Gif file has invalid dimensions.\n"); + fclose(bgGif); + return false; + } + const u32 gifSignatureStart = 0x38464947; + const u32 gifSignatureEnd = 0x3B000044; + + auto [gifOffsetStart, gifOffsetEnd] = gifOffsets[installerVersion]; + + auto* gifStart = reinterpret_cast((unlaunchInstallerBuffer + 520) + gifOffsetStart); + auto* gifEnd = reinterpret_cast((unlaunchInstallerBuffer + 520) + gifOffsetEnd); + + if(*gifStart != gifSignatureStart || *gifEnd != gifSignatureEnd) + { + messageBox("\x1B[31mError:\x1B[33m Gif offsets not matching.\n"); + fclose(bgGif); + return false; + } + + fseek(bgGif, 0, SEEK_SET); + + //read the whole file, could be less than 0x3C70, but unlaunch should then just ignore the leftover data + fread(gifStart, 1, 0x3C70, bgGif); + + return true; +} + +static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath) +{ + tonccpy(unlaunchInstallerBuffer, ogUnlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer)); if(disableAllPatches) { if(installerVersion == v1_8) @@ -450,6 +513,10 @@ static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSou } fclose(patch); } + if(!patchCustomBackground(customBackgroundPath)) + { + return false; + } return true; } @@ -457,14 +524,15 @@ UNLAUNCH_VERSION loadUnlaunchInstaller(const char* path) { if(readUnlaunchInstaller(path) && verifyUnlaunchInstaller()) { + tonccpy(ogUnlaunchInstallerBuffer, unlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer)); return installerVersion; } return INVALID; } -bool installUnlaunch(bool retailConsole, const char* retailLauncherTmdPath, bool disableAllPatches, const char* splashSoundBinaryPatchPath) +bool installUnlaunch(bool retailConsole, const char* retailLauncherTmdPath, bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath) { - if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath)) + if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath, customBackgroundPath)) return false; // Treat protos differently diff --git a/arm9/src/unlaunch.h b/arm9/src/unlaunch.h index 3910874..30e8541 100644 --- a/arm9/src/unlaunch.h +++ b/arm9/src/unlaunch.h @@ -14,7 +14,7 @@ typedef enum UNLAUNCH_VERSION { } UNLAUNCH_VERSION; bool uninstallUnlaunch(bool notProto, bool hasHNAABackup, const char* retailLauncherTmdPath); -bool installUnlaunch(bool retailConsole, const char* retailLauncherTmdPath, bool disableAllPatches, const char* splashSoundBinaryPatchPath); +bool installUnlaunch(bool retailConsole, const char* retailLauncherTmdPath, bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath); bool isLauncherTmdPatched(const char* path); diff --git a/nitrofiles/backgrounds/black.gif b/nitrofiles/backgrounds/black.gif new file mode 100644 index 0000000..4c09303 Binary files /dev/null and b/nitrofiles/backgrounds/black.gif differ diff --git a/nitrofiles/backgrounds/desert.gif b/nitrofiles/backgrounds/desert.gif new file mode 100644 index 0000000..9c7f641 Binary files /dev/null and b/nitrofiles/backgrounds/desert.gif differ diff --git a/nitrofiles/backgrounds/desert_gray.gif b/nitrofiles/backgrounds/desert_gray.gif new file mode 100644 index 0000000..e6a7cd4 Binary files /dev/null and b/nitrofiles/backgrounds/desert_gray.gif differ