One problem in many nds homebrew games is improper handling of missing assets and improper handling when user content could not be created.
A commercial game that is being shipped on its own cartrige can rely on the fact all game assets are available. There is no way to delete files from it. In this case, you don’t need to keep attention if fopen or fread succeded, because they always do.
With nds homebrew it’s a whole different. The user of your game has to copy related game assets and the .nds executable itself to his/her flashcard. Everything can go wrong during this process. Beside missing files, it could be also a flashcard compatibility problem, such as an invalid DLDI driver. In these cases, many games just die silently, rather that displaying an error-message to let the user know what happened.
You often find file loading code such as (don’t copy!):
1 2 3 4 5 6 7 8 9 10 11 | uint8_t* LoadFile(const char* filename) { FILE* file = fopen(filename, "rb"); fseek(file, 0, SEEK_END); size_t size = ftell(file); rewind(file); uint8_t* buffer = (uint8_t*)malloc(size); fread(buffer, size, sizeof(uint8_t), file); fclose(file); return buffer; } |
All those functions have return values, which must not be ignored. If any function call fails here, it’s very unlikely the program will continue to operate correctly.
We do have to find a better way to ensure loading either works “in all cases” or does not. The very first thing that comes to mind is to use some sort of file archive. If you bundle all assets in one archive, there is a good chance everything is available and there is no way to delete files from it. At application startup, test if the archive is available, open it and read a test file to confirm the system works. If it does not, display a human readable error message, numerical error codes don’t count.
Another critical part is deployment. If there is more than one file to deploy, there is also more than one chance to fail. So, it would be quite good if the user needs to deploy only one file for the entire game, rather than the .nds file and the archive seperately. This can be done by appending the archive to the .nds file!
I’ve created my own tool set for all this, which I won’t share, but there is a free public solution for it called Embedded File System Library as well. I’ve never used EFS, but it should do exactly what we need if we trust its documentation. If we use this approach, an archive appended to the .ds file, we can be pretty sure assets are available as long as the appended archive is available. Only two problems left
- Incompatible DLDI driver.
To detect an incompatible DLDI driver, check the return value of fatInitDefault. This function is part of libfat. If it returns false, display an appropriate message and make the user aware it could be due to an incompatible DLDI driver. This gives him/her a chance to solve the problem rather than just being frustrated, deleting your game and sending you hate-mails.
- User aborted the copy process before the whole file was written to the flashcard.
To check if the whole file was written to the flashcard could be done by appending a magic value at the end of the archive. At application startup seek to this position, read the value and compare it with what it should be. If it’s different, display a warning that the attached data seems to be corrupted. You could let the user continue with the game, he/she will know what could have caused the problem when the game crashes eventually.
What I do to ensure file i/o works
Create an archive of all assets, append it to the .nds file as well as a magic value.
The magic value can be a string like “I FEEL GOOD” at the end of the file. At application startup, initialize libfat and repsond to its return value. Display an error message when fatInitDefault failed.
Open the application file where the archive is appened. Check if this operation succeded and display an error message if anything went wrong.
Seek to and read the “I FEEL GOOD” magic value. If it’s different, display a “data is corrupted” warning. Open and read a file from the archive to verify the system works, display an error if it fails.
From now on assume the file i/o system will work and continue with further initialization.
If all checks last longer than a few milliseconds, display a “Please wait, initializing file system…” message before.
Source code snippet to verify that at the end of the program file is “I FEEL GOOD” located:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | // opens the file specified by filename and checks if at the // end of the file is "I FEED GOOD" located. // returns true on success, false otherwise. If return value // is false, further error information is stored at errorString. bool VerifyProgramFile(const char* filename, char* errorString) { const char* const MAGICVALUE="I FEEL GOOD"; // open file FILE *file = fopen(filename, "rb"); if(file == NULL) { sprintf(errorString, "Cannot open file '%s'.", filename); return false; } // seek to magic value offset fseek(file, 0, SEEK_END); size_t magicOffset = ftell(file) - strlen(MAGICVALUE); if(fseek(file, magicOffset, SEEK_SET) != 0) { // could not seek to magic sprintf(errorString, "Data in file '%s' seems to be corrupted.", filename); fclose(file); return false; } // read magic value char magicValue[64]; if(fread(magicValue, sizeof(char), strlen(MAGICVALUE), file) != strlen(MAGICVALUE)) { // could not read magic sprintf(errorString, "Data in file '%s' seems to be corrupted.", filename); fclose(file); return false; } // compare magic value if(memcmp(magicValue, MAGICVALUE, strlen(MAGICVALUE)) != 0) { // magic is different sprintf(errorString, "Data in file '%s' seems to be corrupted.", filename); fclose(file); return false; } // all tests successfully passed fclose(file); return true; } |
Source code snippet of application startup code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | int main() { if(!fatInitDefault()) { DisplayMessage("libfat file system initialization failed. Did you apply the correct DLDI driver?"); Halt(); } char errorString[1024]; if(!VerifyProgramFile(pathToFileArchive, errorString)) { DisplayMessage(errorString); WaitForUserConfirmation(); } // open archive and test if a file // can be read from the archive. if all tests // succeed, continue with application initialization... return 0; } |

[...] When you aim for a game with a decent quality, you can’t #include data as this would blow up memory, you have to load resources from the file system. To be more precise, you load only resources to memory, that you actually need in the particular level (file i/o handling in nds homebrew). [...]
Add A Comment