From 765aaa04d5b5058c9f6acc2a08d2079d3472a87a Mon Sep 17 00:00:00 2001 From: MechDragon <> Date: Sun, 29 Aug 2021 19:33:17 -0700 Subject: [PATCH] add BMP support for custom icons --- .gitignore | 5 ++- include/bmp.hpp | 30 ++++++++++++++++ include/builder.hpp | 23 ++---------- include/helpers.hpp | 4 +++ include/nds.h | 2 +- include/settings.hpp | 5 ++- source/bmp.cpp | 84 ++++++++++++++++++++++++++++++++++++++++++++ source/builder.cpp | 29 +++++++++++++-- source/helpers.cpp | 28 ++++++++++++++- source/main.cpp | 1 + 10 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 include/bmp.hpp create mode 100644 source/bmp.cpp diff --git a/.gitignore b/.gitignore index e3b7d00..740d22a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ *.3ds .vscode/ build/ -!romfs \ No newline at end of file +!romfs +core +*.bmp +*.srl1 diff --git a/include/bmp.hpp b/include/bmp.hpp new file mode 100644 index 0000000..f2a19ea --- /dev/null +++ b/include/bmp.hpp @@ -0,0 +1,30 @@ +#pragma once +#include <3ds.h> +#include "nds.h" + +typedef struct __attribute__((__packed__)) { + u32 size; + u32 width; + u32 height; + u16 planes; + u16 bpp; + u32 compression; + u32 imageSizeCompressed; + u32 ppmX; + u32 ppmY; + u32 colors; + u32 importantColors; +} InfoHeader; +typedef struct __attribute__((__packed__)) { + u16 magic; + u32 filesize; + u32 reserved; + u32 dataOffset; + InfoHeader infoHeader; +} BMPHeader; + + +void toDSPixelOrder(u8* store, u8* source); +void convertImage(u8* store, u8* source); +void convertColors(u16 palette[16], u32 BGR888_palette[16]); +Result loadBmpAsIcon(std::string filename, tNDSBanner* banner); \ No newline at end of file diff --git a/include/builder.hpp b/include/builder.hpp index 5de1ee5..fb94b1a 100644 --- a/include/builder.hpp +++ b/include/builder.hpp @@ -5,14 +5,13 @@ #include #include #include -#include -#include +//#include +//#include //#define ROMFS_SRL "romfs:/sdcard.nds" //#define ROMFS_TEMPLATE "romfs:/sdcard.fwd" //#define SDCARD_SRL "sdmc:/3ds/forwarder/sdcard.nds" //#define SDCARD_TEMPLATE "sdmc:/3ds/forwarder/sdcard.fwd" -#define SDCARD_BANNER_PATH "sdmc:/3ds/forwarder/banners" #define NDSV1_HEADER_SIZE 0xA00 #define NDSV2_HEADER_SIZE 0x1240 #define NDSV3_HEADER_SIZE 0x2340 @@ -41,21 +40,3 @@ class Builder { Result buildCIA(std::string filename, bool randomTid=false, std::string customTitle=""); Result installCIA(); }; -inline std::string readEntireFile(const std::string& path) { - std::ostringstream buf; - std::ifstream input (path.c_str()); - buf << input.rdbuf(); - return buf.str(); -} - -inline bool fileExists (const std::string& name) { - struct stat buffer; - return (stat (name.c_str(), &buffer) == 0); -} -inline unsigned long fileSize (const std::string& name) { - struct stat buffer; - if (stat (name.c_str(), &buffer)==0) { - return buffer.st_size; - } - return 0; -} diff --git a/include/helpers.hpp b/include/helpers.hpp index a4cd8ab..383af13 100644 --- a/include/helpers.hpp +++ b/include/helpers.hpp @@ -21,3 +21,7 @@ typedef struct }__attribute__((__packed__)) sSignature; +std::string readEntireFile(const std::string& path); + +bool fileExists (const std::string& name); +unsigned long fileSize (const std::string& name); \ No newline at end of file diff --git a/include/nds.h b/include/nds.h index 6d435cd..5bac634 100644 --- a/include/nds.h +++ b/include/nds.h @@ -1,7 +1,7 @@ /// data in this file was copied from libnds which is part of devkitpro /// https://github.com/devkitPro/libnds /// this header's structs are not my own and i make no claims to it - +#pragma once #include <3ds.h> #include typedef struct sNDSHeader { diff --git a/include/settings.hpp b/include/settings.hpp index 9304bb5..7ea85c4 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -1,10 +1,13 @@ #pragma once -#define VERSION "v1.1.0-beta" +#define VERSION "v1.2.0-beta" +//#define DEBUG 1 #define FORWARDER_DIR std::string("sdmc:/3ds/forwarder") #define SDCARD_TEMPLATE_DIR FORWARDER_DIR+std::string("/templates/") #define ROMFS_TEMPLATE_DIR std::string("romfs:/templates/") +#define SDCARD_BANNER_PATH FORWARDER_DIR+std::string("/banners/") +#define SDCARD_ICON_PATH FORWARDER_DIR+std::string("/icons/") #define ENTRY_HEIGHT 48 #define FILELIST_HEIGHT (240-MENU_HEADING_HEIGHT-MENU_BORDER_HEIGHT) diff --git a/source/bmp.cpp b/source/bmp.cpp new file mode 100644 index 0000000..07c18b4 --- /dev/null +++ b/source/bmp.cpp @@ -0,0 +1,84 @@ +#include <3ds.h> +#include +#include +#include "bmp.hpp" +#include "logger.hpp" + +Logger bmplogger("BmpHandler"); +Result loadBmpAsIcon(std::string filename, tNDSBanner* banner) { + BMPHeader bmpHeader={0}; + std::ifstream f(filename); + if (f.fail()) { + bmplogger.error("Failed to open "+filename); + return -1; + } + f.read((char*)&bmpHeader, sizeof(bmpHeader)); + if (bmpHeader.magic !=0x4D42) { + f.close(); + bmplogger.error(filename + " is not a valid bitmap file"); + bmplogger.error(std::to_string(bmpHeader.magic)); + return -1; + } + if (bmpHeader.infoHeader.width != 32 || bmpHeader.infoHeader.height != 32) { + bmplogger.error(filename + " has invalid width or height. Must be 32x32"); + bmplogger.error(filename + " is " + std::to_string(bmpHeader.infoHeader.width) + "x"+std::to_string(bmpHeader.infoHeader.height)); + return -1; + } + if (bmpHeader.infoHeader.importantColors>16) { + bmplogger.error(filename + " has too many colors."); + return -1; + } + if (bmpHeader.infoHeader.bpp>4) { + bmplogger.error(filename + " is too deep. use 4bit."); + return -1; + } + if (bmpHeader.infoHeader.compression != 0) { + bmplogger.error(filename + " is compressed. Compression is not supported."); + } + u32 BGR888_palette[16]={0}; + u16 palette[16]={0}; + f.read((char*)BGR888_palette,sizeof(u32)*16); + f.seekg(bmpHeader.dataOffset); + u8 BGR888_data[0x200]={0}; + f.read((char*)BGR888_data,0x200); + f.close(); + convertColors(palette, BGR888_palette); + u8 image[0x200]={0}; + convertImage(image,BGR888_data); + memcpy(banner->icon,image,0x200); + memcpy(banner->palette,palette,32); + return 0; +} +void convertColors(u16 palette[16], u32 BGR888_palette[16]) { + for (u8 i=0;i<16;i++) { + u32 color = BGR888_palette[i]; + u16 r = (color >> 19) & 0b11111; + u16 g = (color >> 11) & 0b11111; + u16 b = (color >> 3) & 0b11111; + palette[i] = b<<10|g<<5|r; + } +} +void convertImage(u8* store, u8* source) { + toDSPixelOrder(store, source); +} +void toDSPixelOrder(u8* store, u8* source) { + +// // ds icons are 8x8 pixel boxes, with 4bit width. 4x8 byte boxes +// // they are arranged in a 4 wide by 4 tall pattern of boxes to 32x32 pixels + u8 tmp[0x200]; + int offset = 0; + for (int i=0;i<0x200;i++) { + source[i]=(source[i]<<4)|(source[i]>>4); + } + // start at the bottom set of boxes + for (int yy=3;yy>=0;yy--) { + // start at the bottom row of the bottom set of boxes + for(int y=7;y>=0;y--) { + for (int cc=0;cc<4;cc++) { + memcpy(&tmp[y*4+cc*32+yy*128],source+offset,4); + offset+=4; + } + } + } + memcpy(store,tmp,0x200); +} \ No newline at end of file diff --git a/source/builder.cpp b/source/builder.cpp index 2afcd8a..1d6a250 100644 --- a/source/builder.cpp +++ b/source/builder.cpp @@ -12,6 +12,7 @@ #include "tmd.h" #include "cia.h" #include "ticket.h" +#include "bmp.hpp" #include "settings.hpp" Logger logger("Builder"); @@ -43,7 +44,7 @@ Result Builder::loadTemplate(std::string templateName) { } this->srl = readEntireFile(srlFileName); memcpy(&this->srlBannerLocation,this->srl.c_str() + 0x68,4); - + parseTemplate(srlTemplate); if (this->launchPathLocation == 0 || this->launchPathLen==0) return -1; return 0; @@ -107,17 +108,31 @@ std::string Builder::buildSRL(std::string filename, bool randomTid, std::string char extraTitles[2][0x100] = {0}; const u8 noAnimation[] = {0x01,0x00,0x00,0x01}; std::string customBannerFilename = filename.substr(0,filename.find_last_of('.'))+".bin"; + std::string customIconFilename = filename.substr(0,filename.find_last_of('.'))+".bmp"; + logger.info("looking for banner at "+customBannerFilename); bool customBanner = fileExists(customBannerFilename) && fileSize(customBannerFilename) == 0x23C0; + bool customBMPIcon = fileExists(customIconFilename); if (!customBanner) { - customBannerFilename=filename.substr(filename.find_last_of('/'),filename.find_last_of('.')-filename.find_last_of('/'))+".bin"; + customBannerFilename=filename.substr(filename.find_last_of('/')+1,filename.find_last_of('.')-filename.find_last_of('/')-1)+".bin"; customBannerFilename=SDCARD_BANNER_PATH+customBannerFilename; logger.info("looking for banner at "+customBannerFilename); customBanner = fileExists(customBannerFilename) && fileSize(customBannerFilename) == 0x23C0; } + if (!customBMPIcon) { + customIconFilename=filename.substr(filename.find_last_of('/')+1,filename.find_last_of('.')-filename.find_last_of('/')-1)+".bmp"; + customIconFilename=SDCARD_ICON_PATH+customIconFilename; + logger.info("looking for custom bmp icon at "+customIconFilename); + customBMPIcon = fileExists(customIconFilename); + } std::ifstream f(filename); f.seekg(0); f.read((char*)&header,sizeof(header)); + bool extendedHeader = (header.headerSize == 0x4000); + if (header.bannerOffset == 0) { + logger.error("NDS file contains no banner. This file is not supported."); + return ""; + } f.seekg(header.bannerOffset); f.read((char*)&banner,sizeof(banner)); if ((banner.version & 0xFF) > 1) { @@ -156,6 +171,9 @@ std::string Builder::buildSRL(std::string filename, bool randomTid, std::string f.read(animatedIconData,0x1180); } f.close(); + }else if(customBMPIcon) { + if (R_SUCCEEDED(loadBmpAsIcon(customIconFilename,&banner))) + banner.version &= 3; } //TODO apply nds file to srl std::string dsiware = this->srl; @@ -186,7 +204,8 @@ std::string Builder::buildSRL(std::string filename, bool randomTid, std::string dsiware.replace(0,0x0C,header.gameTitle,0x0C); dsiware.replace(0x0C,0x04,header.gameCode,0x04); char emagCode[] = {header.gameCode[0x03],header.gameCode[0x02],header.gameCode[0x01],header.gameCode[0x00]}; - dsiware.replace(0x230,0x04,emagCode,0x04); + if (extendedHeader) + dsiware.replace(0x230,0x04,emagCode,0x04); dsiware.replace(0x10,0x02,header.makercode,0x02); // Set Banner @@ -248,6 +267,10 @@ Result Builder::buildCIA(std::string filename, bool randomTid, std::string custo PS_GenerateRandomBytes(contentID+1,3); this->sections["content"] = buildSRL(filename, randomTid, customTitle); + if (this->sections["content"].size() == 0) { + logger.error("Failed to create forwarder."); + return -1; + } std::string srlSha = sha256( (u8*)this->sections["content"].c_str(), this->sections["content"].size()); this->sections["ticket"]=buildTicket(filename); this->sections["tmd"]=this->buildTMD(contentID); diff --git a/source/helpers.cpp b/source/helpers.cpp index 991fe39..c3427b5 100644 --- a/source/helpers.cpp +++ b/source/helpers.cpp @@ -5,6 +5,8 @@ #include #include #include "helpers.hpp" +#include + std::string sha256(u8* data, u32 size) { u8 buf[0x20]={0}; @@ -67,4 +69,28 @@ std::string toLowerCase(std::string s) { std::string ret=s; std::transform(ret.begin(), ret.end(), ret.begin(),[](unsigned char c){ return std::tolower(c); }); return ret; -} \ No newline at end of file +} +std::string readEntireFile(const std::string& path) { + FILE * f = fopen(path.c_str(),"rb"); + fseek(f,0,SEEK_END); + size_t s = ftell(f); + fseek(f,0,SEEK_SET); + char *buf=(char*)calloc(1,s); + fread(buf,1,s,f); + fclose(f); + std::string ret(buf,s); + free(buf); + return ret; +} + +bool fileExists (const std::string& name) { + struct stat buffer; + return (stat (name.c_str(), &buffer) == 0); +} +unsigned long fileSize (const std::string& name) { + struct stat buffer; + if (stat (name.c_str(), &buffer)==0) { + return buffer.st_size; + } + return 0; +} diff --git a/source/main.cpp b/source/main.cpp index 91ab83e..d781e62 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -16,6 +16,7 @@ extern "C" { #include "builder.hpp" #include "settings.hpp" #include "config.hpp" +#include "helpers.hpp" namespace fs = std::filesystem; #define SYSTEM_APP_COUNT 6