Created
June 24, 2024 21:25
-
-
Save ariankordi/ea0ab58269d62ba8c18d1953d0f567b9 to your computer and use it in GitHub Desktop.
Tool using the FFL decomp (not straightforward to run) that extracts FFSDs from an FFL_ODB.dat, aka Wii U Mii Maker database
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// NOTE: Meant to be built in my FFL-Testing repo, placed in src. | |
// LDFLAGS="-lssl -lcrypto" make SRC=src/extract-ffsd-from-ffl-odb.cpp EXEC=extract-ffsd-from-ffl-odb | |
#include <filedevice/rio_FileDeviceMgr.h> | |
#include <nn/ffl.h> | |
// get mii studio title id | |
#include <nn/ffl/detail/FFLiFileWriteBuffer.h> | |
#include <nn/ffl/FFLiMiiData.h> | |
// convert utf 16 to utf 8 | |
#include <codecvt> | |
#include <locale> | |
#include <cstdio> | |
// NOTE: openssl is only included for base64 LOL! | |
#include <openssl/bio.h> | |
#include <openssl/evp.h> | |
#include <openssl/buffer.h> | |
#include <cstring> | |
#include <sys/stat.h> | |
#ifdef _WIN32 | |
#include <direct.h> | |
#define getcwd _getcwd | |
#define mkdir _mkdir | |
#else | |
#include <unistd.h> | |
#endif | |
#define private public | |
#include <nn/ffl/FFLiDatabaseFile.h> | |
#include <nn/ffl/FFLiManager.h> | |
//#include "../ffl/src/FFLiDatabaseFileAccessor.cpp" | |
// Function to encode data in Base64 | |
std::string base64_encode(const char* input, int length) { | |
BIO *bmem, *b64; | |
BUF_MEM *bptr; | |
b64 = BIO_new(BIO_f_base64()); | |
bmem = BIO_new(BIO_s_mem()); | |
b64 = BIO_push(b64, bmem); | |
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); // No newlines | |
BIO_write(b64, input, length); | |
BIO_flush(b64); | |
BIO_get_mem_ptr(b64, &bptr); | |
std::string encoded(bptr->data, bptr->length); | |
BIO_free_all(b64); | |
return encoded; | |
} | |
// NOTE: COPIED FROM FFLiDatabaseFileAccessor.cpp | |
enum FFLiFsFileResult | |
{ | |
FFLI_FS_FILE_RESULT_OK = 0, | |
FFLI_FS_FILE_RESULT_READ_BUFFER_EMPTY = 1, | |
FFLI_FS_FILE_RESULT_WRITE_BUFFER_EMPTY = 2, | |
FFLI_FS_FILE_RESULT_OUT_OF_MEMORY = 3, | |
FFLI_FS_FILE_RESULT_NOT_FOUND = 4 | |
}; | |
struct FFLiFsResult | |
{ | |
FFLiFsFileResult fileResult; | |
rio::RawErrorCode fsStatus; | |
}; | |
static bool CheckFFLiFsResult(const FFLiFsResult& result) | |
{ | |
if (result.fileResult != FFLI_FS_FILE_RESULT_OK) | |
return false; | |
if (result.fsStatus != rio::RAW_ERROR_OK) | |
return false; | |
return true; | |
} | |
FFLiFsResult LoadDatabaseOfficial(FFLiDatabaseFileOfficial* pOfficial, const char* pPath); | |
// clears stdin | |
void seek_to_next_line( void ) | |
{ | |
int c; | |
while( (c = fgetc( stdin )) != EOF && c != '\n' ); | |
} | |
int main(int argc, char *argv[]) { | |
// check whether first argument starts with -, to show usage | |
if(argc > 1 && argv[1][0] == '-' || argc == 2) { | |
printf("\e[0;34mUsage: %s <path to FFL_ODB.dat> <output folder>\e[0m\n", argv[0]); | |
return 0; | |
} | |
printf("before ffl init\n"); | |
// initialize filedevicemgr | |
// called by rio::Initialize | |
rio::FileDeviceMgr::createSingleton(); | |
/* | |
FFLInitDesc init_desc; | |
init_desc.fontRegion = FFL_FONT_REGION_0; | |
init_desc._c = false; | |
init_desc._10 = true; | |
// NOTE: NOT LOADING ANY RESOURCES HERE, NOT RENDERING!!! | |
FFLResourceDesc resource_desc; | |
FFLResult result = FFLInitResEx(&init_desc, &resource_desc); | |
if(result != FFL_RESULT_OK) { | |
printf("FFLInitResEx() failed with result: %d\n", (s32)result); | |
return 1; | |
} | |
int is_ffl_available = FFLIsAvailable(); | |
printf("ffl is available? %d\n", is_ffl_available); | |
if(is_ffl_available != 1) { | |
printf("FFLIsAvailable() != 1, aborting\n"); | |
return 1; | |
} | |
FFLiManager* pManager = FFLiManager::GetInstance(); | |
FFLiDatabaseManager& database_manager = pManager->GetDatabaseManager(); | |
const int index = 0; | |
FFLiCharInfo char_info; | |
const FFLDataSource data_source = FFL_DATA_SOURCE_OFFICIAL; | |
result = database_manager.PickupCharInfo(&char_info, data_source, NULL, index); | |
printf("PickupCharInfo result: %d\n", result); | |
*/ | |
FFLiFileWriteBuffer* file_write_buffer(static_cast<FFLiFileWriteBuffer*>(rio::MemUtil::alloc(sizeof(FFLiFileWriteBuffer), rio::FileDevice::cBufferMinAlignment))); | |
FFLiDatabaseFile* database_file = static_cast<FFLiDatabaseFile*>(rio::MemUtil::alloc(sizeof(FFLiDatabaseFile), rio::FileDevice::cBufferMinAlignment)); | |
printf("FFLiDatabaseFile->official.Init()\n"); | |
database_file->official.Init(); | |
FFLiDatabaseFileAccessor database_file_accessor(database_file, file_write_buffer); | |
printf("FFLiDatabaseFileAccessor.Init()\n"); | |
database_file_accessor.Init(); | |
char output_folder[256]; | |
// /home/arian/Downloads/2024-05-31-working/FFL_ODB.dat | |
// /home/arian/Downloads/1004a100/user/common/db/FFL_ODB.dat | |
// OBTAIN ARGUMENTS | |
if(argc < 2) { | |
// no arguments | |
// hopefully these scanfs won't allow more than 255 characters | |
printf("\e[1menter path to you FFL_ODB 🥺?\e[0m\n"); | |
scanf("%255[^\n]", database_file_accessor.m_PathOfficial); | |
printf("\e[1menter output folder path, we will put FFSD files here:\e[0m\n"); | |
seek_to_next_line(); | |
scanf("%255[^\n]", output_folder); | |
} else if(argc > 2) { | |
// all arguments | |
// copy only 256 characters | |
strncpy(database_file_accessor.m_PathOfficial, argv[1], 256); | |
strncpy(output_folder, argv[2], 256); | |
} | |
// make folder if it does not exist | |
#ifdef _WIN32 | |
if (mkdir(output_folder) != 0 && errno != EEXIST) { | |
#else | |
if (mkdir(output_folder, 0755) != 0 && errno != EEXIST) { | |
#endif | |
perror("Error creating output directory"); | |
return 1; | |
} | |
// TODO CHECK RESULT | |
//database_file_accessor.AfterConstruct(FFLiGetMiiStudioTitleID()); | |
// NOTE: 256 SIZE MAXMIMUM!!!!!!! | |
//strcpy(database_file_accessor.m_PathOfficial, "/home/arian/.local/share/Cemu/mlc01/usr/save/00050010/1004a100/user/common/db/FFL_ODB.dat"); | |
//printf("calling FFLiDatabaseFileAccessor::BootLoad()...\n"); | |
//database_file_accessor.m_IsPathSet = true; | |
// TODO CHECK RESULT | |
//database_file_accessor.BootLoad(); | |
printf("calling LoadDatabaseOfficial()...\n"); | |
FFLiFsResult result = LoadDatabaseOfficial(&database_file->official, database_file_accessor.m_PathOfficial); | |
if(!CheckFFLiFsResult(result)) { | |
char error[512]; | |
sprintf(error, "LoadDatabaseOfficial(%s) FAILED!!!!", database_file_accessor.m_PathOfficial); | |
perror(error); | |
return 1; | |
} | |
//FFLiDatabaseFileAccessor& database_file_accessor = database_manager.GetDatabaseFileAccessor(); | |
FFLiDatabaseFileOfficial database_file_official = database_file_accessor.GetDatabaseFile()->official; | |
// TODO CHECK RESULT | |
/* database_file_official.Get(&char_info, index, true, true); | |
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert; | |
// print in bold | |
printf("mii index 0 name: %s\n", | |
convert.to_bytes((char16_t*) char_info.name).c_str()); | |
FFLiMiiDataOfficial* firstMii = &database_file_official.m_MiiDataOfficial[0]; | |
printf("no endian swap: "); | |
for(int i = 0; i < sizeof(FFLiMiiDataOfficial); i++) { | |
printf("%02x", reinterpret_cast<uint8_t*>(firstMii)[i]); | |
} | |
printf("\n%s\n", base64_encode(reinterpret_cast<char*>(firstMii), sizeof(FFLiMiiDataOfficial)).c_str()); | |
*/ | |
int miis_written = 0; | |
// NOTE: 3000 is the maximum amount of entries in FFLiDatabaseFileOfficial | |
for(int i = 0; i < 3000; i++) { | |
FFLiMiiDataOfficial* mii = &database_file_official.m_MiiDataOfficial[i]; | |
if(FFLiIsNullMiiID(&mii->CreatorID())) { | |
//printf("MII DATA OFFICIAL INDEX %d IS NULL, SKIPPING!!!!!!\n", i); | |
// skip null mii | |
continue; | |
} | |
/*FFLiCharInfo char_info; | |
if(!database_file_official.Get(&char_info, i, false, true)) { | |
continue; | |
}*/ | |
// NOTE: SET MII VERSION TO 3DS | |
// the mii maker does this, and clears favorite flag | |
// whenever making a qr code, though note that it | |
// makes the mii version for wii u miis 4 in networking + nnas | |
//char_info.miiVersion = 3; | |
mii->SetMiiVersion(3); | |
// 10 characters long | |
std::u16string u16str((char16_t*)mii->Name(), 10); | |
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert; | |
std::string mii_name_string = convert.to_bytes(u16str); | |
const char* mii_name = mii_name_string.c_str(); | |
// convert name to utf-8 | |
printf("mii index %d name: %s\ndata: ", i, mii_name); | |
FFLStoreData store_data; | |
FFLiMiiDataOfficialToStoreDataCFL(static_cast<FFLiStoreDataCFL&>(store_data), *mii); | |
// TODO CHECK RESULT | |
//FFLiCharInfoToStoreDataCFL(reinterpret_cast<FFLiStoreDataCFL*>(&store_data), &char_info); | |
char* mii_bytes = reinterpret_cast<char*>(&store_data); | |
printf("%s\n", base64_encode(mii_bytes, sizeof(FFLStoreData)).c_str()); | |
char filename[300]; | |
sprintf(filename, "%s/%d %s.ffsd", output_folder, i, mii_name); | |
FILE* file = fopen(filename, "w+"); | |
if(file == NULL) { | |
char error[512]; | |
sprintf(error, "fopen(%s) failed", filename); | |
perror(error); | |
// return and EXIT PROGRAM | |
return 1;//continue; | |
} | |
fwrite(mii_bytes, sizeof(FFLStoreData), 1, file); | |
fclose(file); | |
miis_written++; | |
} | |
// if no miis were written | |
if(!miis_written) { | |
printf("hmmm... no miis were written.\n"); | |
} else { | |
printf("all miis were successfully written to %s :).\n", output_folder); | |
} | |
// success... free... | |
rio::MemUtil::free(database_file); | |
database_file = nullptr; | |
rio::MemUtil::free(file_write_buffer); | |
file_write_buffer = nullptr; | |
FFLExit(); | |
// press any key to continue, only on windows | |
#ifdef _WIN32 | |
printf("Press any key to continue...\n"); | |
_getch(); | |
#endif | |
return 0; | |
} | |
// NOTE: COPIED FROM FFLiDatabaseFileAccessor.cpp | |
FFLiFsResult ReadFile(void* pDst, u32 size, const char* pPath) | |
{ | |
rio::NativeFileDevice* device = rio::FileDeviceMgr::instance()->getNativeFileDevice(); | |
rio::FileHandle fileHandle; | |
if (!device->tryOpen(&fileHandle, pPath, rio::FileDevice::FILE_OPEN_FLAG_READ)) | |
{ | |
rio::RawErrorCode status = device->getLastRawError(); | |
RIO_ASSERT(status != rio::RAW_ERROR_OK); | |
return FFLiFsResult { FFLI_FS_FILE_RESULT_OK, status }; | |
} | |
u32 readSize = 0; | |
if (!fileHandle.tryRead(&readSize, static_cast<u8*>(pDst), size)) | |
{ | |
fileHandle.tryClose(); | |
rio::RawErrorCode status = device->getLastRawError(); | |
RIO_ASSERT(status != rio::RAW_ERROR_OK); | |
return FFLiFsResult { FFLI_FS_FILE_RESULT_OK, status }; | |
} | |
if (readSize == 0) | |
{ | |
fileHandle.tryClose(); | |
return FFLiFsResult { FFLI_FS_FILE_RESULT_READ_BUFFER_EMPTY }; | |
} | |
else | |
{ | |
if (!fileHandle.tryClose()) | |
{ | |
rio::RawErrorCode status = device->getLastRawError(); | |
RIO_ASSERT(status != rio::RAW_ERROR_OK); | |
return FFLiFsResult { FFLI_FS_FILE_RESULT_OK, status }; | |
} | |
} | |
return FFLiFsResult { FFLI_FS_FILE_RESULT_OK, rio::RAW_ERROR_OK }; | |
} | |
FFLiFsResult LoadDatabaseOfficial(FFLiDatabaseFileOfficial* pOfficial, const char* pPath) | |
{ | |
FFLiFsResult result = ReadFile(pOfficial, sizeof(FFLiDatabaseFileOfficial), pPath); | |
if (!CheckFFLiFsResult(result)) | |
return result; | |
#if __BYTE_ORDER__ != __ORDER_BIG_ENDIAN__ | |
pOfficial->SwapEndian(false); | |
#endif // __BYTE_ORDER__ | |
return FFLiFsResult { FFLI_FS_FILE_RESULT_OK, rio::RAW_ERROR_OK }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment