diff --git a/arm9/source/filesys/vff.c b/arm9/source/filesys/vff.c index 724d42d..f5590d8 100644 --- a/arm9/source/filesys/vff.c +++ b/arm9/source/filesys/vff.c @@ -193,6 +193,44 @@ FRESULT fvx_qwrite (const TCHAR* path, const void* buff, FSIZE_t ofs, UINT btw, return res; } +FRESULT fvx_qcreate (const TCHAR* path, UINT btc) { + FIL fp; + FRESULT res; + + res = fvx_open(&fp, path, FA_WRITE | FA_CREATE_ALWAYS); + if (res != FR_OK) return res; + + res = fvx_lseek(&fp, btc); + fvx_close(&fp); + + return res; +} + +/* // untested / unused, might come in handy at a later point +FRESULT fvx_qfill (const TCHAR* path, const void* buff, UINT btb) { + FIL fp; + FRESULT res; + UINT bwtt = 0; + UINT fsiz = 0; + + res = fvx_open(&fp, path, FA_WRITE | FA_OPEN_EXISTING); + if (res != FR_OK) return res; + + fsiz = fvx_size(&fp); + while (bwtt < fsiz) { + UINT btw = ((fsiz - bwtt) >= btb) ? btb : (fsiz - bwtt); + UINT bwt; + + res = fvx_write(&fp, buff, btw, &bwt); + if ((res == FR_OK) && (bwt != btw)) res = FR_DENIED; + if (res != FR_OK) break; + bwtt += bwt; + } + fvx_close(&fp); + + return res; +}*/ + FSIZE_t fvx_qsize (const TCHAR* path) { FILINFO fno; return (fvx_stat(path, &fno) == FR_OK) ? fno.fsize : 0; diff --git a/arm9/source/filesys/vff.h b/arm9/source/filesys/vff.h index e441aa1..23a6d0b 100644 --- a/arm9/source/filesys/vff.h +++ b/arm9/source/filesys/vff.h @@ -29,9 +29,10 @@ FRESULT fvx_opendir (DIR* dp, const TCHAR* path); FRESULT fvx_closedir (DIR* dp); FRESULT fvx_readdir (DIR* dp, FILINFO* fno); -// additional quick read / write functions +// additional quick read / write / create functions FRESULT fvx_qread (const TCHAR* path, void* buff, FSIZE_t ofs, UINT btr, UINT* br); FRESULT fvx_qwrite (const TCHAR* path, const void* buff, FSIZE_t ofs, UINT btw, UINT* bw); +FRESULT fvx_qcreate (const TCHAR* path, UINT btc); // additional quick file info functions FSIZE_t fvx_qsize (const TCHAR* path); diff --git a/arm9/source/game/nds.c b/arm9/source/game/nds.c index 3ae9d25..a6f3448 100644 --- a/arm9/source/game/nds.c +++ b/arm9/source/game/nds.c @@ -1,4 +1,5 @@ #include "nds.h" +#include "fatmbr.h" #include "vff.h" #include "crc16.h" #include "utf.h" @@ -26,6 +27,66 @@ u32 ValidateTwlHeader(TwlHeader* twl) { return (crc16_quick(twl->logo, sizeof(twl->logo)) == NDS_LOGO_CRC16) ? 0 : 1; } +u32 BuildTwlSaveHeader(void* sav, u32 size) { + const u16 sct_size = 0x200; + if (size / (u32) sct_size > 0xFFFF) + return 1; + + // fit max number of sectors into size + // that's how Nintendo does it ¯\_(ツ)_/¯ + const u16 n_sct_max = size / sct_size; + u16 n_sct = 1; + u16 sct_track = 1; + u16 sct_heads = 1; + while (true) { + if (sct_heads < sct_track) { + u16 n_sct_next = sct_track * (sct_heads+1) * (sct_heads+1); + if (n_sct_next < n_sct_max) { + sct_heads++; + n_sct = n_sct_next; + } else break; + } else { + u16 n_sct_next = (sct_track+1) * sct_heads * sct_heads; + if (n_sct_next < n_sct_max) { + sct_track++; + n_sct = n_sct_next; + } else break; + } + } + + // sectors per cluster (should be identical to Nintendo) + u8 clr_size = (n_sct > 8 * 1024) ? 8 : (n_sct > 1024) ? 4 : 1; + + // how many FAT sectors do we need? + u16 tot_clr = align(n_sct, clr_size) / clr_size; + u32 fat_byte = (align(tot_clr, 2) / 2) * 3; // 2 sectors -> 3 byte + u16 fat_size = align(fat_byte, sct_size) / sct_size; + + // build the FAT header + Fat16Header* fat = sav; + memset(fat, 0x00, sizeof(Fat16Header)); + fat->jmp[0] = 0xE9; // E9 00 00 + memcpy(fat->oemname, "MSWIN4.1", 8); + fat->sct_size = sct_size; // 512 byte / sector + fat->clr_size = clr_size; // sectors per cluster + fat->sct_reserved = 0x0001; // 1 reserved sector + fat->fat_n = 0x02; // 2 FATs + fat->root_n = 0x0020; // 32 root dir entries (2 sectors) + fat->reserved0 = n_sct; // sectors in filesystem + fat->mediatype = 0xF8; // "hard disk" + fat->fat_size = fat_size; // sectors per fat (1 sector) + fat->sct_track = sct_track; // sectors per track (legacy? see above) + fat->sct_heads = sct_heads; // sectors per head (legacy? see above) + fat->ndrive = 0x05; // for whatever reason + fat->boot_sig = 0x29; // "boot signature" + fat->vol_id = 0x12345678; // volume id + memcpy(fat->vol_label, "VOLUMELABEL", 11); // standard volume label + memcpy(fat->fs_type, "FAT12 ", 8); // filesystem type + fat->magic = 0xAA55; + + return 0; +} + u32 LoadTwlMetaData(const char* path, TwlHeader* hdr, TwlIconData* icon) { u8 ALIGN(32) ntr_header[0x200]; // we only need the NTR header (ignore TWL stuff) TwlHeader* twl = hdr ? hdr : (void*) ntr_header; diff --git a/arm9/source/game/tie.c b/arm9/source/game/tie.c index cd53c66..f081db9 100644 --- a/arm9/source/game/tie.c +++ b/arm9/source/game/tie.c @@ -22,21 +22,32 @@ u32 BuildTitleInfoEntryTmd(TitleInfoEntry* tie, TitleMetaData* tmd, bool sd) { // align size: 0x4000 for TWL and CTRNAND, 0x8000 for SD u32 align_size = CMD_SIZE_ALIGN(sd); u32 content_count = getbe16(tmd->content_count); - TmdContentChunk* chunk = (TmdContentChunk*) (tmd + 1); tie->title_size = (align_size * 3) + // base folder + 'content' + 'cmd' align(TMD_SIZE_N(content_count), align_size) + // TMD align_size; // CMD, placeholder (!!!) + if (getle32(tmd->save_size) || getle32(tmd->twl_privsave_size) || (tmd->twl_flag & 0x2)) { + tie->title_size += + align_size + // data folder + align(getle32(tmd->save_size), align_size) + + align(getle32(tmd->twl_privsave_size), align_size) + + ((tmd->twl_flag & 0x2) ? align(sizeof(TwlIconData), align_size) : 0); + } + + // contents title size + some additional stuff + TmdContentChunk* chunk = (TmdContentChunk*) (tmd + 1); + tie->content0_id = getbe32(chunk->id); for (u32 i = 0; (i < content_count) && (i < TMD_MAX_CONTENTS); i++, chunk++) { if (getbe16(chunk->index) == 1) has_idx1 = true; // will be useful later else if (getbe16(chunk->index) == 2) has_idx2 = true; // will be useful later tie->title_size += align(getbe64(chunk->size), align_size); } - // manual? dlp? (we need to properly check this later) + // manual? dlp? save? (we need to properly check this later) if (((title_id >> 32) == 0x00040000) || ((title_id >> 32) == 0x00040010)) { if (has_idx1) tie->flags_0[0] = 0x1; // this may have a manual - if (has_idx2) tie->title_version |= (0xFFFF << 16); // this may have dlp + if (has_idx2) tie->title_version |= (0xFFFF << 16); // this may have a dlp + if (getle32(tmd->save_size)) tie->flags_1[0] = 0x01; // this may have an sd save } return 0; @@ -62,7 +73,7 @@ u32 BuildTitleInfoEntryTwl(TitleInfoEntry* tie, TitleMetaData* tmd, TwlHeader* t tie->flags_2[0] = 0x01; tie->flags_2[4] = 0x01; tie->flags_2[5] = 0x01; - } + } else tie->content0_id = 0; return 0; } @@ -76,6 +87,9 @@ u32 BuildTitleInfoEntryNcch(TitleInfoEntry* tie, TitleMetaData* tmd, NcchHeader* // product code, extended title version memcpy(tie->product_code, ncch->productcode, 0x10); tie->title_version &= ((ncch->version << 16) | 0xFFFF); + + // NCCH titles need no content0 ID + tie->content0_id = 0; // specific flags // see: http://3dbrew.org/wiki/Titles @@ -84,18 +98,11 @@ u32 BuildTitleInfoEntryNcch(TitleInfoEntry* tie, TitleMetaData* tmd, NcchHeader* // stuff from extheader if (exthdr) { - // add save data size to title size - if (exthdr->savedata_size) { - u32 align_size = CMD_SIZE_ALIGN(sd); - tie->title_size += - align_size + // 'data' folder - align(exthdr->savedata_size, align_size); // savegame - tie->flags_1[0] = 0x01; // has SD save - }; // extdata ID low (hacky, we navigate to storage info) tie->extdata_id_low = getle32(exthdr->aci_data + (0x30 - 0x0C)); } else { tie->flags_0[0] = 0x00; // no manual + tie->flags_1[0] = 0x00; // no sd save tie->title_version &= 0xFFFF; // no dlp } diff --git a/arm9/source/game/tie.h b/arm9/source/game/tie.h index f6de4dc..14beb34 100644 --- a/arm9/source/game/tie.h +++ b/arm9/source/game/tie.h @@ -22,7 +22,8 @@ typedef struct { u8 reserved1[4]; u8 flags_2[8]; char product_code[16]; - u8 reserved2[16]; + u8 reserved2[12]; + u32 content0_id; // only relevant for TWL? u8 unknown[4]; // appears to not matter what's here u8 reserved3[44]; } __attribute__((packed)) TitleInfoEntry; diff --git a/arm9/source/utils/gameutil.c b/arm9/source/utils/gameutil.c index cca29ad..79fe1a5 100644 --- a/arm9/source/utils/gameutil.c +++ b/arm9/source/utils/gameutil.c @@ -1332,8 +1332,12 @@ u32 GetInstallPath(char* path, const char* drv, u64 tid64, const u8* content_id, return 0; } -u32 GetInstallSavePath(char* path, const char* drv, u64 tid64) { +u32 CreateSaveData(const char* drv, u64 tid64, const char* name, u32 save_size, bool overwrite) { + bool is_twl = ((*drv == '2') || (*drv == '5')); + char path_save[128]; + // generate the save path (thanks ihaveamac for system path) + // we use hardcoded names / numbers for CTR saves if ((*drv == '1') || (*drv == '4')) { // ooof, system save // get the id0 u8 sd_keyy[16] __attribute__((aligned(4))); @@ -1345,13 +1349,48 @@ u32 GetInstallSavePath(char* path, const char* drv, u64 tid64) { sha_quick(sha256sum, sd_keyy, 0x10, SHA256_MODE); // build path u32 tid_low = (u32) (tid64 & 0xFFFFFFFF); - snprintf(path, 128, "%2.2s/data/%08lx%08lx%08lx%08lx/sysdata/%08lx/00000000", + snprintf(path_save, 128, "%2.2s/data/%08lx%08lx%08lx%08lx/sysdata/%08lx%s", drv, sha256sum[0], sha256sum[1], sha256sum[2], sha256sum[3], - tid_low | 0x00020000); + tid_low | 0x00020000, name ? "/00000000" : ""); return 0; - } else { // SD save, simple - return GetInstallPath(path, drv, tid64, NULL, "data/00000001.sav"); + } else if (!is_twl || !name) { // SD CTR save or no name, simple + GetInstallPath(path_save, drv, tid64, NULL, name ? "data/00000001.sav" : "data"); + } else { + char substr[64]; + snprintf(substr, 64, "data/%s", name); + GetInstallPath(path_save, drv, tid64, NULL, substr); } + + // if name is NULL, we remove instead of create + if (!name) { + fvx_runlink(path_save); + return 0; + } + + // generate the save file, first check if it already exists + if (overwrite || (fvx_qsize(path_save) != save_size)) { + fvx_rmkpath(path_save); + if (fvx_qcreate(path_save, save_size) != FR_OK) return 1; + + if (!is_twl) { // CTR save, simple case + static const u8 zeroes[0x20] = { 0x00 }; + if (fvx_qwrite(path_save, zeroes, 0, 0x20, NULL) != FR_OK) + return 1; + } else if ((strncmp(name, "public.sav", 11) == 0) || // fat12 image + (strncmp(name, "private.sav", 12) == 0)) { + u8* fat16k = (u8*) malloc(0x4000); // 16kiB, that's enough + if (!fat16k) return 1; + memset(fat16k, 0x00, 0x4000); + + if ((BuildTwlSaveHeader(fat16k, save_size) != 0) || + (fvx_qwrite(path_save, fat16k, 0, min(save_size, 0x4000), NULL) != FR_OK)) { + free(fat16k); + return 1; + } + } + } + + return 0; } u32 UninstallGameData(u64 tid64, bool remove_tie, bool remove_ticket, bool remove_save, bool from_emunand) { @@ -1520,6 +1559,7 @@ u32 InstallCiaSystemData(CiaStub* cia, const char* drv) { TmdContentChunk* content_list = cia->content_list; u32 content_count = getbe16(tmd->content_count); u8* title_id = ticket->title_id; + u64 tid64 = getbe64(title_id); bool sdtie = ((*drv == 'A') || (*drv == 'B')); bool syscmd = (((*drv == '1') || (*drv == '4')) || @@ -1541,7 +1581,7 @@ u32 InstallCiaSystemData(CiaStub* cia, const char* drv) { u8 hdr_cnt0[0x600]; // we don't need more NcchHeader* ncch = NULL; NcchExtHeader* exthdr = NULL; - GetInstallPath(path_cnt0, drv, getbe64(title_id), content_list->id, NULL); + GetInstallPath(path_cnt0, drv, tid64, content_list->id, NULL); if (fvx_qread(path_cnt0, hdr_cnt0, 0, 0x600, NULL) != FR_OK) return 1; if (ValidateNcchHeader((void*) hdr_cnt0) == 0) { @@ -1568,8 +1608,8 @@ u32 InstallCiaSystemData(CiaStub* cia, const char* drv) { snprintf(path_ticketdb, 32, "%2.2s/dbs/ticket.db", ((*drv == 'A') || (*drv == '2')) ? "1:" : ((*drv == 'B') || (*drv == '5')) ? "4:" : drv); - GetInstallPath(path_tmd, drv, getbe64(title_id), NULL, "content/00000000.tmd"); - GetInstallPath(path_cmd, drv, getbe64(title_id), NULL, "content/cmd/00000001.cmd"); + GetInstallPath(path_tmd, drv, tid64, NULL, "content/00000000.tmd"); + GetInstallPath(path_cmd, drv, tid64, NULL, "content/cmd/00000001.cmd"); // copy tmd & cmd fvx_rmkpath(path_tmd); @@ -1582,28 +1622,20 @@ u32 InstallCiaSystemData(CiaStub* cia, const char* drv) { free(cmd); // we don't need this anymore // generate savedata - if (exthdr && (exthdr->savedata_size)) { - char path_save[128]; - - // generate the path - GetInstallSavePath(path_save, drv, getbe64(title_id)); - - // generate the save file, first check if it already exists - if (fvx_qsize(path_save) != exthdr->savedata_size) { - static const u8 zeroes[0x20] = { 0x00 }; - UINT bw; - FIL save; - fvx_rmkpath(path_save); - if (fvx_open(&save, path_save, FA_WRITE | FA_CREATE_ALWAYS) != FR_OK) - return 1; - if ((fvx_write(&save, zeroes, 0x20, &bw) != FR_OK) || (bw != 0x20)) - bw = 0; - fvx_lseek(&save, exthdr->savedata_size); - fvx_sync(&save); - fvx_close(&save); - if (bw != 0x20) return 1; - } - } + u32 save_size = getle32(tmd->save_size); + u32 twl_privsave_size = getle32(tmd->twl_privsave_size); + if (exthdr && save_size && // NCCH + (CreateSaveData(drv, tid64, "*", save_size, false) != 0)) + return 1; + if (!ncch && save_size && // TWL public.sav + (CreateSaveData(drv, tid64, "public.sav", save_size, false) != 0)) + return 1; + if (!ncch && twl_privsave_size && // TWL private.sav + (CreateSaveData(drv, tid64, "private.sav", twl_privsave_size, false) != 0)) + return 1; + if ((tmd->twl_flag & 0x2) && // TWL banner.sav + (CreateSaveData(drv, tid64, "banner.sav", sizeof(TwlIconData), false) != 0)) + return 1; // write ticket and title databases // ensure remounting the old mount path