From 761de83ee0fb734ef7fbbb5fb259306c5f8fbd4c Mon Sep 17 00:00:00 2001 From: msiewert76 Date: Fri, 20 May 2011 19:19:12 +0000 Subject: [PATCH] - restore missing ini file template (long story short? don't use TUGzip to compress SVN folders!) - add "hacking.txt", which gives some hints where to start hacking - rework significant sections of the display subsystem - update Readme file - finish implementation of translation subsystem; also add template file for translation and German strings file --- HACKING.txt | 30 +++++ arm9/source/auxspi.cpp | 2 +- arm9/source/auxspi.h | 2 + arm9/source/display.cpp | 219 +++++++++++++++++++++++++++---------- arm9/source/display.h | 14 +-- arm9/source/fileselect.cpp | 8 +- arm9/source/gba.cpp | 51 +++++---- arm9/source/gba.h | 4 +- arm9/source/hardware.cpp | 170 ++++++++++++++-------------- arm9/source/main.cpp | 45 ++++---- arm9/source/strings.cpp | 105 +++++++++--------- arm9/source/strings.h | 107 ++++++++++-------- lang/sgm_german.ini | 95 ++++++++++++++++ lang/template.ini | 95 ++++++++++++++++ savegame_manager.ini | 7 ++ savegame_manager.odt | Bin 26552 -> 26949 bytes 16 files changed, 660 insertions(+), 294 deletions(-) create mode 100644 HACKING.txt create mode 100644 lang/sgm_german.ini create mode 100644 lang/template.ini create mode 100644 savegame_manager.ini diff --git a/HACKING.txt b/HACKING.txt new file mode 100644 index 0000000..6df6090 --- /dev/null +++ b/HACKING.txt @@ -0,0 +1,30 @@ +This file gives an overiew how the code is organised; if you want to start hacking, I hope this helps you to find the target file faster. + +Custom ARM7 binary: + +The program uses a (non-)custom ARM7 binary. Right now, this is a straight copy of default_arm7 from devkitpro; it exists anyway because it allows to implement/test various hardware-specific stuff that only ARM7 can access. + +Main program (ARM9): +- main.cpp. The main program, including the main(argc, argv) function, event handlers for the various modes, plus some subfunctions for handling argv on cards that do not support them. +- auxspi.h, auxspi.cpp, auxspi_core.inc: This is the actual magic - a reimplementation of the eeprom functions from libnds, using inline functions (found in auxspi_core.cpp). +- dsCard.h, dsCard.cpp: This is a small hack of the code sample made available by Team EZFlash to address the EZFlash 3in1. Some fixes to make it work on older cards. +- gba.h, gba.cpp: This implements the eeprom functions for GBA games, however tailored to work on a DS phat/lite. +- hardware.h, hardware.cpp: This is a happy collection of functions working with hardware. No low-level functions (they are found in different files), but instead working methods to access the save and write it back. Basically, this is what the event handlers in main.cpp do call. Hardware detection has also been moved here. +- fileselect.h, fileselect.cpp: This is a file select function written from scratch, that works both with libfat filesystems and a remote FTP server. It is somewhat tailored to the program (but could probably be recycled for other projects). +- dsi.h, dsi.cpp: A collection of functions exclusive to the DSi. Right now, it does not do anything noteworthy. +- display.h, display.cpp: A collection of functions that are used to write most text used by the program, in a somewhat intependent version. This is where to start if you want to change the GUI. +- strings.h, strings.cpp: The new translation interface. If you use these functions, you are able to swap out strings by providing an external ini file. +- globals.h, globals.cpp: All global variables are defined and implemented here. + +FTPlib (by Thomas Pfau): +A handy and compact library for accessing an FTP server. Written in pure C, with no additional external dependencies. Mostly untouched, just some quick hacks so it builds. One milion warnings during the build process, which you can safely ignore. +- ftplib.h +- ftplib.c + +libini (by Simon White) +A small library for parsing ini files. Mostly untouched. +- libini.h +- ini*.h +- *.i +- ini.cpp + diff --git a/arm9/source/auxspi.cpp b/arm9/source/auxspi.cpp index d5f2967..ce3f68b 100644 --- a/arm9/source/auxspi.cpp +++ b/arm9/source/auxspi.cpp @@ -97,7 +97,7 @@ uint8 auxspi_save_type(bool ir) if ((sr & 0xfd) == 0xF0 && (jedec == 0x00ffffff)) return 1; if ((sr & 0xfd) == 0x00 && (jedec == 0x00ffffff)) return 2; - if ((sr & 0xfd) == 0x00 && (jedec != 0x00ffffff)) return 3; // should also cover Pokemon HG/SS + if ((sr & 0xfd) == 0x00 && (jedec != 0x00ffffff)) return 3; // TODO: add support for Band Brothers DX (as soon as I know how) return 0; diff --git a/arm9/source/auxspi.h b/arm9/source/auxspi.h index ab71329..d9f880f 100644 --- a/arm9/source/auxspi.h +++ b/arm9/source/auxspi.h @@ -48,4 +48,6 @@ void auxspi_erase(bool ir = false); bool auxspi_has_infrared(); //void auxspi_gpio_init_save(); +bool auxspi_is_unknown_type3(); + #endif \ No newline at end of file diff --git a/arm9/source/display.cpp b/arm9/source/display.cpp index de13916..af784bc 100644 --- a/arm9/source/display.cpp +++ b/arm9/source/display.cpp @@ -53,22 +53,18 @@ void displayInit() videoSetModeSub(MODE_0_2D); vramSetBankC(VRAM_C_SUB_BG); consoleInit(&lowerScreen, 3,BgType_Text4bpp, BgSize_T_256x256, 31, 0, false, true); +} - consoleSelect(&upperScreen); - iprintf("\n\n\n\n\nDS savegame manager\nVersion 0.2.5 semi-stable\nBy Pokedoc"); +void displayTitle() +{ + displayMessageF(STR_TITLE_MSG); - displayPrintState("Press (B) to continue"); + displayStateF(STR_STR, "Press (B) to continue"); while (!(keysCurrent() & KEY_B)); } void displayPrintUpper() { - /* - //extern uint32 ezflash; - extern bool gba; - extern uint32 dstype; - extern bool slot2; - */ bool gba = (mode == 1); u32 dstype = (mode == 3) ? 1 : 0; bool ir = (slot_1_type == 1) ? true : false; @@ -85,7 +81,7 @@ void displayPrintUpper() iprintf("Special :\n"); iprintf("--- SLOT 2 ---------------------"); if (dstype > 0) - iprintf("This device has no Slot-2\n"); + iprintf("This device has no Slot 2\n"); else { iprintf("Game ID :\n"); iprintf("Game name:\n"); @@ -127,7 +123,7 @@ void displayPrintUpper() sprintf(&name[0], "Download Play"); break; default: - sprintf(&name[0], "* Unknown *"); + sprintf(&name[0], "* ??? *"); break; } consoleClear(); @@ -173,10 +169,13 @@ void displayPrintUpper() sprintf(&name[0], "FRAM (%i kB)", 1 << (size - 10)); break; case 3: - sprintf(&name[0], "Flash (%i kB)", 1 << (size - 10)); + if (size == 0) + sprintf(&name[0], "Flash (? kB, ID=%x)", auxspi_save_jedec_id(ir)); + else + sprintf(&name[0], "Flash (%i kB)", 1 << (size - 10)); break; default: - sprintf(&name[0], "unknown"); + sprintf(&name[0], "???"); break; } } @@ -207,7 +206,7 @@ void displayPrintUpper() else if (ezflash == 0x227E2202) sprintf(name, "3in1 (256M V1)"); else - sprintf(name, "3in1 (unknown type)"); + sprintf(name, "3in1 (???M)"); } else if (gba) sprintf(name, "%.4s", (char*)0x080000ac); else if (slot2) @@ -311,47 +310,8 @@ void displayPrintLower() iprintf(" .sav -> Game"); consoleSetWindow(&lowerScreen, 1, 17, 30, 6); - iprintf("\n RESET\n\n"); - iprintf(" WIPES OUT ALL SAVE DATA\n"); - iprintf(" ON YOUR GAME !"); -} - -void displayMessage(const char *msg) -{ - consoleSelect(&upperScreen); - consoleSetWindow(0, 0, 16, 32, 6); - consoleClear(); - - iprintf("%s", msg); -} - -void displayMessageA(int id) -{ - displayMessage(stringsGetMessageString(id)); -} - -void displayMessage2(const char *msg, bool warn) -{ - consoleSelect(&lowerScreen); - consoleSetWindow(&lowerScreen, 0, 0, 32, 24); - consoleClear(); - - if (warn) { - iprintf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); - for (int i = 0; i < 22; i++) - iprintf("! !"); - iprintf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); - } - - consoleSetWindow(&lowerScreen, 2, 2, 28, 20); - consoleClear(); - - iprintf("%s", msg); -} - -void displayMessage2A(int id, bool warn) -{ - displayMessage2(stringsGetMessageString(id), warn); + iprintf("\n RESET\n"); + iprintf(stringsGetMessageString(STR_MM_WIPE)); } void displayPrintState(const char *txt) @@ -363,6 +323,64 @@ void displayPrintState(const char *txt) iprintf("%s", txt); } +// ----- internal function +char *ParseLine(char *start, const char *end, int &length) +{ + // This function takes a line and does a quick word-wrap, by fitting it into a certain fixed-length line + int len = 0; + char *cur = start; + char *separator = start; + + while (start < end) { + if (*start == '\n') { + length = 1; + return start; + } + + // look for a working "start" position + if ((*start == ' ') || (*start == '\n') || (*start == '\t')) { + start++; cur++; separator++; + continue; + } + + // do count characters + if (len < length) { + cur++; len++; + if ((*cur == ' ') || (*cur == '\n') || (*cur == '\t') || (*cur == '\0')) { + separator = cur; + if ((*cur == '\n') || (*cur == '\0')) + break; + } + } else { + // emergency exit + if (!separator) + separator = cur; + break; + } + } + + length = separator - start; + return start; +} +// ------------------ + +void displayStateF(int id, ...) +{ + // prevent flickering + swiWaitForVBlank(); + + va_list argp; + va_start(argp, id); + memset(txt, 0, 256); + vsnprintf(txt, 256, stringsGetMessageString(id), argp); + va_end(argp); + + consoleSelect(&upperScreen); + consoleSetWindow(0, 0, 22, 32, 1); + consoleClear(); + + iprintf("%s", txt); +} void displayProgressBar(int cur, int max0) { @@ -403,3 +421,94 @@ void displayProgressBar(int cur, int max0) iprintf("%s", buffer); } +void displayMessageF(int id, ...) +{ + va_list argp; + va_start(argp, id); + memset(txt, 0, 256); + vsnprintf(txt, 256, stringsGetMessageString(id), argp); + va_end(argp); + + consoleSelect(&upperScreen); + consoleSetWindow(&upperScreen, 0, 16, 32, 6); + consoleClear(); + + char *start = txt; + char *end = start + strlen(txt); + for (int i = 0; i < 6; i++) { + int l; + l = 32; + start = ParseLine(start, end, l); + consoleSetWindow(&upperScreen, 0, i+16, 32, 1); + char tmp = *(start+l); + *(start+l) = '\0'; + iprintf(start); + *(start+l) = tmp; + start += l+1; + if (start >= end) + break; + } +} + +void displayMessage2F(int id, ...) +{ + va_list argp; + va_start(argp, id); + memset(txt, 0, 256); + vsnprintf(txt, 256, stringsGetMessageString(id), argp); + va_end(argp); + + consoleSelect(&lowerScreen); + consoleSetWindow(&lowerScreen, 0, 0, 32, 24); + consoleClear(); + + char *start = txt; + char *end = start + strlen(txt); + for (int i = 0; i < 20; i++) { + int l; + l = 28; + start = ParseLine(start, end, l); + consoleSetWindow(&lowerScreen, 2, i+2, 28, 1); + char tmp = *(start+l); + *(start+l) = '\0'; + iprintf(start); + *(start+l) = tmp; + start += l+1; + if (start >= end) + break; + } +} + +void displayWarning2F(int id, ...) +{ + va_list argp; + va_start(argp, id); + memset(txt, 0, 256); + vsnprintf(txt, 256, stringsGetMessageString(id), argp); + va_end(argp); + + consoleSelect(&lowerScreen); + consoleSetWindow(&lowerScreen, 0, 0, 32, 24); + consoleClear(); + + iprintf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + for (int i = 0; i < 22; i++) + iprintf("! !"); + iprintf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + char *start = txt; + char *end = start + strlen(txt); + for (int i = 0; i < 20; i++) { + int l; + l = 28; + start = ParseLine(start, end, l); + consoleSetWindow(&lowerScreen, 2, i+2, 28, 1); + char tmp = *(start+l); + *(start+l) = '\0'; + iprintf(start); + *(start+l) = tmp; + start += l+1; + if (start >= end) + break; + } +} diff --git a/arm9/source/display.h b/arm9/source/display.h index caad1eb..6729539 100644 --- a/arm9/source/display.h +++ b/arm9/source/display.h @@ -26,7 +26,8 @@ #ifndef SAVE_DISPLAY_H #define SAVE_DISPLAY_H -#include +#include +#include extern PrintConsole upperScreen; @@ -34,19 +35,18 @@ extern PrintConsole lowerScreen; void displayInit(); +void displayTitle(); void displayPrintUpper(); void displayPrintLower(); -void displayMessage(const char *msg);// upper screen -void displayMessage2(const char *msg, bool warn); // lower screen +void displayMessageF(int id, ...); +void displayMessage2F(int id, ...); +void displayWarning2F(int id, ...); -// ftplib includes this file, and it is pure c, so no operator overloading for now. -void displayMessageA(int id);// upper screen, string table version -void displayMessage2A(int id, bool warn); // lower screen, string table version +void displayStateF(int id, ...); void displayProgressBar(int cur, int max0); -void displayPrintState(const char *txt); diff --git a/arm9/source/fileselect.cpp b/arm9/source/fileselect.cpp index 1fc31ee..f9cf1ef 100644 --- a/arm9/source/fileselect.cpp +++ b/arm9/source/fileselect.cpp @@ -36,6 +36,7 @@ #include #include "display.h" +#include "strings.h" #include "fileselect.h" @@ -155,11 +156,10 @@ void filePrintFileList(const char *dir, int first, int select, int count, bool c consoleSetWindow(&lowerScreen, 0, 18, 32, 6); consoleClear(); iprintf("================================"); - iprintf("Please select a .sav file\n"); - iprintf(" (A) Select\n"); - iprintf(" (B) One directory up\n"); if (cancel) - iprintf(" (L+R) cancel (new file)"); + iprintf(stringsGetMessageString(STR_FS_WRITE)); + else + iprintf(stringsGetMessageString(STR_FS_READ)); } // ========================***************************============================== diff --git a/arm9/source/gba.cpp b/arm9/source/gba.cpp index 70692fe..39ba18b 100644 --- a/arm9/source/gba.cpp +++ b/arm9/source/gba.cpp @@ -45,6 +45,7 @@ #include "display.h" #include "globals.h" +#include "strings.h" inline u32 min(u32 i, u32 j) { return (i < j) ? i : j;} inline u32 max(u32 i, u32 j) { return (i > j) ? i : j;} @@ -253,7 +254,7 @@ void gbaEepromRead8Bytes(u8 *out, u32 addr, bool short_addr = false) static u32 eeprom = 0x09ffff00; // send command to eeprom - displayPrintState("Sending command"); + displayStateF(STR_STR, "Sending command"); // commenting this out or not does not have any impact on the EEPROM device. Therefore, // the following command does not "make" it to the hardware! DC_FlushRange(&buf[0], sizeof(buf)); @@ -267,7 +268,7 @@ void gbaEepromRead8Bytes(u8 *out, u32 addr, bool short_addr = false) //while ((*(u16*)0x09ffff00 & 1) == 0); // get answer from eeprom - displayPrintState("listening"); + displayStateF(STR_STR, "listening"); DC_FlushRange(&buf[0], sizeof(buf)); DMA_SRC(3) = (uint32)eeprom; DMA_DEST(3) = (uint32)&buf[0]; @@ -346,7 +347,7 @@ void gbaEepromWrite8Bytes(u8 *out, u32 addr, bool short_addr = false) } // send command to eeprom - displayPrintState("Sending command"); + displayStateF(STR_STR, "Sending command"); static u32 eeprom = 0x09ffff00; DMA_SRC(3) = (uint32)&buf[0]; DMA_DEST(3) = (uint32)eeprom; @@ -356,7 +357,7 @@ void gbaEepromWrite8Bytes(u8 *out, u32 addr, bool short_addr = false) while(DMA_CR(3) & DMA_BUSY); // get answer from eeprom - displayPrintState("listening"); + displayStateF(STR_STR, "listening"); DMA_SRC(3) = (uint32)eeprom; DMA_DEST(3) = (uint32)&buf[0]; // there is a bit for eeprom access, but it only seems to freeze the transfer!? @@ -381,7 +382,7 @@ void gbaEepromWrite8Bytes(u8 *out, u32 addr, bool short_addr = false) #endif } -bool gbaReadSave(u8 *dst, u8 src, u32 len, u8 type) +bool gbaReadSave(u8 *dst, u32 src, u32 len, u8 type) { int nbanks = 2; // for type 4,5 bool eeprom_long = true; @@ -465,14 +466,14 @@ bool gbaIsAtmel() // //char txt[128]; sprintf(txt, "Man: %x, Dev: %x", man, dev); - displayPrintState(txt); + displayStateF(STR_STR, txt); if ((man == 0x3d) && (dev == 0x1f)) return true; else return false; } -bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) +bool gbaWriteSave(u32 dst, u8 *src, u32 len, u8 type) { int nbanks = 2; // for type 4,5 bool eeprom_long = true; @@ -482,6 +483,7 @@ bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) eeprom_long = false; } case 2: { + /* int start, end; start = src >> 3; end = (src + len - 1) >> 3; @@ -492,15 +494,16 @@ bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) } memcpy(dst, tmp, len); free(tmp); + */ break; } case 3: { // SRAM: blind write - u32 start = 0x0a000000 + src; - u8 *tmpsrc = (u8*)start; + u32 start = 0x0a000000 + dst; + u8 *tmpdst = (u8*)start; sysSetBusOwners(true, true); - for (u32 i = 0; i < len; i++, tmpsrc++, dst++) - *tmpsrc = *dst; + for (u32 i = 0; i < len; i++, tmpdst++, src++) + *tmpdst = *src; swiDelay(10); // mabe we don't need this, but better safe than sorry break; } @@ -509,7 +512,7 @@ bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) if (atmel) { // only 64k, no bank switching required u32 len7 = len >> 7; - u8 *tmpsrc = (u8*)(0x0a000000+src); + u8 *tmpdst = (u8*)(0x0a000000+dst); for (u32 j = 0; j < len7; j++) { u32 ime = enterCriticalSection(); *(u8*)0x0a005555 = 0xaa; @@ -519,11 +522,11 @@ bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) *(u8*)0x0a005555 = 0xa0; swiDelay(10); for (int i = 0; i < 128; i++) { - *tmpsrc = *dst; + *tmpdst = *src; swiDelay(10); } leaveCriticalSection(ime); - while (*tmpsrc != *dst) {swiDelay(10);} + while (*tmpdst != *src) {swiDelay(10);} } break; } @@ -534,7 +537,7 @@ bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) // FIXME: currently, you can only write "all or nothing" nbanks = 2; for (int j = 0; j < nbanks; j++) { - displayPrintState("Switching Bank."); + displayStateF(STR_STR, "Switching Bank."); *(u8*)0x0a005555 = 0xaa; swiDelay(10); *(u8*)0x0a002aaa = 0x55; @@ -546,15 +549,15 @@ bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) // u32 start, sublen; if (j == 0) { - start = 0x0a000000 + src; - sublen = (src < 0x10000) ? min(len, (1 << 16) - src) : 0; + start = 0x0a000000 + dst; + sublen = (dst < 0x10000) ? min(len, (1 << 16) - dst) : 0; } else if (j == 1) { - start = max(0x09ff0000 + src, 0x0a000000); - sublen = (src + len < 0x10000) ? 0 : min(len, len - (0x10000 - src)); + start = max(0x09ff0000 + dst, 0x0a000000); + sublen = (dst + len < 0x10000) ? 0 : min(len, len - (0x10000 - dst)); } - u8 *tmpsrc = (u8*)start; + u8 *tmpdst = (u8*)start; sysSetBusOwners(true, true); - for (u32 i = 0; i < sublen; i++, tmpsrc++, dst++) { + for (u32 i = 0; i < sublen; i++, tmpdst++, src++) { // we need to wait a few cycles before the hardware reacts! *(u8*)0x0a005555 = 0xaa; swiDelay(10); @@ -563,10 +566,10 @@ bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type) *(u8*)0x0a005555 = 0xa0; // write byte command swiDelay(10); // - *tmpsrc = *dst; + *tmpdst = *src; swiDelay(10); // - while (*tmpsrc != *dst) {swiDelay(10);} + while (*tmpdst != *src) {swiDelay(10);} } } break; @@ -582,6 +585,8 @@ bool gbaFormatSave(u8 type) // TODO: eeprom is not supported yet break; case 3: + memset(data, 0, 1 << 15); + gbaWriteSave(0, data, 1 << 15, 3); break; case 4: case 5: diff --git a/arm9/source/gba.h b/arm9/source/gba.h index 8b1d26a..f46aa6a 100644 --- a/arm9/source/gba.h +++ b/arm9/source/gba.h @@ -68,8 +68,8 @@ uint8 gbaGetSaveType(); uint32 gbaGetSaveSize(uint8 type = 255); uint32 gbaGetSaveSizeLog2(uint8 type = 255); -bool gbaReadSave(u8 *dst, u8 src, u32 len, u8 type); -bool gbaWriteSave(u8 *dst, u8 src, u32 len, u8 type); +bool gbaReadSave(u8 *dst, u32 src, u32 len, u8 type); +bool gbaWriteSave(u32 dst, u8 *src, u32 len, u8 type); bool gbaFormatSave(u8 type); diff --git a/arm9/source/hardware.cpp b/arm9/source/hardware.cpp index 01505d9..06db229 100644 --- a/arm9/source/hardware.cpp +++ b/arm9/source/hardware.cpp @@ -63,7 +63,7 @@ bool swap_cart() nds.gameTitle[0] = 0; while (!nds.gameTitle[0]) { - displayMessage2A(STR_HW_SWAP_CARD, false); + displayMessage2F(STR_HW_SWAP_CARD); bool swap = false; while (!swap) { @@ -79,7 +79,7 @@ bool swap_cart() cardReadHeader((u8*)&nds); displayPrintUpper(); if (!nds.gameTitle[0]) { - displayMessage2A(STR_HW_CARD_UNREADABLE, false); + displayMessage2F(STR_HW_CARD_UNREADABLE); continue; } @@ -232,10 +232,10 @@ void hwBackup3in1() int size_blocks = 1 << max(0, (int8(size) - 18)); // ... in units of 0x40000 bytes - that's 256 kB uint8 type = auxspi_save_type(ir); - displayMessage2A(STR_HW_3IN1_FORMAT_NOR, false); + displayMessage2F(STR_HW_3IN1_FORMAT_NOR); hwFormatNor(0, size_blocks+1); - displayMessage2A(STR_HW_3IN1_WRITE_NOR, false); + displayMessage2F(STR_HW_READ_GAME); if (size < 15) size_blocks = 1; else @@ -257,11 +257,11 @@ void hwBackup3in1() ReadNorFlash(test, (i << 15) + pitch, LEN); hwRelease3in1(ime); if (*((vuint16 *)(FlashBase+0x2002)) == 0x227E) { - displayMessage2A(STR_HW_3IN1_ERR_IDMODE, false); + displayMessage2F(STR_HW_3IN1_ERR_IDMODE); while(1); } if (memcmp(test, data, LEN)) { - displayMessage2A(STR_HW_3IN1_ERR_NOR, false); + displayMessage2F(STR_HW_3IN1_ERR_NOR); while(1); } } @@ -270,7 +270,7 @@ void hwBackup3in1() // Write a flag to tell the app what happens on restart. // This is necessary, since some DLDI drivers cease working after swapping a card. // Fix for dead batteries: don't write "reboot" values to SRAM, but to NOR. - displayMessage2A(STR_HW_3IN1_PREPARE_REBOOT, false); + displayMessage2F(STR_HW_3IN1_PREPARE_REBOOT); // sNDSHeader nds; cardReadHeader((u8*)&nds); // on a Cyclops Evolution, this call *will* mess up your DLDI driver! @@ -286,10 +286,10 @@ void hwBackup3in1() memset(data, 0, 0x8000); memcpy(&data[0x1000], (u8*)&data2, sizeof(data2)); uint32 ime = hwGrab3in1(); - WriteNorFlash(0, data, 0x8000); // this should work - but it does not! + WriteNorFlash(0, data, 0x8000); hwRelease3in1(ime); - displayMessage2A(STR_HW_3IN1_PLEASE_REBOOT, false); + displayMessage2F(STR_HW_3IN1_PLEASE_REBOOT); while(1) {}; } @@ -298,7 +298,7 @@ void hwDump3in1(uint32 size, const char *gamename) { u32 size_blocks = 1 << max(0, (uint8(size) - 18)); - displayMessageA(STR_HW_SELECT_FILE_OW); + displayMessageF(STR_HW_SELECT_FILE_OW); char path[256]; char fname[256] = ""; @@ -308,14 +308,12 @@ void hwDump3in1(uint32 size, const char *gamename) if (!fname[0]) { uint32 cnt = 0; sprintf(fname, "/%s.%i.sav", gamename, cnt); - //char txt[256]; - sprintf(txt, stringsGetMessageString(STR_HW_SEEK_UNUSED_FNAME), fname); - displayMessage2(txt, false); + displayMessage2F(STR_HW_SEEK_UNUSED_FNAME, fname); while (fileExists(fname)) { if (cnt < 65536) cnt++; else { - displayMessage2A(STR_ERR_NO_FNAME, true); + displayWarning2F(STR_ERR_NO_FNAME); while(1); } sprintf(fname, "%s.%i.sav", gamename, cnt); @@ -323,9 +321,8 @@ void hwDump3in1(uint32 size, const char *gamename) } char fullpath[256]; sprintf(fullpath, "%s/%s", path, fname); - sprintf(txt, stringsGetMessageString(STR_HW_3IN1_DUMP), fullpath); - displayMessage2(txt, false); - + displayMessage2F(STR_HW_WRITE_FILE, fullpath); + FILE *file = fopen(fullpath, "wb"); if (size < 15) size_blocks = 1; @@ -341,8 +338,7 @@ void hwDump3in1(uint32 size, const char *gamename) } fclose(file); - sprintf(txt, stringsGetMessageString(STR_HW_3IN1_DONE_DUMP), fullpath); - displayMessage2(txt, false); + displayMessage2F(STR_HW_3IN1_DONE_DUMP, fullpath); while (!(keysCurrent() & KEY_B)) {}; } @@ -351,12 +347,12 @@ void hwRestore3in1() char path[256]; char fname[256]; memset(fname, 0, 256); - displayMessageA(STR_HW_SELECT_FILE); // lower screen is used for file browser + displayMessageF(STR_HW_SELECT_FILE); // lower screen is used for file browser fileSelect("/", path, fname, 0, false, false); char msg[256]; // This does not have to be translated. + // FIXME: make this more meaningful! sprintf(msg, "%s/%s", path, fname); - displayMessage(msg); FILE *file = fopen(msg, "rb"); uint32 size0 = fileSize(msg); @@ -368,10 +364,10 @@ void hwRestore3in1() } int size_blocks = 1 << max(0, int8(size) - 18); // ... in units of 0x40000 bytes - that's 256 kB - displayMessage2A(STR_HW_3IN1_FORMAT_NOR, false); + displayMessage2F(STR_HW_3IN1_FORMAT_NOR); hwFormatNor(1, size_blocks); - displayMessage2A(STR_HW_3IN1_WRITE_NOR, false); + displayMessage2F(STR_HW_READ_FILE, msg); if (size < 15) size_blocks = 1; else @@ -388,11 +384,11 @@ void hwRestore3in1() ReadNorFlash(test, (i << 15) + pitch, LEN); hwRelease3in1(ime); if (*((vuint16 *)(FlashBase+0x2002)) == 0x227E) { - displayMessage2A(STR_HW_3IN1_ERR_IDMODE, false); + displayMessage2F(STR_HW_3IN1_ERR_IDMODE); while(1); } if (memcmp(test, data, LEN)) { - displayMessage2A(STR_HW_3IN1_ERR_NOR, false); + displayMessage2F(STR_HW_3IN1_ERR_NOR); while(1); } } @@ -411,9 +407,9 @@ void hwRestore3in1_b(uint32 size_file) uint32 size = auxspi_save_size_log_2(ir); while ((size_file < size) || (slot_1_type == 2)) { if (slot_1_type == 2) - displayMessage2A(STR_HW_SWAP_CARD, false); + displayMessage2F(STR_HW_SWAP_CARD); else if (size_file < size) - displayMessage2A(STR_HW_WRONG_GAME, false); + displayMessage2F(STR_HW_WRONG_GAME); swap_cart(); ir = (slot_1_type == 1) ? true : false; size = auxspi_save_size_log_2(ir); @@ -422,12 +418,12 @@ void hwRestore3in1_b(uint32 size_file) uint8 type = auxspi_save_type(ir); if (type == 3) { - displayMessage2A(STR_HW_FORMAT_GAME, false); + displayMessage2F(STR_HW_FORMAT_GAME); auxspi_erase(ir); } // And finally, write the save! - displayMessage2A(STR_HW_WRITE_GAME, false); + displayMessage2F(STR_HW_WRITE_GAME); u32 LEN = 0, num_blocks = 0, shift = 0; switch (type) { case 1: @@ -455,8 +451,7 @@ void hwRestore3in1_b(uint32 size_file) } displayProgressBar(1, 1); - displayMessage2A(STR_HW_3IN1_RESTORE, false); - + displayMessage2F(STR_HW_PLEASE_REBOOT); while (1) {}; } @@ -464,7 +459,7 @@ void hwRestore3in1_b(uint32 size_file) void hwErase() { bool ir = (slot_1_type == 1) ? true : false; - displayMessage2A(STR_HW_WARN_DELETE, true); + displayMessage2F(STR_HW_WARN_DELETE); while (!(keysCurrent() & (KEY_UP | KEY_R | KEY_Y))) {}; auxspi_erase(ir); } @@ -491,29 +486,29 @@ void hwBackupFTP(bool dlp) size_blocks = 1 << (uint8(size) - 15); // Second: connect to FTP server - displayMessage2A(STR_HW_FTP_SEEK_AP, false); + displayMessage2F(STR_HW_FTP_SEEK_AP); if (!Wifi_InitDefault(true)) { - displayMessage2A(STR_HW_FTP_ERR_AP, true); + displayWarning2F(STR_HW_FTP_ERR_AP); while(1); } - displayMessage2A(STR_HW_FTP_SEEK_FTP, false); + displayMessage2F(STR_HW_FTP_SEEK_FTP); char fullname[512]; sprintf(fullname, "%s:%i", ftp_ip, ftp_port); j = 0; while (!FtpConnect(fullname, &buf)) { j++; if (j >= jmax) { - displayMessage2A(STR_HW_FTP_ERR_FTP, true); + displayWarning2F(STR_HW_FTP_ERR_FTP); while(1); } swiDelay(10000); } - displayMessage2A(STR_HW_FTP_LOGIN, false); + displayMessage2F(STR_HW_FTP_LOGIN); j = 0; while (!FtpLogin(ftp_user, ftp_pass, buf)) { j++; if (j >= jmax) { - displayMessage2A(STR_HW_FTP_ERR_LOGIN, true); + displayWarning2F(STR_HW_FTP_ERR_LOGIN); while(1); } swiDelay(10000); @@ -522,8 +517,8 @@ void hwBackupFTP(bool dlp) char fdir[256] = ""; char fname[256] =""; memset(fname, 0, 256); - displayMessageA(STR_HW_SELECT_FILE_OW); - displayPrintState("FTP: dir"); // TODO: translate me! + displayMessageF(STR_HW_SELECT_FILE_OW); + displayStateF(STR_HW_FTP_DIR); fileSelect("/", fdir, fname, buf, true, false); bool newfile; if (!fname[0]) @@ -540,12 +535,11 @@ void hwBackupFTP(bool dlp) int tsize = 0; sprintf(fname, "%.12s.%i.sav", nds.gameTitle, cnt); while (FtpSize(fname, &tsize, FTPLIB_IMAGE, buf) != 0) { - sprintf(txt, stringsGetMessageString(STR_HW_SEEK_UNUSED_FNAME), fname); - displayMessage2(txt, false); + displayMessage2F(STR_HW_SEEK_UNUSED_FNAME, fname); if (cnt < 65536) cnt++; else { - displayMessage2A(STR_ERR_NO_FNAME, true); + displayWarning2F(STR_ERR_NO_FNAME); while(1); } sprintf(fname, "%.12s.%i.sav", nds.gameTitle, cnt); @@ -553,10 +547,10 @@ void hwBackupFTP(bool dlp) } // Fourth: dump save - displayPrintState(""); + displayStateF(STR_EMPTY); // This may not have to be translated. + // FIXME: make this more meaningful! sprintf(fullname, "%s%s", fdir, fname); - displayMessage(fullname); FtpAccess(fname, FTPLIB_FILE_WRITE, FTPLIB_IMAGE, buf, &ndata); u32 length = 0x200; if (size < 9) @@ -569,14 +563,14 @@ void hwBackupFTP(bool dlp) u32 delta = FtpWrite((u8*)&data[out], length-out, ndata); out += delta; if (delta == 0) { - displayMessage2A(STR_HW_FTP_READ_ONLY, false); + displayMessage2F(STR_HW_FTP_READ_ONLY); } else { - displayMessage2("", false); + displayMessage2F(STR_EMPTY); } if (delta < length) { - displayPrintState(stringsGetMessageString(STR_HW_FTP_SLOW)); + displayStateF(STR_HW_FTP_SLOW); } else { - displayPrintState(""); + displayStateF(STR_EMPTY); } } } @@ -586,7 +580,7 @@ void hwBackupFTP(bool dlp) Wifi_DisconnectAP(); if (dlp) { - displayMessage2A(STR_HW_PLEASE_REBOOT, false); + displayMessage2F(STR_HW_PLEASE_REBOOT); while(1); } } @@ -600,29 +594,29 @@ void hwRestoreFTP(bool dlp) // Dump save and write it to FTP server // First: connect to FTP server - displayMessage2A(STR_HW_FTP_SEEK_AP, false); + displayMessage2F(STR_HW_FTP_SEEK_AP); if (!Wifi_InitDefault(true)) { - displayMessage2A(STR_HW_FTP_ERR_AP, true); + displayWarning2F(STR_HW_FTP_ERR_AP); while(1); } - displayMessage2A(STR_HW_FTP_SEEK_FTP, false); + displayMessage2F(STR_HW_FTP_SEEK_FTP); char fullname[512]; sprintf(fullname, "%s:%i", ftp_ip, ftp_port); j = 0; while (!FtpConnect(fullname, &buf)) { j++; if (j >= jmax) { - displayMessage2A(STR_HW_FTP_ERR_FTP, true); + displayWarning2F(STR_HW_FTP_ERR_FTP); while(1); } swiDelay(10000); } - displayMessage2A(STR_HW_FTP_LOGIN, false); + displayMessage2F(STR_HW_FTP_LOGIN); j = 0; while (!FtpLogin(ftp_user, ftp_pass, buf)) { j++; if (j >= jmax) { - displayMessage2A(STR_HW_FTP_ERR_LOGIN, false); + displayWarning2F(STR_HW_FTP_ERR_LOGIN); while(1); } swiDelay(10000); @@ -632,8 +626,8 @@ void hwRestoreFTP(bool dlp) char fdir[256] = ""; char fname[256] =""; memset(fname, 0, 256); - displayMessageA(STR_HW_SELECT_FILE); - displayPrintState("FTP: dir"); + displayMessageF(STR_HW_SELECT_FILE); + displayStateF(STR_STR, "FTP: dir"); fileSelect("/", fdir, fname, buf, false, false); // Third: swap card @@ -644,10 +638,10 @@ void hwRestoreFTP(bool dlp) uint8 type = auxspi_save_type(ir); // Fourth: read file - displayPrintState(""); + displayStateF(STR_EMPTY); FtpChdir(fdir, buf); + // FIXME: make this more meaningful! sprintf(fullname, "%s%s", fdir, fname); - displayMessage(fullname); u32 LEN = 0, num_blocks = 0, shift = 0; switch (type) { case 1: @@ -670,16 +664,16 @@ void hwRestoreFTP(bool dlp) bool insecure = ((1 << size) > size_buf); if (insecure) { #if 0 - displayMessage("Save larger than available RAM.\nRestoring will be dangerous!\n\nPress Start + Select to proceed"); + displayMessageF("Save larger than available RAM.\nRestoring will be dangerous!\n\nPress Start + Select to proceed"); while (!(keysCurrent() & (KEY_START | KEY_SELECT))); #else - displayMessage("Save larger than available RAM.\nRestoring is buggy, so\nI will stop here.\n(Try Rudolphs tools.)"); + // TODO: translate this message! + displayMessage2F(STR_STR, "Save larger than available RAM.\nRestoring is buggy, so\nI will stop here.\n(Try Rudolphs tools.)"); while (1); #endif } if ((type == 3) && (insecure)) { - displayMessage2A(STR_HW_FORMAT_GAME, false); - //displayPrintState("Formating Flash chip"); + displayMessage2F(STR_HW_FORMAT_GAME); auxspi_erase(ir); } FtpAccess(fname, FTPLIB_FILE_READ, FTPLIB_IMAGE, buf, &ndata); @@ -690,14 +684,14 @@ void hwRestoreFTP(bool dlp) while (in < 512) { in += FtpRead((u8*)&pdata[in], 512-in, ndata); if (in < 512) - displayPrintState(stringsGetMessageString(STR_HW_FTP_SLOW)); + displayStateF(STR_HW_FTP_SLOW); else - displayPrintState(""); + displayStateF(STR_EMPTY); } // does not fit into memory: unsafe mode // TODO: reactivate this! if (insecure) { - displayPrintState("Writing save (insecure!)"); + displayStateF(STR_STR, "Writing save (insecure!)"); for (int j = 0; j < 1 << (9-shift); j++) { auxspi_write_data((i << 9)+(j << shift), ((u8*)pdata)+(j< 5)) + return; + + memset(data, 0, gbaGetSaveSize(type)); + + switch (type) { + } } // ------------------------------------------------- diff --git a/arm9/source/main.cpp b/arm9/source/main.cpp index 5eb312b..87feeac 100644 --- a/arm9/source/main.cpp +++ b/arm9/source/main.cpp @@ -62,12 +62,12 @@ void mode_dsi() // TODO: test is SD card is present! // use 3in1 to buffer data - displayPrintState(""); + displayStateF(STR_EMPTY); displayPrintUpper(); displayPrintLower(); // DSi mode, does nothing at the moment - displayMessage2A(STR_BOOT_MODE_UNSUPPORTED,true); + displayWarning2F(STR_BOOT_MODE_UNSUPPORTED); while(1); touchPosition touchXY; @@ -97,11 +97,11 @@ void mode_dsi() void mode_slot2() { // use slot2 DLDI device to store data - displayPrintState(""); + displayStateF(STR_EMPTY); displayPrintUpper(); displayPrintLower(); - displayMessage2A(STR_BOOT_MODE_UNSUPPORTED,true); + displayWarning2F(STR_BOOT_MODE_UNSUPPORTED); while(1); touchPosition touchXY; @@ -143,11 +143,11 @@ void mode_3in1() char name[13]; memcpy(&name[0], &data2.name[0], 12); name[12] = 0; - displayMessageA(STR_HW_3IN1_CLEAR_FLAG); // lower screen is used for file browser + displayMessageF(STR_HW_3IN1_CLEAR_FLAG); // lower screen is used for file browser hwFormatNor(0, 1); // clear reboot flag hwDump3in1(size, name); } - displayMessage(""); + displayMessageF(STR_EMPTY); displayProgressBar(0,0); displayPrintLower(); @@ -183,7 +183,7 @@ void mode_gba() u8 gbatype = gbaGetSaveType(); // use 3in1 to buffer data - displayPrintState(""); + displayStateF(STR_EMPTY); gbatype = gbaGetSaveType(); displayPrintUpper(); displayPrintLower(); @@ -216,7 +216,7 @@ void mode_gba() void mode_wifi() { // use 3in1 to buffer data - displayPrintState(""); + displayStateF(STR_EMPTY); displayPrintUpper(); displayPrintLower(); @@ -250,7 +250,7 @@ void mode_wifi() void mode_dlp() { // use non-flash card based exploits (download play or Sudoku?). untested, and does not work yet! - displayPrintState(""); + displayStateF(STR_EMPTY); displayPrintUpper(); displayPrintLower(); @@ -273,7 +273,7 @@ void mode_dlp() if ((touchXY.py > 8*16) && (touchXY.py < 8*24)) { displayPrintUpper(); hwErase(); - displayMessage2A(STR_HW_DID_DELETE, true); + displayWarning2F(STR_HW_DID_DELETE); while(1); } } @@ -304,8 +304,7 @@ bool loadIniFile(char* path) ini = ini_open(inipath, "r", ""); } if (!ini) { - // FIXME - displayMessage2A(STR_BOOT_NO_INI,true); + displayWarning2F(STR_BOOT_NO_INI); while (1); } @@ -325,16 +324,15 @@ bool loadIniFile(char* path) ini_locateKey(ini, "slot2"); ini_readInt(ini, &slot2); - - ini_close(ini); - + // load additional Flash chip signatures (JEDEC IDs) ini_locateHeading(ini, "new chips"); for (int i = 0; i < EXTRA_ARRAY_SIZE; i++) { int tmp; sprintf(txt, "%i-id", i); ini_locateKey(ini, txt); - ini_readInt(ini, &tmp); + ini_readString(ini, txt, 256); + sscanf(txt, "%x", &extra_id[i]); extra_id[i] = (u32)tmp; // sprintf(txt, "%i-size", i); @@ -342,6 +340,12 @@ bool loadIniFile(char* path) ini_readInt(ini, &tmp); extra_size[i] = tmp; } + + ini_locateHeading(ini, ""); + ini_locateKey(ini, "language"); + ini_readString(ini, txt, 256); + + ini_close(ini); // delete temp file (which is a remnant of inilib) remove("/tmpfile"); @@ -399,7 +403,7 @@ int main(int argc, char* argv[]) // Init DLDI (file system driver) int fat = fatInitDefault(); if (fat == 0) { - displayMessage2A(STR_BOOT_DLDI_ERROR,true); + displayWarning2F(STR_BOOT_DLDI_ERROR); while (1); } @@ -414,19 +418,22 @@ int main(int argc, char* argv[]) loadIniFile(0); // load strings - stringsLoadFile(0); + // "txt" is used as a temp buffer for the language file name + stringsLoadFile(txt); // prepare the global data buffer data = (u8*)malloc(size_buf); // On my Cyclops, no 2MB buffer is available after loading strings! bad memory management? // Anyway, we can't do anything except for the following (which restricts the usefulness // of FTP safe mode) - // possible that no continuous 2 MB are available. while (data == NULL) { size_buf >>= 1; data = (u8*)malloc(size_buf); } + // Init the screens + displayTitle(); + // okay, we got our HW identified; now branch to the corresponding main function/event handler switch (mode) { case 1: diff --git a/arm9/source/strings.cpp b/arm9/source/strings.cpp index e75a2a3..a89f1bb 100644 --- a/arm9/source/strings.cpp +++ b/arm9/source/strings.cpp @@ -37,89 +37,94 @@ using namespace std; // a global string array char **message_strings; -//char txt[512]; -#define ADD_STRING(id,text) strcpy(txt,text);\ +#define ADD_STRING(id,text) sprintf(txt, "%i", id);\ + if (ini_locateKey(ini, txt) == 0) {\ + ini_readString(ini, txt, 256);\ + } else {\ + strcpy(txt,text);\ + }\ message_strings[id] = new char[strlen(txt)+1];\ memcpy(message_strings[id], txt, strlen(txt)+1); bool stringsLoadFile(const char *fname) { -#define NOT_FINISHED - -#ifndef NOT_FINISHED // load ini file... ini_fd_t ini = 0; if (fileExists(fname)) ini = ini_open(fname, "r", ""); - else - return false; -#endif message_strings = new char*[STR_LAST]; -#ifndef NOT_FINISHED ini_locateHeading(ini, ""); - //char txt[512]; - for (int i = 0; i < STR_LAST; i++) { - sprintf(txt, "%i", i); - ini_locateKey(ini, txt); - ini_readString(ini, txt, 512); - message_strings[i] = new char[strlen(txt)+1]; - memcpy(message_strings[i], txt, strlen(txt)+1); - } - // delete temp file (which is a remnant of inilib) - remove("/tmpfile"); -#else + ADD_STRING(STR_EMPTY, ""); + ADD_STRING(STR_STR, "%s"); + // + ADD_STRING(STR_MM_WIPE,"\n WIPES OUT ALL SAVE DATA\n ON YOUR GAME !"); + ADD_STRING(STR_TITLE_MSG,"DS savegame manager\nVersion 0.2.5 semi-stable\nBy Pokedoc"); ADD_STRING(STR_BOOT_NO_INI,"Unable to open ini file!\nPlease make sure that it is\n1. in this apps folder, or" "\n2. in the root folder\nIf 1. does not work, use 2."); ADD_STRING(STR_BOOT_MODE_UNSUPPORTED,"This mode is DISABLED.\nPlease restart the system."); ADD_STRING(STR_BOOT_DLDI_ERROR,"DLDI initialisation error!"); // - ADD_STRING(STR_HW_SWAP_CARD,"Please take out Slot 1\nflash card and insert a game\n\nPress A when done."); - ADD_STRING(STR_HW_CARD_UNREADABLE,"I can't read the game\ninserted in slot 1.\nPlease eject and retry."); - ADD_STRING(STR_HW_WRONG_GAME,"This game has a save chip\nthat is smaller than the\nsave you are going to\nwrite. Please insert a\ndifferent game.\n\nPress A when done."); - ADD_STRING(STR_HW_PLEASE_REBOOT,"Done! Please power off\n(and restart if you want\nto do more)."); + ADD_STRING(STR_HW_SWAP_CARD,"Please take out Slot 1 flash card and insert a game.\nPress A when done."); + ADD_STRING(STR_HW_CARD_UNREADABLE,"I can't read the game inserted in slot 1.\nPlease eject and retry."); + ADD_STRING(STR_HW_WRONG_GAME,"This game has a save chip that is bigger than the save you are going to write. Please insert a different game.\nPress A when done."); + ADD_STRING(STR_HW_PLEASE_REBOOT,"Done! Please power off\n(and restart if you want to do more)."); // ADD_STRING(STR_HW_SELECT_FILE,"Please select a .sav file."); - ADD_STRING(STR_HW_SELECT_FILE_OW,"Please select a file to\noverwrite, or press L+R in afolder to create a new file."); - ADD_STRING(STR_HW_SEEK_UNUSED_FNAME,"Please wait... searching for\nan unused filename.\n\nTrying: %s"); - ADD_STRING(STR_ERR_NO_FNAME,"ERROR: Unable to get an\nunused nfilename! This means that you have more than\n65536 saves!\n\n(wow!)"); + ADD_STRING(STR_HW_SELECT_FILE_OW,"Please select a file to overwrite, or press L+R in a folder to create a new file."); + ADD_STRING(STR_HW_SEEK_UNUSED_FNAME,"Please wait... searching for an unused filename.\nTrying: %s"); + ADD_STRING(STR_ERR_NO_FNAME,"ERROR: Unable to get an unused nfilename! This means that you have more than 65536 saves!\n(wow!)"); // - ADD_STRING(STR_HW_FORMAT_GAME,"Preparing to write to your\ngame. Please wait..."); - ADD_STRING(STR_HW_WRITE_GAME,"Writing the save to your\ngame. Please wait..."); + ADD_STRING(STR_HW_FORMAT_GAME,"Preparing to write to your game.\nPlease wait..."); + ADD_STRING(STR_HW_WRITE_GAME,"Writing save to your game.\nPlease wait..."); + ADD_STRING(STR_HW_READ_GAME,"Reading save from your game.\nPlease wait..."); + ADD_STRING(STR_HW_WRITE_FILE, "Writing file:\n%s"); + ADD_STRING(STR_HW_READ_FILE, "Reading file:\n%s"); // - ADD_STRING(STR_HW_3IN1_FORMAT_NOR,"Preparing to write to the\n3in1. Please wait..."); - ADD_STRING(STR_HW_3IN1_WRITE_NOR,"Writing save data to the\n3in1. Please wait..."); + ADD_STRING(STR_HW_3IN1_FORMAT_NOR,"Preparing to write to the 3in1.\nPlease wait..."); ADD_STRING(STR_HW_3IN1_PREPARE_REBOOT,"Preparing reboot..."); - ADD_STRING(STR_HW_3IN1_PLEASE_REBOOT,"Save has been written to\nthe 3in1. Please power off\nand restart this tool."); - ADD_STRING(STR_HW_3IN1_CLEAR_FLAG,"Preparing to dump your\nsave... Please wait..."); - ADD_STRING(STR_HW_3IN1_DUMP,"Dumping the save from the\n3in1 to your flash card.\nFilename:\n%s"); - ADD_STRING(STR_HW_3IN1_DONE_DUMP,"Done. Your game save has\nbeen dumped using your\n3in1. Filename:\n%s\n\nPress (B) to continue."); - ADD_STRING(STR_HW_3IN1_RESTORE,"Done. Your game save has\nbeen restored using your\n3in1.\n\nPlease restart your DS."); + ADD_STRING(STR_HW_3IN1_PLEASE_REBOOT,"Save has been written to the 3in1. Please power off and restart this tool to finish the dump."); + ADD_STRING(STR_HW_3IN1_CLEAR_FLAG,"Preparing to dump your save...\nPlease wait..."); + ADD_STRING(STR_HW_3IN1_DONE_DUMP,"Done. Your game save has been dumped using your 3in1. Filename:\n%s\nPress (B) to continue."); ADD_STRING(STR_HW_3IN1_ERR_IDMODE,"ERROR!\nID mode still active!"); - ADD_STRING(STR_HW_3IN1_ERR_NOR,"ERROR!\nNOR memory seems to be\ndamaged!"); + ADD_STRING(STR_HW_3IN1_ERR_NOR,"ERROR!\nWriting to NOR failed!"); // - ADD_STRING(STR_HW_FTP_SEEK_AP,"Connecting to an access\npoint...\n\nplease wait..."); - ADD_STRING(STR_HW_FTP_ERR_AP,"ERROR!\nCould not find a compatible\nAccess Point. Please config\nyour WiFi Connection from\na WiFi-enabled game!"); - ADD_STRING(STR_HW_FTP_SEEK_FTP,"Connecting to an FTP\nserver...\n\nplease wait..."); - ADD_STRING(STR_HW_FTP_ERR_FTP,"ERROR!\nCould not find an FTP\nserver. Please refer to\nthe documentation."); + ADD_STRING(STR_HW_FTP_SEEK_AP,"Connecting to an access point...\nplease wait..."); + ADD_STRING(STR_HW_FTP_ERR_AP,"ERROR!\nCould not find a compatible Access Point. Please configure your WiFi Connection from a WiFi-enabled game!"); + ADD_STRING(STR_HW_FTP_SEEK_FTP,"Connecting to an FTP server...\nplease wait..."); + ADD_STRING(STR_HW_FTP_ERR_FTP,"ERROR!\nCould not find an FTP server. Please refer to the documentation."); ADD_STRING(STR_HW_FTP_LOGIN,"Logging in..."); - ADD_STRING(STR_HW_FTP_ERR_LOGIN,"ERROR!\nCould not log in to the\nFTP server. Please verify\nyour username and password\nare correct in your\nini file."); + ADD_STRING(STR_HW_FTP_ERR_LOGIN,"ERROR!\nCould not log in to the FTP server. Please verify your username and password are correct in your ini file."); ADD_STRING(STR_HW_FTP_DIR,"Reading FTP directory..."); ADD_STRING(STR_HW_FTP_SLOW,"FTP is slow, please wait..."); - ADD_STRING(STR_HW_FTP_READ_ONLY,"WARNING: could not write to\nyour FTP server! Maybe you\nforgot to enable write\naccess?"); + ADD_STRING(STR_HW_FTP_READ_ONLY,"WARNING:\nCould not write to your FTP server! Maybe you forgot to enable write access?"); // - ADD_STRING(STR_HW_GBA_READ,"Reading save from your game.Please wait..."); - ADD_STRING(STR_HW_GBA_DUMP,"Writing save to flash card.\nPlease wait..."); - ADD_STRING(STR_HW_GBA_LOAD,"Reading save from your\nflash card. Filename:\n%s\n\nPlease wait..."); - ADD_STRING(STR_HW_GBA_RESTORE,"Restoring save to your game\n\nPlease wait..."); + ADD_STRING(STR_HW_WARN_DELETE,"This will WIPE OUT your entire save! ARE YOU SURE?\n\nPress R+up+Y to confim!"); + ADD_STRING(STR_HW_DID_DELETE,"Done. Your game save has been PERMANENTLY deleted.\n\nPlease restart your DS."); // - ADD_STRING(STR_HW_WARN_DELETE,"This will WIPE OUT your\nentire save! ARE YOU SURE?\n\nPress R+up+Y to confim!"); - ADD_STRING(STR_HW_DID_DELETE,"Done. Your game save has\nbeen PERMANENTLY deleted.\n\nPlease restart your DS."); + ADD_STRING(STR_FS_READ,"Please select a .sav file\n (A) Select\n (B) One directory up\n"); + ADD_STRING(STR_FS_WRITE,"Please select a .sav file\n (A) Select\n (B) One directory up\n (L+R) cancel (new file)"); // -#endif + + // delete temp file (which is a remnant of inilib) + remove("/tmpfile"); + + // Convert manual newline commands added as plaintext in the translation file. + // FIXME: this does not seem to work reliably yet (or maybe it is the print function). + for (int i = 0; i < STR_LAST; i++) { + char *ptr = message_strings[i]; + while ((ptr = strchr(ptr, '\\')) != NULL) { + if (strlen(ptr+1)) { + if (ptr[1] == 'n') { + ptr[0] = '\n'; + memmove(ptr+1, ptr+2, strlen(ptr)-1); + } + } + } + } return true; } diff --git a/arm9/source/strings.h b/arm9/source/strings.h index 76aef93..031b7cd 100644 --- a/arm9/source/strings.h +++ b/arm9/source/strings.h @@ -31,53 +31,66 @@ #define STRINGS_H enum { - STR_TITLE_MSG, - STR_BOOT_NO_INI, - STR_BOOT_MODE_UNSUPPORTED, - STR_BOOT_DLDI_ERROR, - // - STR_HW_SWAP_CARD, - STR_HW_WRONG_GAME, - STR_HW_PLEASE_REBOOT, - // - STR_HW_SELECT_FILE, - STR_HW_SELECT_FILE_OW, - STR_HW_SEEK_UNUSED_FNAME, - STR_ERR_NO_FNAME, - // - STR_HW_FORMAT_GAME, - STR_HW_WRITE_GAME, - // - STR_HW_3IN1_FORMAT_NOR, - STR_HW_3IN1_WRITE_NOR, - STR_HW_3IN1_PREPARE_REBOOT, - STR_HW_3IN1_PLEASE_REBOOT, - STR_HW_3IN1_CLEAR_FLAG, - STR_HW_3IN1_DUMP, - STR_HW_3IN1_DONE_DUMP, - STR_HW_3IN1_RESTORE, - STR_HW_3IN1_ERR_IDMODE, - STR_HW_3IN1_ERR_NOR, - // - STR_HW_FTP_SEEK_AP, - STR_HW_FTP_ERR_AP, - STR_HW_FTP_SEEK_FTP, - STR_HW_FTP_ERR_FTP, - STR_HW_FTP_LOGIN, - STR_HW_FTP_ERR_LOGIN, - STR_HW_FTP_DIR, - STR_HW_FTP_SLOW, - STR_HW_FTP_READ_ONLY, - // - STR_HW_GBA_READ, - STR_HW_GBA_DUMP, - STR_HW_GBA_LOAD, - STR_HW_GBA_RESTORE, - // - STR_HW_WARN_DELETE, - STR_HW_DID_DELETE, - // - STR_LAST + // internal message IDs (do not translate!) + STR_EMPTY, + STR_STR, + // + // boot messages (2-5) + STR_TITLE_MSG, + STR_BOOT_NO_INI, + STR_BOOT_MODE_UNSUPPORTED, + STR_BOOT_DLDI_ERROR, + // + // swap card messages (6-9) + STR_HW_SWAP_CARD, + STR_HW_WRONG_GAME, + STR_HW_CARD_UNREADABLE, + STR_HW_PLEASE_REBOOT, + // + // file select messages (10-13) + STR_HW_SELECT_FILE, + STR_HW_SELECT_FILE_OW, + STR_HW_SEEK_UNUSED_FNAME, + STR_ERR_NO_FNAME, + // + // shared messages messages (14-18) + STR_HW_FORMAT_GAME, + STR_HW_WRITE_GAME, + STR_HW_READ_GAME, + STR_HW_WRITE_FILE, + STR_HW_READ_FILE, + // + // 3in1 messages (19-25) + STR_HW_3IN1_FORMAT_NOR, + STR_HW_3IN1_PREPARE_REBOOT, + STR_HW_3IN1_PLEASE_REBOOT, + STR_HW_3IN1_CLEAR_FLAG, + STR_HW_3IN1_DONE_DUMP, + STR_HW_3IN1_ERR_IDMODE, + STR_HW_3IN1_ERR_NOR, + // + // FTP specific messages (26-3) + STR_HW_FTP_SEEK_AP, + STR_HW_FTP_ERR_AP, + STR_HW_FTP_SEEK_FTP, + STR_HW_FTP_ERR_FTP, + STR_HW_FTP_LOGIN, + STR_HW_FTP_ERR_LOGIN, + STR_HW_FTP_DIR, + STR_HW_FTP_SLOW, + STR_HW_FTP_READ_ONLY, + // + // delete messages (35-36) + STR_HW_WARN_DELETE, + STR_HW_DID_DELETE, + // + // more messages for the file select widget (37-38) + STR_FS_READ, + STR_FS_WRITE, + // messages for the main menu (39) + STR_MM_WIPE, + // + STR_LAST }; bool stringsLoadFile(const char *fname); diff --git a/lang/sgm_german.ini b/lang/sgm_german.ini new file mode 100644 index 0000000..6d2f348 --- /dev/null +++ b/lang/sgm_german.ini @@ -0,0 +1,95 @@ +# Template for an external language file, allowing to translate savegame_manager. +# Some lines are C-format strings, any "%s" etc. should be kept. +# The strings should not exceed 256 bytes. +# You can add newline tags by adding '\n'; this is converted into a 'true' newline by the program. + +# The strings "0" and "1" should never be used; they are internal stuff for "empty" and "untranslated" strings. + +# 2-5: These are boot-time messages. +2=DS savegame manager\nVersion 0.2.5 semi-stable\nBy Pokedoc +# This is an error message appearing when the ini file can not be found. +3=Kann die ini-Datei nicht finden!\nBitte kopiere sie entweder\n1. in den selben Ordner wie die nds Datei, oder\n2. in das Hauptverzeichnis.\nWenn 1. nicht funktioniert, musst Du 2. verwenden +# Thsis is an error message called when the system tries to enter a mode that is not supported yet (e.g. DSi mode) +4=Dieser Modus ist noch nicht verfuegbar.\nBitte starte das System neu. +# This is an error called when the DLDI driver is not available. +5=DLDI konnte nicht gestartet werden! + +# 6-9: These are generic messages used by multiple backup modes. +# This message does just what it says. +6=Bitte nimm die Slot 1 Flashkarte heraus und lege ein Spiel ein.\nDruecke danach A. +# This message is called when the syste can't identify the game cartridge (by reading the game header) in Slot 1 due to bad contacts. +7=Ich kann das Spiel in Slot nicht erkennen.\nBitte nimm er heraus und lege es erneut ein. +# If the file you want to write is smaller than the save chip on the game cartridge, something must be wrong. +8=Dieses Spiel hat einen Spielstand, der groesser ist als die Datei, die Du schreiben willst. Bitte lege ein Spiel mit einem groesseren Spielstand ein.\nDruecke danach A. +# This is a generic "please reboot" message +9=Erledigt! Bitte schalte den DS aus\n(und starte das Programm neu, wenn Du mehr machen willst). + +# 10-13 These messages are used by the file select "widget". +# This tells you to select an existing file to read +10=Bitte waehle eine .sav Datei. +# This tells you to select an existing or new file to (over)write. +11=Bitte waehle eine .sav Datei, die ueberschrieben wird, oder druecke L+R, um eine neue Datei zu schreiben. +# This tells you that the program is lookign for an unused filename. +12=Bitte warten... ich suche einen passenden Dateinamen.\n%s +# And this is displayed when all valid filenames are already used (the program tries "gamename.number.sav", with number = 0..65535) +# ... so you should never see this message unless libfat/DLDI is broken! +13=ERROR: FEHLER: Konnte keinen Dateinamen finden, der nicht benutzt ist. Entweder hast Du sehr viele Dateien auf deiner Karte, oder Etwas funktioniert nicht. + +# 14-18: These messages are used to tell you when the program is reading/writing a save to the game or a file. +# This message is called when the save chip on your game is being formated in preparation of being written (needed for type 3 saves) +14=Ich werde gleich auf das Spiel schreiben.\nBitte etwas Geduld... +# Accessing the save +15=Ich schreibe gerade einen Spielstand auf Dein Spiel.\nBitte etwas Geduld... +16=Ich lese gerade einen Spielstand von Deinem Spiel.\nBitte etwas Geduld... +# Accessing the filesystem on your flash card or a remote FTP server +17=Schreibe Datei:\n%s +18=Lese Datei:\n%s + +# 19-25: These messages are used by the 3in1 method +# This message means that we format the 3in1 +19=Ich werde gleich auf Dein 3in1 schreiben.\nBitte etwas Geduld... +# This message is called when the "continue after reboot" flag is written. It is using the NOR memory as well, which takes some +# time. It also works with a dead 3in1 battery. +20=Bereite Neustart vor... +# A custom "please reboot" message for the 3in1 backup mode. +21=Der Spielstand ist nun auf Deinem 3in1. Bitte starte das Programm neu, um ihn auf die Flashkarte zu schreiben. +# This is a "please wait" message that removes the "continue after reboot" flag from the 3in1. +22=Bereite Auslesen vor...\nBitte etwas Geduld... +# This message is given after dumping a save with the 3in1. Since you can continue after that, we have a custom message. +23=Erledigt, Dein Spielstand ist nun sicher auf Deiner Flashkarte, unter dem Namen\n%s\nDruecke (B) um forzufahren. +# An error message when something goes wrong during 3in1 initialisation. Should never appear. +24=FEHLER!\nDas 3in1 ist noch im ID-Modus! +# An error message when something goes wrong during 3in1 use (data was not written successfully). Should never appear. +25=FEHLER!\nIch konnte nicht auf Dein 3in1 schreiben! + +# 26-34: These messages are used to give feedback on the FTP connection. +# Messages used during connection to an Access Point. +26=Suche nach einem Access Point...\nBitte etwas Geduld... +27=FEHLER!\nIch konnte keinen geigneten Access Point finden. Bitte konfiguriere Deine WiFi-Connection aus einem WiFi-faehigen Spiel heraus. +# Messages used during connection to the FTP server +28=Verbinde mit dem FTP-Server...\nBitte etwas Geduld... +29=FEHLER!\nKonnte keinen FTP-Server finden. Die Dokumentation sollte helfen (leider ist die noch auf Englisch) +# Messages used during login to the FTP server +30=Logge ein... +31=FEHLER!\nKonnte nicht in den FTP-Server einloggen. Stimmen Dein Benutzername und das Passwort in der ini-Datei? +# Message used to indicate FTP "DIR" command. Should be short, it needs to fit into one line! +32=Lese FTP-Verzeichnis... +# Message displayed when FTP access is slow (i.e. data transfered in units smaller than 512 bytes). Needs to fit into one line! +33=FTP ist gerade sehr lahm... +# Warning when writing to your FTP server fails (i.e. when 0 bytes have been written); this usually means that your write permissions are missing. +34=WARNUNG:\nKonnte nicht auf den FTP-Server schreiben! Hast Du Schreibrechte? + +# 35-36: These messages are used during a format command. +# Confirmation request for deleting a save without backup. +35=Dies wird Dein gesamtes Spiel loeschen, ohne einen Weg zurueck! WILLST DU DAS WIRKLICH MACHEN?\n\nDruecke R+oben+Y, um das zu bestaetigen! +# Save has been deleted. This gets a separate message, since it might fail. +36=Du wolltest es ja so. Dein Spielstand ist weg. Futsch.\n\nBitte starte den DS neu. + +# 37-38: Various strings that are inserted in fixed positions and other difficult-to-port stuff +# File select instructions, must fit into 4 lines +37=Bitte waehle eine .sav-Datei.\n (A) Wahl\n (B) Ein Verzeichnis hoch +38=Bitte waehle eine .sav-Datei.\n (A) Wahl\n (B) Ein Verzeichnis hoch\n (L+R) Neue Datei schreiben + +# 39: Title menu strings +# The initial '\n' is required to make the program not skip the first spaces. +39=\n DAS WIRD DEINEN SPIELSTAND\n KOMPLETT LOESCHEN ! diff --git a/lang/template.ini b/lang/template.ini new file mode 100644 index 0000000..65767bc --- /dev/null +++ b/lang/template.ini @@ -0,0 +1,95 @@ +# Template for an external language file, allowing to translate savegame_manager. +# Some lines are C-format strings, any "%s" etc. should be kept. +# The strings should not exceed 256 bytes. +# You can add newline tags by adding '\n'; this is converted into a 'true' newline by the program. + +# The strings "0" and "1" should never be used; they are internal stuff for "empty" and "untranslated" strings. + +# 2-5: These are boot-time messages. +2=DS savegame manager\nVersion 0.2.5 semi-stable\nBy Pokedoc +# This is an error message appearing when the ini file can not be found. +3=Unable to open ini file!\nPlease make sure that it is\n1. in this apps folder, or\n2. in the root folder\nIf 1. does not work, use 2. +# Thsis is an error message called when the system tries to enter a mode that is not supported yet (e.g. DSi mode) +4=This mode is DISABLED.\nPlease restart the system. +# This is an error called when the DLDI driver is not available. +5=DLDI initialisation error! + +# 6-9: These are generic messages used by multiple backup modes. +# This message does just what it says. +6=Please take out Slot 1 flash card and insert a game.\nPress A when done. +# This message is called when the syste can't identify the game cartridge (by reading the game header) in Slot 1 due to bad contacts. +7=I can't read the game inserted in slot 1.\nPlease eject and retry. +# If the file you want to write is smaller than the save chip on the game cartridge, something must be wrong. +8=This game has a save chip that is bigger than the save you are going to write. Please insert a different game.\nPress A when done. +# This is a generic "please reboot" message +9=Done! Please power off\n(and restart if you want to do more). + +# 10-13 These messages are used by the file select "widget". +# This tells you to select an existing file to read +10=Please select a .sav file. +# This tells you to select an existing or new file to (over)write. +11=Please select a file to overwrite, or press L+R in a folder to create a new file. +# This tells you that the program is lookign for an unused filename. +12=Please wait... searching for an unused filename.\nTrying: %s +# And this is displayed when all valid filenames are already used (the program tries "gamename.number.sav", with number = 0..65535) +# ... so you should never see this message unless libfat/DLDI is broken! +13=ERROR: Unable to get an unused nfilename! This means that you have more than 65536 saves!\n(wow!) + +# 14-18: These messages are used to tell you when the program is reading/writing a save to the game or a file. +# This message is called when the save chip on your game is being formated in preparation of being written (needed for type 3 saves) +14=Preparing to write to your game.\nPlease wait... +# Accessing the save +15=Writing save to your game.\nPlease wait... +16=Reading save from your game.\nPlease wait... +# Accessing the filesystem on your flash card or a remote FTP server +17=Writing file:\n%s +18=Reading file:\n%s + +# 19-25: These messages are used by the 3in1 method +# This message means that we format the 3in1 +19=Preparing to write to the 3in1.\nPlease wait... +# This message is called when the "continue after reboot" flag is written. It is using the NOR memory as well, which takes some +# time. It also works with a dead 3in1 battery. +20=Preparing reboot... +# A custom "please reboot" message for the 3in1 backup mode. +21=Save has been written to the 3in1. Please power off and restart this tool to finish the dump. +# This is a "please wait" message that removes the "continue after reboot" flag from the 3in1. +22=Preparing to dump your save...\nPlease wait... +# This message is given after dumping a save with the 3in1. Since you can continue after that, we have a custom message. +23=Done. Your game save has been dumped using your 3in1. Filename:\n%s\nPress (B) to continue. +# An error message when something goes wrong during 3in1 initialisation. Should never appear. +24=ERROR!\nID mode still active! +# An error message when something goes wrong during 3in1 use (data was not written successfully). Should never appear. +25=ERROR!\nWriting to NOR failed! + +# 26-34: These messages are used to give feedback on the FTP connection. +# Messages used during connection to an Access Point. +26=Connecting to an access point...\nplease wait... +27=ERROR!\nCould not find a compatible Access Point. Please configure your WiFi Connection from a WiFi-enabled game! +# Messages used during connection to the FTP server +28=Connecting to an FTP server...\nplease wait... +29=ERROR!\nCould not find an FTP server. Please refer to the documentation. +# Messages used during login to the FTP server +30=Logging in... +31=ERROR!\nCould not log in to the FTP server. Please verify your username and password are correct in your ini file. +# Message used to indicate FTP "DIR" command. Should be short, it needs to fit into one line! +32=Reading FTP directory... +# Message displayed when FTP access is slow (i.e. data transfered in units smaller than 512 bytes). Needs to fit into one line! +33=FTP is slow, please wait... +# Warning when writing to your FTP server fails (i.e. when 0 bytes have been written); this usually means that your write permissions are missing. +34=WARNING:\nCould not write to your FTP server! Maybe you forgot to enable write access? + +# 35-36: These messages are used during a format command. +# Confirmation request for deleting a save without backup. +35=This will WIPE OUT your entire save! ARE YOU SURE?\n\nPress R+up+Y to confim! +# Save has been deleted. This gets a separate message, since it might fail. +36=Done. Your game save has been PERMANENTLY deleted.\n\nPlease restart your DS. + +# 37-38: Various strings that are inserted in fixed positions and other difficult-to-port stuff +# File select instructions, must fit into 4 lines +37=Please select a .sav file\n (A) Select\n (B) One directory up +38=Please select a .sav file\n (A) Select\n (B) One directory up\n (L+R) cancel (new file) + +# 39: Title menu strings +# The initial '\n' is required to make the program not skip the first spaces. +39=\n WIPES OUT ALL SAVE DATA\n ON YOUR GAME ! diff --git a/savegame_manager.ini b/savegame_manager.ini new file mode 100644 index 0000000..0ec9a9a --- /dev/null +++ b/savegame_manager.ini @@ -0,0 +1,7 @@ +ftp_ip = 192.168.123.1 +ftp_user = test +ftp_pass = test +ftp_port = 8080 +#language = /sgm_german.ini + +[new chips] diff --git a/savegame_manager.odt b/savegame_manager.odt index 212d05ddd7a620c020f922cc6d0a55fe15ad8cb2..66260d4e95767e66d2c70807b78fc9942359ae16 100644 GIT binary patch delta 20596 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 delta 20053 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