diff --git a/tools/rom/.gitignore b/tools/rom/.gitignore new file mode 100644 index 00000000..ee6c2dcd --- /dev/null +++ b/tools/rom/.gitignore @@ -0,0 +1,4 @@ +extractrom +extractrom.exe +buildrom +buildrom.exe diff --git a/tools/rom/Makefile b/tools/rom/Makefile new file mode 100644 index 00000000..30ef4568 --- /dev/null +++ b/tools/rom/Makefile @@ -0,0 +1,28 @@ +CC := gcc +CFLAGS := -g + +ifneq ($(DEBUG),1) + CFLAGS += -O2 -DNDEBUG +endif + +ifeq ($(OS),Windows_NT) + EXTRACTFILE := extractrom.exe + BUILDFILE := buildrom.exe +else + EXTRACTFILE := extractrom + BUILDFILE := buildrom +endif + +.PHONY: all clean + +all: $(EXTRACTFILE) $(BUILDFILE) + +clean: + rm $(EXTRACTFILE) + rm $(BUILDFILE) + +$(EXTRACTFILE): extract.c + $(CC) $(CFLAGS) -o $(EXTRACTFILE) extract.c + +$(BUILDFILE): build.c + $(CC) $(CFLAGS) -o $(BUILDFILE) build.c diff --git a/tools/rom/build.c b/tools/rom/build.c new file mode 100644 index 00000000..95b35e6f --- /dev/null +++ b/tools/rom/build.c @@ -0,0 +1,454 @@ +#include +#include +#include + +#include "rom.h" +#include "ph.h" +#include "util.h" +#include "files.h" + +#define VERSION "1.0" + +#define BUFFER_SIZE 1024 * 1024 +uint8_t *readBuffer = NULL; + +#define MAX_DIR_SIZE 256 +#define INITIAL_SUBTABLE_SIZE 1024 * 1024 + +const uint8_t logo[] = { + 0x24, 0xff, 0xae, 0x51, 0x69, 0x9a, 0xa2, 0x21, 0x3d, 0x84, 0x82, 0x0a, 0x84, 0xe4, 0x09, 0xad, + 0x11, 0x24, 0x8b, 0x98, 0xc0, 0x81, 0x7f, 0x21, 0xa3, 0x52, 0xbe, 0x19, 0x93, 0x09, 0xce, 0x20, + 0x10, 0x46, 0x4a, 0x4a, 0xf8, 0x27, 0x31, 0xec, 0x58, 0xc7, 0xe8, 0x33, 0x82, 0xe3, 0xce, 0xbf, + 0x85, 0xf4, 0xdf, 0x94, 0xce, 0x4b, 0x09, 0xc1, 0x94, 0x56, 0x8a, 0xc0, 0x13, 0x72, 0xa7, 0xfc, + 0x9f, 0x84, 0x4d, 0x73, 0xa3, 0xca, 0x9a, 0x61, 0x58, 0x97, 0xa3, 0x27, 0xfc, 0x03, 0x98, 0x76, + 0x23, 0x1d, 0xc7, 0x61, 0x03, 0x04, 0xae, 0x56, 0xbf, 0x38, 0x84, 0x00, 0x40, 0xa7, 0x0e, 0xfd, + 0xff, 0x52, 0xfe, 0x03, 0x6f, 0x95, 0x30, 0xf1, 0x97, 0xfb, 0xc0, 0x85, 0x60, 0xd6, 0x80, 0x25, + 0xa9, 0x63, 0xbe, 0x03, 0x01, 0x4e, 0x38, 0xe2, 0xf9, 0xa2, 0x34, 0xff, 0xbb, 0x3e, 0x03, 0x44, + 0x78, 0x00, 0x90, 0xcb, 0x88, 0x11, 0x3a, 0x94, 0x65, 0xc0, 0x7c, 0x63, 0x87, 0xf0, 0x3c, 0xaf, + 0xd6, 0x25, 0xe4, 0x8b, 0x38, 0x0a, 0xac, 0x72, 0x21, 0xd4, 0xf8, 0x07, +}; + +uint16_t crcTable[0x100]; +void GenerateCrcTable() { + uint16_t polynomial = 0x2f15; + for (size_t i = 0; i < 0x100; ++i) { + uint16_t value = i; + for (size_t j = 0; j < 8; ++j) { + if (value & 1) value = polynomial ^ (value >> 1); + else value >>= 1; + } + crcTable[i] = value; + } +} + +uint16_t Crc(const void *buf, size_t size) { + uint16_t crc = 0xffff; + const uint8_t *data = buf; + for (size_t i = 0; i < size; ++i) { + crc = crcTable[(crc & 0xff) ^ data[i]] ^ (crc >> 8); + } + return crc; +} + +void InitHeader(Header *pHeader, const BuildInfo *info) { + memcpy(&pHeader->title, TITLE, sizeof(pHeader->title)); + memcpy(&pHeader->gamecode, GAMECODE_PREFIX, 3); + pHeader->gamecode[3] = info->region; + memcpy(&pHeader->makercode, "01", sizeof(pHeader->makercode)); + + pHeader->unitcode = 0; + pHeader->encSeedSelect = 0; + pHeader->capacity = 0; // will be set + memset(&pHeader->reserved0, 0, sizeof(pHeader->reserved0)); + pHeader->dsRegion = 0; + pHeader->romVersion = 0; + pHeader->autostart = 0; + + pHeader->arm9.offset = 0; // will be set after header + pHeader->arm9.entry = 0x2000800; // TODO: Get from linker + pHeader->arm9.baseAddr = 0x2000000; // TODO: Get from linker + pHeader->arm9.size = 0; // TODO: Get from arm9.lz + + pHeader->arm7.offset = 0; // will be 256-aligned after ARM9 overlay files + pHeader->arm7.entry = 0x2380000; + pHeader->arm7.baseAddr = 0x2380000; + pHeader->arm7.size = 0; // TODO: Get from arm7.bin + + pHeader->fileNames.offset = 0; // will be 256-aligned after ARM7 program + pHeader->fileNames.size = 0; // will be set + pHeader->fileAllocs.offset = 0; // will be 256-aligned after file name table + pHeader->fileAllocs.size = 0; // will be set + pHeader->arm9Overlays.offset = 0; // will be 256-aligned after ARM9 program + pHeader->arm9Overlays.size = 0; // will be set + pHeader->arm7Overlays.offset = 0; + pHeader->arm7Overlays.size = 0; + + pHeader->normalCmdSetting = 0x00416657; + pHeader->key1CmdSetting = 0x081808f8; + pHeader->bannerOffset = 0; // will be 256-aligned after file alloc table + pHeader->secureAreaCrc = 0; // TODO: Calculate + pHeader->secureAreaDelay = 0x0d7e; + pHeader->arm9AutoloadList = 0; // TODO: Get from linker (always 2000a74) + pHeader->arm7AutoloadList = 0x2380158; + pHeader->secureAreaDisable = 0; + pHeader->romSize = 0; // Will be set + pHeader->headerSize = sizeof(Header); + pHeader->autoloadParamsOffset = 0; // TODO: Get from linker (always 4b64, see 2000b64) + memset(&pHeader->reserved1, 0, sizeof(pHeader->reserved1)); + pHeader->romEnd = 0; + pHeader->rwEnd = 0; + memset(&pHeader->reserved2, 0, sizeof(pHeader->reserved2)); + memset(&pHeader->reserved3, 0, sizeof(pHeader->reserved3)); + memcpy(&pHeader->logo, logo, sizeof(pHeader->logo)); + pHeader->logoCrc = 0xcf56; + pHeader->headerCrc = 0; // Will be set + pHeader->debugRomOffset = 0; + pHeader->debugSize = 0; + pHeader->debugRamAddr = 0; + memset(&pHeader->reserved4, 0, sizeof(pHeader->reserved4)); + memset(&pHeader->reserved5, 0, sizeof(pHeader->reserved5)); + memset(&pHeader->reserved6, 0, sizeof(pHeader->reserved6)); + memset(&pHeader->reserved7, 0, sizeof(pHeader->reserved7)); +} + +bool AppendFile(FILE *fpRom, const char *filePath, size_t *pAddress, uint32_t *pFileSize) { + assert(readBuffer != NULL); + + FILE *fp = fopen(filePath, "rb"); + if (fp == NULL) FATAL("Failed to open file '%s'\n", filePath); + fseek(fp, 0, SEEK_END); + size_t size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + size_t bytesWritten = 0; + while (bytesWritten < size) { + size_t bytesRead = fread(readBuffer, 1, BUFFER_SIZE, fp); + if (bytesRead == 0) FATAL("Failed to read from file '%s'\n", filePath); + if (fwrite(readBuffer, bytesRead, 1, fpRom) != 1) FATAL("Failed to write file '%s' to output ROM\n", filePath); + bytesWritten += bytesRead; + } + fclose(fp); + + *pAddress += size; + if (pFileSize != NULL) *pFileSize = size; + return true; +} + +bool Align(size_t alignment, FILE *fpRom, size_t *pAddress) { + assert((alignment & (alignment - 1)) == 0); + + size_t mask = alignment - 1; + size_t address = ftell(fpRom); + size_t nextAddr = (address + mask) & ~mask; + while (address < nextAddr) { + if (fputc(0xff, fpRom) == -1) FATAL("Failed to pad output ROM at address 0x%x\n", address); + address += 1; + } + return true; +} + +bool WriteArm9Overlays(FILE *fpRom, size_t *pAddress, size_t *pNumOverlays) { + size_t address = *pAddress; + uint32_t ovNum = 0; + char fileName[32]; + + if (chdir(OVERLAYS_SUBDIR) != 0) FATAL("Failed to enter overlays directory '" OVERLAYS_SUBDIR "'\n"); + + while (true) { + sprintf(fileName, "ov%02d.lz", ovNum); + if (!Align(256, fpRom, &address)) return false; + // TODO (aetias): Store start and end address in FAT + if (!AppendFile(fpRom, fileName, &address, NULL)) return false; + } + + if (chdir("..") != 0) FATAL("Failed to leave overlays directory '" OVERLAYS_SUBDIR "'\n"); + + *pAddress = address; + *pNumOverlays = ovNum; + return true; +} + +typedef struct { + uint16_t tableSize; + uint16_t nextFileId; + uint16_t parentId; + + uint8_t *subtable; + size_t subtableSize; + size_t subtableMax; +} FntContext; + +bool GrowFntSubtable(FntContext *pContext, size_t growSize) { + FntContext ctx; + memcpy(&ctx, pContext, sizeof(ctx)); + + if (ctx.subtableSize + growSize < ctx.subtableMax) return true; + while (ctx.subtableSize + growSize >= ctx.subtableMax) { + ctx.subtableMax *= 2; + } + + uint8_t *newTable = realloc(ctx.subtable, ctx.subtableMax); + if (newTable == NULL) FATAL("Failed to reallocate FNT subtable to %d bytes\n", ctx.subtableMax); + ctx.subtable = newTable; + + memcpy(pContext, &ctx, sizeof(ctx)); + return true; +} + +bool WriteFntSubtable(FILE *fpRom, size_t *pAddress, FntContext *pContext) { + size_t address = *pAddress; + FntSubEntry *entries[MAX_DIR_SIZE]; + size_t numEntries; + + FntContext ctx; + memcpy(&ctx, pContext, sizeof(ctx)); + size_t subtableStart = ctx.subtableSize; + + if (!GetFiles(entries, MAX_DIR_SIZE, &numEntries)) return false; + qsort(entries, numEntries, sizeof(FntSubEntry*), CompareFnt); + + // Create initial subtable entries + size_t numFiles = 0; + for (size_t i = 0; i < numEntries; ++i) { + FntSubEntry *entry = entries[i]; + if (!entry->isSubdir) numFiles += 1; + + size_t entrySize = sizeof(*entry) + entry->length + (entry->isSubdir ? 2 : 0); + if (!GrowFntSubtable(&ctx, entrySize)) return false; + + FntSubEntry *dest = ctx.subtable + ctx.subtableSize; + memcpy(dest, entry, entrySize); + ctx.subtableSize += entrySize; + } + + if (!GrowFntSubtable(&ctx, 1)) return false; + ctx.subtable[ctx.subtableSize] = 0; // End of subtable + ctx.subtableSize += 1; + + // Recurse child directories + for (size_t i = 0; i < numEntries; ++i) { + FntSubEntry *entry = entries[i]; + if (!entry->isSubdir) continue; + uint16_t subdirId = 0xf000 | ctx.tableSize; + WRITE_SUBDIR_ID(entry, subdirId); + FntEntry mainEntry; + mainEntry.subtableOffset = ctx.subtableSize; // will add main table length later + mainEntry.firstFile = ctx.nextFileId; + mainEntry.parentId = ctx.parentId; + if (fwrite(&mainEntry, sizeof(mainEntry), 1, fpRom) != 1) { + FATAL("Failed to write FNT entry for directory '%.*s'\n", entry->length, entry->name); + } + address += sizeof(mainEntry); + + ctx.nextFileId += numFiles; + uint16_t oldParentId = ctx.parentId; + ctx.parentId = subdirId; + + char name[128]; + strncpy(name, entry->name, entry->length); + if (chdir(name) != 0) FATAL("Failed to enter assets subdirectory '%s'\n", name); + if (!WriteFntSubtable(fpRom, &address, &ctx)) return false; + if (chdir("..") != 0) FATAL("Failed to leave assets subdirectory '%s'\n", name); + + ctx.parentId = oldParentId; + } + + // Update subdir IDs + size_t subtableOffset = 0; + for (size_t i = 0; i < numEntries; ++i) { + FntSubEntry *entry = entries[i]; + size_t entrySize = sizeof(*entry) + entry->length + (entry->isSubdir ? 2 : 0); + memcpy(ctx.subtable + subtableStart + subtableOffset, entry, entrySize); + subtableOffset += entrySize; + } + + FreeFiles(entries, numEntries); + + memcpy(pContext, &ctx, sizeof(ctx)); + *pAddress = address; + return true; +} + +bool WriteFnt(FILE *fpRom, size_t *pAddress, size_t firstFileId) { + size_t address = *pAddress; + + FntContext ctx; + ctx.tableSize = 0; + ctx.nextFileId = firstFileId; + ctx.parentId = 0xf000; // root directory + ctx.subtable = malloc(INITIAL_SUBTABLE_SIZE); + if (ctx.subtable == NULL) FATAL(stderr, "Failed to allocate FNT subtable\n"); + ctx.subtableSize = 0; + ctx.subtableMax = INITIAL_SUBTABLE_SIZE; + + if (!WriteFntSubtable(fpRom, &address, &ctx)) return false; + free(ctx.subtable); + + *pAddress = address; + return true; +} + +void PrintUsage(const char *program) { + printf( + "buildrom " VERSION "\n" + "\n" + "Usage: %s -a ASSETSDIR -b BUILDDIR -r REGION -o OUTFILE\n" + " -a ASSETSDIR\tAssets directory generated by extractrom\n" + " -b BUILDDIR \tBuild directory generated by Makefile\n" + " -r REGION \tJ = Japan, E = USA, P = Europe\n" + " -o OUTFILE \tOutput ROM file\n", + program + ); +} + +int main(int argc, const char **argv) { + const char *program = argv[0]; + if (argc == 1) { + PrintUsage(program); + return 0; + } + const char *assetsDir = NULL; + const char *buildDir = NULL; + const char *romFile = NULL; + Region region = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "-o") == 0) { + if (++i >= argc) { + fprintf(stderr, "Expected filename after -o\n"); + return 1; + } + romFile = argv[i]; + } else if (strcmp(argv[i], "-a") == 0) { + if (++i >= argc) { + fprintf(stderr, "Expected pathname after -a\n"); + return 1; + } + assetsDir = argv[i]; + } else if (strcmp(argv[i], "-b") == 0) { + if (++i >= argc) { + fprintf(stderr, "Expected pathname after -b\n"); + return 1; + } + buildDir = argv[i]; + } else if (strcmp(argv[i], "-r") == 0) { + if (++i >= argc) { + fprintf(stderr, "Expected region after -r\n"); + return 1; + } + if (strlen(argv[i]) != 1) { + fprintf(stderr, "Region must be a single character\n"); + return 1; + } + region = argv[i][0]; + } else { + fprintf(stderr, "Unknown option '%s'\n", argv[i]); + return 1; + } + } + if (assetsDir == NULL) { + PrintUsage(program); + fprintf(stderr, "Please provide an assets directory, see usage above\n"); + return 1; + } + if (buildDir == NULL) { + PrintUsage(program); + fprintf(stderr, "Please provide a build directory, see usage above\n"); + return 1; + } + if (region != REGION_JAPAN && region != REGION_USA && region != REGION_EUROPE) { + PrintUsage(program); + fprintf(stderr, "Invalid region '%c', see usage above\n", region); + return 1; + } + if (romFile == NULL) { + PrintUsage(program); + fprintf(stderr, "Please provide an output ROM file, see usage above\n"); + return 1; + } + + char rootDir[256]; + if (getcwd(rootDir, sizeof(rootDir)) == NULL) { + fprintf(stderr, "Failed to get root directory\n"); + return 1; + } + + FILE *fpRom = fopen(romFile, "wb"); + if (fpRom == NULL) { + fprintf(stderr, "Failed to open output ROM file '%s'\n", romFile); + return 1; + } + + readBuffer = malloc(BUFFER_SIZE); + if (readBuffer == NULL) { + fprintf(stderr, "Failed to allocate read buffer to %d bytes\n", BUFFER_SIZE); + return 1; + } + + GenerateCrcTable(); + + BuildInfo info; + info.region = region; + size_t address = 0; + + Header header; + InitHeader(&header, ®ion); + + if (fwrite(&header, sizeof(header), 1, fpRom) != 1) { + fprintf(stderr, "Failed to write NDS header\n"); + return 1; + } + address += sizeof(header); + + if (chdir(buildDir) != 0) { + fprintf(stderr, "Failed to enter build directory '%s'\n", buildDir); + return 1; + } + + if (!Align(256, fpRom, &address)) return 1; + header.arm9.offset = address; + if (!AppendFile(fpRom, ARM9_PROGRAM_FILE, &address, &header.arm9.size)) return 1; + if (!AppendFile(fpRom, ARM9_FOOTER_FILE, &address, NULL)) return 1; + + if (!Align(256, fpRom, &address)) return 1; + header.arm9Overlays.offset = address; + if (!AppendFile(fpRom, ARM9_OVERLAY_TABLE_FILE, &address, &header.arm9Overlays.size)) return 1; + + size_t numOverlays = 0; + if (!WriteArm9Overlays(fpRom, &address, &numOverlays)) return 1; + + if (chdir(rootDir) != 0) { + fprintf(stderr, "Failed to leave build directory '%s'\n", buildDir); + return 1; + } + + if (chdir(assetsDir) != 0) { + fprintf(stderr, "Failed to enter assets directory '%s'\n", assetsDir); + return 1; + } + + if (!Align(256, fpRom, &address)) return 1; + header.arm7.offset = address; + if (!AppendFile(fpRom, ARM7_PROGRAM_FILE, &address, &header.arm7.size)) return 1; + + if (!Align(256, fpRom, &address)) return 1; + header.fileNames.offset = address; + if (!WriteFnt(fpRom, &address, numOverlays)) return 1; + + if (!Align(256, fpRom, &address)) return 1; + header.fileAllocs.offset = address; + // TODO (aetias): Write initial FAT + + // TODO (aetias): Write files + + if (chdir(rootDir) != 0) { + fprintf(stderr, "Failed to leave assets directory '%s'\n", assetsDir); + return 1; + } + + size_t romEnd = 1 << (32 - __builtin_clz(address)); + if (!Align(romEnd, fpRom, &address)) return 1; + + free(readBuffer); + flose(fpRom); + free(rootDir); +} diff --git a/tools/rom/extract.c b/tools/rom/extract.c new file mode 100644 index 00000000..15bbd2f3 --- /dev/null +++ b/tools/rom/extract.c @@ -0,0 +1,227 @@ +#include +#include +#include +#include +#include + +#include "rom.h" +#include "ph.h" +#include "util.h" + +#define VERSION "1.0" + +bool MakeDir(const char *dir) { + struct stat dirStat; + if (stat(dir, &dirStat) != 0) { + if (mkdir(dir, 0777) != 0) FATAL("Failed to make directory '%s'\n", dir); + return true; + } + if (!S_ISDIR(dirStat.st_mode)) FATAL("Could not make directory '%s' due to a file with the same name\n"); + return true; +} + +bool CheckRegion(const Header *pHeader, BuildInfo *pInfo) { + Region region = pHeader->gamecode[3]; + if ( + memcmp(pHeader->gamecode, GAMECODE_PREFIX, 3) != 0 || ( + region != REGION_JAPAN && + region != REGION_USA && + region != REGION_EUROPE + ) + ) { + FATAL("Invalid gamecode prefix '%.4s'\n", pHeader->gamecode); + } + + pInfo->region = region; + return true; +} + +bool ExtractTitle(const char *language, const char *file, const wchar_t *title, size_t titleSize) { + size_t bufSize = 1024; + char *buf = malloc(1024); + if (buf == NULL) FATAL("Failed to allocate UTF-8 buffer for %s banner title\n", language); + + FILE *fp = fopen(file, "wb"); + if (fp == NULL) FATAL("Failed to create %s banner title '%s'\n", language, file); + size_t resultSize = 0; + if (!WcharToUtf8((wchar_t*) title, titleSize, buf, bufSize, &resultSize)) return false; + if (fputs(buf, fp) == -1) FATAL("Failed to write %s banner title '%s'\n", language, file); + fclose(fp); + + free(buf); + return true; +} + +bool ExtractBanner(const Banner *pBanner, const BuildInfo *pInfo) { + if (!MakeDir(BANNER_SUBDIR)) return 1; + + FILE *fp; + + fp = fopen(ICON_BITMAP_FILE, "wb"); + if (fp == NULL) FATAL("Failed to create banner icon bitmap '" ICON_BITMAP_FILE "'\n"); + if (fwrite(pBanner->iconBitmap, sizeof(pBanner->iconBitmap), 1, fp) != 1) { + FATAL("Failed to write banner icon bitmap '" ICON_BITMAP_FILE "'\n"); + } + fclose(fp); + + fp = fopen(ICON_PALETTE_FILE, "wb"); + if (fp == NULL) FATAL("Failed to create banner icon palette '" ICON_PALETTE_FILE "'\n"); + if (fwrite(pBanner->iconPalette, sizeof(pBanner->iconPalette), 1, fp) != 1) { + FATAL("Failed to write banner icon palette '" ICON_PALETTE_FILE "'\n"); + } + fclose(fp); + + if (!ExtractTitle("Japanese", TITLE_JAP_FILE, pBanner->japaneseTitle, sizeof(pBanner->japaneseTitle))) return false; + if (!ExtractTitle("English", TITLE_ENG_FILE, pBanner->englishTitle, sizeof(pBanner->englishTitle))) return false; + if (!ExtractTitle("French", TITLE_FRE_FILE, pBanner->frenchTitle, sizeof(pBanner->frenchTitle))) return false; + if (!ExtractTitle("German", TITLE_GER_FILE, pBanner->germanTitle, sizeof(pBanner->germanTitle))) return false; + if (!ExtractTitle("Italian", TITLE_ITA_FILE, pBanner->italianTitle, sizeof(pBanner->italianTitle))) return false; + if (!ExtractTitle("Spanish", TITLE_SPA_FILE, pBanner->spanishTitle, sizeof(pBanner->spanishTitle))) return false; + + return true; +} + +bool ExtractAssets(const uint8_t *rom, const uint8_t *fatStart, const uint8_t *fntStart, const FntEntry *pFntEntry) { + const uint8_t *subEntryAddr = fntStart + pFntEntry->subtableOffset; + const FntSubEntry *pSubEntry = (const FntSubEntry*) subEntryAddr; + uint16_t fileId = pFntEntry->firstFile; + while(pSubEntry->length > 0) { + char name[128]; + memcpy(name, pSubEntry->name, pSubEntry->length); + name[pSubEntry->length] = '\0'; + + if (!pSubEntry->isSubdir) { + printf("File '%s'\n", name); + + const FatEntry *pFatEntry = (const FatEntry*) fatStart + fileId; + size_t fileSize = pFatEntry->endOffset - pFatEntry->startOffset; + const uint8_t *pFileBytes = rom + pFatEntry->startOffset; + + FILE *fp = fopen(name, "wb"); + if (fp == NULL) FATAL("Failed to open assets file '%s'\n", name); + if (fwrite(pFileBytes, fileSize, 1, fp) != 1) FATAL("Failed to write to assets file '%s'\n", name); + fclose(fp); + + subEntryAddr += sizeof(FntSubEntry) + pSubEntry->length; + pSubEntry = (const FntSubEntry*) subEntryAddr; + ++fileId; + continue; + } + + printf("Dir '%s'\n", name); + if (!MakeDir(name)) return false; + if (chdir(name) != 0) FATAL("Failed to enter assets subdirectory '%s'\n", name); + + uint16_t subdirId = READ16(subEntryAddr + sizeof(FntSubEntry) + pSubEntry->length); + uint16_t subdirIndex = subdirId & 0xfff; + if (!ExtractAssets(rom, fatStart, fntStart, (FntEntry*) fntStart + subdirIndex)) return false; + + printf("Leave '%s'\n", name); + + if (chdir("..") != 0) FATAL("Failed to leave assets subdirectory '%s'\n", name); + subEntryAddr += sizeof(FntSubEntry) + pSubEntry->length + sizeof(subdirId); + pSubEntry = (const FntSubEntry*) subEntryAddr; + } + + return true; +} + +void PrintUsage(const char *program) { + printf( + "extractrom " VERSION "\n" + "\n" + "Usage: %s -i ROMFILE -o OUTDIR\n" + " -o OUTDIR \tDirectory to extract files to\n" + " -i ROMFILE\tROM to extract from", + program + ); +} + +int main(int argc, const char **argv) { + const char *program = argv[0]; + if (argc == 1) { + PrintUsage(program); + return 0; + } + const char *romFile = NULL; + const char *outDir = NULL; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "-o") == 0) { + if (++i >= argc) { + fprintf(stderr, "Expected dirname after -o\n"); + return 1; + } + outDir = argv[i]; + } else if (strcmp(argv[i], "-i") == 0) { + if (++i >= argc) { + fprintf(stderr, "Expected filename after -i\n"); + return 1; + } + romFile = argv[i]; + } else { + fprintf(stderr, "Unknown option '%s'\n", argv[i]); + return 1; + } + } + if (romFile == NULL) { + PrintUsage(program); + fprintf(stderr, "Please provide a ROM file, see usage above.\n"); + return 1; + } + if (outDir == NULL) { + PrintUsage(program); + fprintf(stderr, "Please provide an output directory, see usage above.\n"); + return 1; + } + + FILE *fpRom = fopen(romFile, "rb"); + if (fpRom == NULL) { + fprintf(stderr, "Failed to open input ROM '%s'\n", romFile); + return 1; + } + fseek(fpRom, 0, SEEK_END); + size_t romSize = ftell(fpRom); + fseek(fpRom, 0, SEEK_SET); + uint8_t *rom = malloc(romSize); + if (rom == NULL) { + fprintf(stderr, "Failed to allocate buffer for '%s'\n", romFile); + return 1; + } + if (fread(rom, romSize, 1, fpRom) != 1) { + fprintf(stderr, "Failed to read from '%s'\n", romFile); + return 1; + } + fclose(fpRom); + + Header *pHeader = (Header*) rom; + BuildInfo info; + if (!CheckRegion(pHeader, &info)) return 1; + if (!MakeDir(outDir)) return 1; + if (chdir(outDir) != 0) { + fprintf(stderr, "Failed to enter output directory '%s'\n", outDir); + return 1; + } + + Banner *pBanner = (Banner*) (rom + pHeader->bannerOffset); + if (!ExtractBanner(pBanner, &info)) return 1; + + if (!MakeDir(ASSETS_SUBDIR)) return 1; + if (chdir(ASSETS_SUBDIR) != 0) { + fprintf(stderr, "Failed to enter assets directory '" ASSETS_SUBDIR "'\n"); + return 1; + } + const uint8_t *fntStart = rom + pHeader->fileNames.offset; + const uint8_t *fatStart = rom + pHeader->fileAllocs.offset; + if (!ExtractAssets(rom, fatStart, fntStart, (FntEntry*) fntStart)) return 1; + if (chdir("..") != 0) { + fprintf(stderr, "Failed to leave assets directory '" ASSETS_SUBDIR "'\n"); + return 1; + } + + if (chdir("..") != 0) { + fprintf(stderr, "Failed to leave output directory '%s'\n", outDir); + return 1; + } + + free(rom); +} diff --git a/tools/rom/files.h b/tools/rom/files.h new file mode 100644 index 00000000..a776a8ab --- /dev/null +++ b/tools/rom/files.h @@ -0,0 +1,71 @@ +#ifndef __FILES_H +#define __FILES_H + +#include "util.h" +#include "rom.h" + +bool GetFiles(FntSubEntry **entries, size_t maxLength, size_t *pLength) { +#ifdef _WIN32 + size_t length = 0; + WIN32_FIND_DATA findData; + HANDLE hFind = FindFirstFileA("*", &findData); + if (hFind == INVALID_HANDLE_VALUE) FATAL("Failed to open directory to get files\n"); + do { + if (length >= maxLength) FATAL("Max file entries surpassed\n"); + size_t nameLength = strlen(findData.cFileName); + if (nameLength > 127) FATAL("File name '%s' longer than 127 characters\n", findData.cFileName); + + bool isSubdir = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + size_t extraSize = isSubdir ? 2 : 0; + FntSubEntry *entry = (FntSubEntry*) malloc(sizeof(FntSubEntry) + nameLength + extraSize); + entry->isSubdir = isSubdir; + entry->length = nameLength; + memcpy(entry->name, findData.cFileName, nameLength); + WRITE_SUBDIR_ID(entry, 0); + + entries[length] = entry; + length += 1; + } while (FindNextFileA(hFind, &findData)); + FindClose(hFind); + *pLength = length; + return true; +#elif __linux__ + size_t length = 0; + DIR *dir = opendir("."); + if (dir == NULL) FATAL("Failed to open directory to get files\n"); + struct dirent *dirent; + while ((dirent = readdir(dir)) != NULL) { + if (length >= maxLength) FATAL("Max file entries surpassed\n"); + size_t nameLength = strlen(dirent->d_name); + if (nameLength > 127) FATAL("File name '%s' longer than 127 characters\n", dirent->d_name); + + bool isSubdir = dirent->d_type == DT_DIR; + size_t extraSize = isSubdir ? 2 : 0; + FntSubEntry *entry = (FntSubEntry*) malloc(sizeof(FntSubEntry) + nameLength + extraSize); + entry->isSubdir = isSubdir; + entry->length = nameLength; + memcpy(entry->name, findData.cFileName, nameLength); + + entries[length] = entry; + length += 1; + } + closedir(dir); + *pLength = length; + return true; +#endif +} + +bool FreeFiles(FntSubEntry **pEntries, size_t length) { + for (size_t i = 0; i < length; ++i) free(pEntries[i]); +} + +int CompareFnt(const FntSubEntry *a, const FntSubEntry *b) { + size_t minSize = ((a->length < b->length) ? a : b)->length; + int cmp = strncmp(a->name, b->name, minSize); + if (cmp != 0) return cmp; + if (a->length < b->length) return -1; + if (a->length > b->length) return 1; + return 0; +} + +#endif diff --git a/tools/rom/ph.h b/tools/rom/ph.h new file mode 100644 index 00000000..31dc32af --- /dev/null +++ b/tools/rom/ph.h @@ -0,0 +1,36 @@ +#ifndef __PH_H +#define __PH_H + +#define TITLE "ZELDA_DS:PH" +#define GAMECODE_PREFIX "AZE" + +#define BANNER_SUBDIR "banner" +#define ICON_BITMAP_FILE BANNER_SUBDIR "/icon.bin" +#define ICON_PALETTE_FILE BANNER_SUBDIR "/icon.pal" +#define TITLE_JAP_FILE BANNER_SUBDIR "/title_jap.txt" +#define TITLE_ENG_FILE BANNER_SUBDIR "/title_eng.txt" +#define TITLE_FRE_FILE BANNER_SUBDIR "/title_fre.txt" +#define TITLE_GER_FILE BANNER_SUBDIR "/title_ger.txt" +#define TITLE_ITA_FILE BANNER_SUBDIR "/title_ita.txt" +#define TITLE_SPA_FILE BANNER_SUBDIR "/title_spa.txt" + +#define ASSETS_SUBDIR "assets" + +#define ARM9_PROGRAM_FILE "arm9.lz" +#define ARM9_FOOTER_FILE "footer.bin" +#define ARM9_OVERLAY_TABLE_FILE "arm9_ovt.bin" +#define OVERLAYS_SUBDIR "overlays" + +#define ARM7_PROGRAM_FILE "arm7.bin" + +typedef enum { + REGION_JAPAN = 'J', + REGION_USA = 'E', + REGION_EUROPE = 'P', +} Region; + +typedef struct { + Region region; +} BuildInfo; + +#endif diff --git a/tools/rom/rom.h b/tools/rom/rom.h new file mode 100644 index 00000000..cbeaddef --- /dev/null +++ b/tools/rom/rom.h @@ -0,0 +1,108 @@ +#ifndef __ROM_H +#define __ROM_H + +#include +#include + +typedef struct { + /* 00 */ uint32_t offset; + /* 04 */ uint32_t entry; + /* 08 */ uint32_t baseAddr; + /* 0c */ uint32_t size; + /* 10 */ +} ProgramOffset; + +typedef struct { + /* 0 */ uint32_t offset; + /* 4 */ uint32_t size; + /* 8 */ +} TableOffset; + +typedef struct { + /* 0000 */ char title[0xc]; + /* 000c */ char gamecode[0x4]; + /* 0010 */ char makercode[0x2]; + /* 0012 */ uint8_t unitcode; + /* 0013 */ uint8_t encSeedSelect; + /* 0014 */ uint8_t capacity; + /* 0015 */ uint8_t reserved0[0x8]; + /* 001d */ uint8_t dsRegion; + /* 001e */ uint8_t romVersion; + /* 001f */ uint8_t autostart; + /* 0020 */ ProgramOffset arm9; + /* 0030 */ ProgramOffset arm7; + /* 0040 */ TableOffset fileNames; + /* 0048 */ TableOffset fileAllocs; + /* 0050 */ TableOffset arm9Overlays; + /* 0058 */ TableOffset arm7Overlays; + /* 0060 */ uint32_t normalCmdSetting; + /* 0064 */ uint32_t key1CmdSetting; + /* 0068 */ uint32_t bannerOffset; + /* 006c */ uint16_t secureAreaCrc; + /* 006e */ uint16_t secureAreaDelay; + /* 0070 */ uint32_t arm9AutoloadList; + /* 0074 */ uint32_t arm7AutoloadList; + /* 0078 */ uint64_t secureAreaDisable; + /* 0080 */ uint32_t romSize; + /* 0084 */ uint32_t headerSize; + /* 0088 */ uint32_t autoloadParamsOffset; + /* 008c */ uint8_t reserved1[0x8]; + /* 0094 */ uint32_t romEnd; + /* 0096 */ uint32_t rwEnd; + /* 0098 */ uint8_t reserved2[0x18]; + /* 00b0 */ uint8_t reserved3[0x10]; + /* 00c0 */ uint8_t logo[0x9c]; + /* 015c */ uint16_t logoCrc; + /* 015e */ uint16_t headerCrc; + /* 0160 */ uint32_t debugRomOffset; + /* 0164 */ uint32_t debugSize; + /* 0168 */ uint32_t debugRamAddr; + /* 016c */ uint8_t reserved4[0x4]; + /* 0170 */ uint8_t reserved5[0x90]; + /* 0200 */ uint8_t reserved6[0xe00]; + /* 1000 */ uint8_t reserved7[0x3000]; + /* 4000 */ +} Header; + +typedef struct { + /* 0000 */ uint16_t version; + /* 0002 */ uint16_t crc; + /* 0004 */ uint8_t reserved0[0x1c]; + /* 0020 */ uint8_t iconBitmap[0x200]; + /* 0220 */ uint16_t iconPalette[0x10]; + /* 0240 */ wchar_t japaneseTitle[0x80]; + /* 0340 */ wchar_t englishTitle[0x80]; + /* 0440 */ wchar_t frenchTitle[0x80]; + /* 0440 */ wchar_t germanTitle[0x80]; + /* 0440 */ wchar_t italianTitle[0x80]; + /* 0440 */ wchar_t spanishTitle[0x80]; + /* 0540 */ +} Banner; + +typedef struct { + /* 0 */ uint32_t subtableOffset; + /* 4 */ uint16_t firstFile; + /* 6 */ uint16_t parentId; + /* 8 */ +} FntEntry; + +typedef struct { + /* 0.0 */ uint8_t length : 7; + /* 0.7 */ bool isSubdir : 1; + /* 1.0 */ char name[]; + // If isSubdir + /* 1.0 + length */ // uint16_t subdirId; + /* 1.0 + length + 2 */ + // Else + /* 1.0 + length */ +} FntSubEntry; + +#define READ_SUBDIR_ID(entry) READ16(entry + sizeof(*entry) + entry->length); +#define WRITE_SUBDIR_ID(entry,id) WRITE16(entry + sizeof(*entry) + entry->length, id) + +typedef struct { + /* 0 */ uint32_t startOffset; + /* 4 */ uint32_t endOffset; +} FatEntry; + +#endif diff --git a/tools/rom/util.h b/tools/rom/util.h new file mode 100644 index 00000000..5d60cb4b --- /dev/null +++ b/tools/rom/util.h @@ -0,0 +1,94 @@ +#ifndef __FS_H +#define __FS_H + +#include +#include +#include + +#define FATAL(...) do { fprintf(stderr, __VA_ARGS__); return false; } while (0); + +#define WRITE16(buf,val) do { ((char*) buf)[0] = (val) & 0xFF; ((char*) buf)[1] = ((val) >> 8) & 0xFF; } while (0) +#define WRITE24(buf,val) do { ((char*) buf)[0] = (val) & 0xFF; ((char*) buf)[1] = ((val) >> 8) & 0xFF; ((char*) buf)[2] = ((val) >> 16) & 0xFF; } while (0) +#define WRITE32(buf,val) do { ((char*) buf)[0] = (val) & 0xFF; ((char*) buf)[1] = ((val) >> 8) & 0xFF; ((char*) buf)[2] = ((val) >> 16) & 0xFF; ((char*) buf)[3] = ((val) >> 24) & 0xFF; } while (0) +#define READ16(buf) (((char*) buf)[0] | (((char*) buf)[1] << 8)) +#define READ24(buf) (((char*) buf)[0] | (((char*) buf)[1] << 8) | (((char*) buf)[2] << 16)) +#define READ32(buf) (((char*) buf)[0] | (((char*) buf)[1] << 8) | (((char*) buf)[2] << 16) | (((char*) buf)[3] << 24)) + +#ifdef _WIN32 +# include +# include +# define mkdir(path, mode) mkdir(path) +#elif __linux__ +# include +# include +# include +#else +# error "Target platform not supported" +#endif + +bool WcharToUtf8(wchar_t *in, size_t inSize, char *out, size_t outSize, size_t *pResultSize) { +#ifdef _WIN32 + size_t resultSize = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, in, inSize / sizeof(wchar_t), out, outSize, NULL, NULL); + if (resultSize == 0) FATAL("Failed to convert to UTF-8\n"); + *pResultSize = resultSize; + return true; +#elif __linux__ + iconv_t convDesc = iconv_open("UTF-16", "UTF-8"); + if (convDesc == -1) FATAL("Failed to get conversion description to UTF-8\n"); + size_t remainingBytes = outSize; + if (iconv(convDesc, &in, &inSize, &out, &remainingBytes) == -1) FATAL("Failed to convert to UTF-8\n"); + if (inSize > 0) FATAL("Some characters were not converted to UTF-8\n"); + *pResultSize = outSize - remainingBytes; + return true; +#endif +} + +// typedef struct { +// #ifdef _WIN32 +// WIN32_FIND_DATA findData; +// HANDLE hFind; +// bool done; +// #elif __linux__ +// DIR *dir; +// struct dirent *entry; +// #endif +// } DirContext; + +// bool BeginDir(DirContext *ctx) { +// #ifdef _WIN32 +// ctx->hFind = FindFirstFileA("*", &ctx->findData); +// if (ctx->hFind == INVALID_HANDLE_VALUE) FATAL("Failed to begin walking directory\n"); +// return true; +// #elif __linux__ +// ctx->dir = opendir("."); +// if (ctx->dir == NULL) FATAL("Failed to begin walking directory\n"); +// ctx->entry = readdir(ctx->dir); +// return true; +// #endif +// } + +// bool NextFile(DirContext *ctx, char *pName, size_t nameSize, bool *pIsDir) { +// #ifdef _WIN32 +// if (ctx->done) return false; +// strncpy(pName, ctx->findData.cFileName, nameSize); +// *pIsDir = (ctx->findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; +// if (!FindNextFileA(ctx->hFind, &ctx->findData)) ctx->done = true; +// return true; +// #elif __linux__ +// if (ctx->entry == NULL) return false; +// strncpy(pName, ctx->entry.d_name, nameSize); +// *pIsDir = ctx->entry.d_type == DT_DIR; +// ctx->entry = readdir(ctx->dir); +// return true; +// #endif +// } + +// void EndDir(DirContext *ctx) { +// #ifdef _WIN32 +// FindClose(ctx->hFind); +// #elif __linux__ +// closedir(ctx->dir); +// #endif +// } + +#endif