Feature: Move game database from flatfile to SQLite3
@@ -41,6 +41,7 @@ - GBA I/O: Clear JOYSTAT RECV flag when reading JOY_RECV registers
- GBA I/O: Set JOYSTAT TRANS flag when writing JOY_TRANS registers - Qt: Improved HiDPI support - Qt: Expose configuration directory + - Feature: Move game database from flatfile to SQLite3 0.5.2: (2016-12-31) Bugfixes:
@@ -15,6 +15,7 @@ set(USE_MINIZIP ON CACHE BOOL "Whether or not to enable external minizip support")
set(USE_PNG ON CACHE BOOL "Whether or not to enable PNG support") set(USE_LIBZIP ON CACHE BOOL "Whether or not to enable LIBZIP support") set(USE_MAGICK ON CACHE BOOL "Whether or not to enable ImageMagick support") +set(USE_SQLITE3 ON CACHE BOOL "Whether or not to enable SQLite3 support") set(M_CORE_GBA ON CACHE BOOL "Build Game Boy Advance core") set(M_CORE_GB ON CACHE BOOL "Build Game Boy core") set(USE_LZMA ON CACHE BOOL "Whether or not to enable 7-Zip support")@@ -362,6 +363,7 @@ find_feature(USE_LIBZIP "libzip")
find_feature(USE_MAGICK "MagickWand") find_feature(USE_EPOXY "epoxy") find_feature(USE_CMOCKA "cmocka") +find_feature(USE_SQLITE3 "sqlite3") # Features set(DEBUGGER_SRC@@ -544,6 +546,14 @@ set(OPENGLES2_LIBRARY ${EPOXY_LIBRARIES})
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libepoxy0") endif() +if(USE_SQLITE3) + list(APPEND FEATURES SQLITE3) + include_directories(AFTER ${SQLITE3_INCLUDE_DIRS}) + list(APPEND DEPENDENCY_LIB ${SQLITE3_LIBRARIES}) + set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libsqlite3-0") + list(APPEND FEATURE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/feature/sqlite3/no-intro.c") +endif() + set(TEST_SRC ${CORE_TEST_SRC}) if(M_CORE_GB) add_definitions(-DM_CORE_GB)@@ -840,6 +850,7 @@ message(STATUS " GIF recording: ${USE_MAGICK}")
message(STATUS " Screenshot/advanced savestate support: ${USE_PNG}") message(STATUS " ZIP support: ${SUMMARY_ZIP}") message(STATUS " 7-Zip support: ${USE_LZMA}") + message(STATUS " SQLite3 game database: ${USE_SQLITE3}") message(STATUS " OpenGL support: ${SUMMARY_GL}") message(STATUS "Frontends:") message(STATUS " Qt: ${BUILD_QT}")
@@ -125,6 +125,7 @@ - libedit: for command-line debugger support.
- ffmpeg or libav: for video recording. - libzip or zlib: for loading ROMs stored in zip files. - ImageMagick: for GIF recording. +- SQLite3: for game databases. Both libpng and zlib are included with the emulator, so they do not need to be externally compiled first.
@@ -1,10 +1,10 @@
-/* Copyright (c) 2013-2015 Jeffrey Pfau +/* Copyright (c) 2013-2017 Jeffrey Pfau * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#ifndef NOINTRO_H -#define NOINTRO_H +#ifndef NO_INTRO_H +#define NO_INTRO_H #include <mgba-util/common.h>@@ -13,7 +13,6 @@
struct NoIntroGame { const char* name; const char* romName; - const char* description; size_t size; uint32_t crc32; uint8_t md5[16];@@ -24,7 +23,8 @@
struct NoIntroDB; struct VFile; -struct NoIntroDB* NoIntroDBLoad(struct VFile* vf); +struct NoIntroDB* NoIntroDBLoad(const char* path); +bool NoIntroDBLoadClrMamePro(struct NoIntroDB* db, struct VFile* vf); void NoIntroDBDestroy(struct NoIntroDB* db); bool NoIntroDBLookupGameByCRC(const struct NoIntroDB* db, uint32_t crc32, struct NoIntroGame* game);
@@ -87,6 +87,10 @@ #ifndef USE_PTHREADS
#cmakedefine USE_PTHREADS #endif +#ifndef USE_SQLITE3 +#cmakedefine USE_SQLITE3 +#endif + #ifndef USE_ZLIB #cmakedefine USE_ZLIB #endif
@@ -0,0 +1,282 @@
+/* Copyright (c) 2013-2017 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "no-intro.h" + +#include <mgba-util/string.h> +#include <mgba-util/vector.h> +#include <mgba-util/vfs.h> + +#include <sqlite3.h> + +struct NoIntroDB { + sqlite3* db; + sqlite3_stmt* crc32; +}; + +struct NoIntroDB* NoIntroDBLoad(const char* path) { + struct NoIntroDB* db = malloc(sizeof(*db)); + + if (sqlite3_open(path, &db->db)) { + goto error; + } + + static const char createTables[] = + "PRAGMA foreign_keys = ON;\n" + "CREATE TABLE IF NOT EXISTS gamedb (" + "dbid INTEGER NOT NULL PRIMARY KEY ASC," + "name TEXT," + "version TEXT," + "CONSTRAINT versioning UNIQUE (name, version)" + ");\n" + "CREATE TABLE IF NOT EXISTS games (" + "gid INTEGER NOT NULL PRIMARY KEY ASC," + "name TEXT," + "dbid INTEGER NOT NULL REFERENCES gamedb(dbid) ON DELETE CASCADE" + ");\n" + "CREATE TABLE IF NOT EXISTS roms (" + "name TEXT," + "size INTEGER," + "crc32 INTEGER," + "md5 BLOB," + "sha1 BLOB," + "flags INTEGER DEFAULT 0," + "gid INTEGER NOT NULL REFERENCES games(gid) ON DELETE CASCADE" + ");\n" + "CREATE INDEX IF NOT EXISTS crc32 ON roms (crc32);"; + if (sqlite3_exec(db->db, createTables, NULL, NULL, NULL)) { + goto error; + } + + static const char selectRom[] = "SELECT * FROM games JOIN roms USING (gid) WHERE roms.crc32 = ?;"; + if (sqlite3_prepare_v2(db->db, selectRom, -1, &db->crc32, NULL)) { + goto error; + } + + return db; + +error: + if (db->crc32) { + sqlite3_finalize(db->crc32); + } + NoIntroDBDestroy(db); + return NULL; + +} + +bool NoIntroDBLoadClrMamePro(struct NoIntroDB* db, struct VFile* vf) { + struct NoIntroGame buffer = { 0 }; + + sqlite3_stmt* gamedbTable = NULL; + sqlite3_stmt* gamedbDrop = NULL; + sqlite3_stmt* gameTable = NULL; + sqlite3_stmt* romTable = NULL; + char* fieldName = NULL; + sqlite3_int64 currentGame = -1; + sqlite3_int64 currentDb = -1; + char* dbType = NULL; + char* dbVersion = NULL; + char line[512]; + + static const char insertGamedb[] = "INSERT INTO gamedb (name, version) VALUES (?, ?);"; + if (sqlite3_prepare_v2(db->db, insertGamedb, -1, &gamedbTable, NULL)) { + return false; + } + + static const char deleteGamedb[] = "DELETE FROM gamedb WHERE name = ? AND version < ?;"; + if (sqlite3_prepare_v2(db->db, deleteGamedb, -1, &gamedbDrop, NULL)) { + return false; + } + + static const char insertGame[] = "INSERT INTO games (dbid, name) VALUES (?, ?);"; + if (sqlite3_prepare_v2(db->db, insertGame, -1, &gameTable, NULL)) { + return false; + } + + static const char insertRom[] = "INSERT INTO roms (gid, name, size, crc32, md5, sha1, flags) VALUES (:game, :name, :size, :crc32, :md5, :sha1, :flags);"; + if (sqlite3_prepare_v2(db->db, insertRom, -1, &romTable, NULL)) { + return false; + } + + while (true) { + ssize_t bytesRead = vf->readline(vf, line, sizeof(line)); + if (!bytesRead) { + break; + } + ssize_t i; + const char* token; + for (i = 0; i < bytesRead; ++i) { + while (isspace((int) line[i]) && i < bytesRead) { + ++i; + } + if (i >= bytesRead) { + break; + } + token = &line[i]; + while (!isspace((int) line[i]) && i < bytesRead) { + ++i; + } + if (i >= bytesRead) { + break; + } + switch (token[0]) { + case '(': + if (!fieldName) { + break; + } + if (strcmp(fieldName, "clrmamepro") == 0) { + free((void*) dbType); + free((void*) dbVersion); + dbType = NULL; + dbVersion = NULL; + currentDb = -1; + currentGame = -1; + } else if (currentDb >= 0 && strcmp(fieldName, "game") == 0) { + free((void*) buffer.name); + free((void*) buffer.romName); + memset(&buffer, 0, sizeof(buffer)); + currentGame = -1; + } else if (currentDb >= 0 && strcmp(fieldName, "rom") == 0) { + sqlite3_clear_bindings(gameTable); + sqlite3_reset(gameTable); + sqlite3_bind_int64(gameTable, 1, currentDb); + sqlite3_bind_text(gameTable, 2, buffer.name, -1, SQLITE_TRANSIENT); + sqlite3_step(gameTable); + currentGame = sqlite3_last_insert_rowid(db->db); + } + free(fieldName); + fieldName = NULL; + break; + case ')': + if (currentDb < 0 && dbType && dbVersion) { + sqlite3_clear_bindings(gamedbDrop); + sqlite3_reset(gamedbDrop); + sqlite3_bind_text(gamedbDrop, 1, dbType, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(gamedbDrop, 2, dbVersion, -1, SQLITE_TRANSIENT); + sqlite3_step(gamedbDrop); + + sqlite3_clear_bindings(gamedbTable); + sqlite3_reset(gamedbTable); + sqlite3_bind_text(gamedbTable, 1, dbType, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(gamedbTable, 2, dbVersion, -1, SQLITE_TRANSIENT); + if (sqlite3_step(gamedbTable) == SQLITE_DONE) { + currentDb = sqlite3_last_insert_rowid(db->db); + } + free((void*) dbType); + free((void*) dbVersion); + dbType = NULL; + dbVersion = NULL; + } + if (currentGame >= 0 && buffer.romName) { + sqlite3_clear_bindings(romTable); + sqlite3_reset(romTable); + sqlite3_bind_int64(romTable, 1, currentGame); + sqlite3_bind_text(romTable, 2, buffer.romName, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(romTable, 3, buffer.size); + sqlite3_bind_int(romTable, 4, buffer.crc32); + sqlite3_bind_blob(romTable, 5, buffer.md5, sizeof(buffer.md5), NULL); + sqlite3_bind_blob(romTable, 6, buffer.sha1, sizeof(buffer.sha1), NULL); + sqlite3_bind_int(romTable, 7, buffer.verified); + sqlite3_step(romTable); + free((void*) buffer.romName); + buffer.romName = NULL; + } + break; + case '"': + ++token; + for (; line[i] != '"' && i < bytesRead; ++i); + // Fall through + default: + line[i] = '\0'; + if (fieldName) { + if (currentGame >= 0) { + if (strcmp("name", fieldName) == 0) { + free((void*) buffer.romName); + buffer.romName = strdup(token); + } else if (strcmp("size", fieldName) == 0) { + char* end; + unsigned long value = strtoul(token, &end, 10); + if (end) { + buffer.size = value; + } + } else if (strcmp("crc", fieldName) == 0) { + char* end; + unsigned long value = strtoul(token, &end, 16); + if (end) { + buffer.crc32 = value; + } + } else if (strcmp("md5", fieldName) == 0) { + size_t b; + for (b = 0; b < sizeof(buffer.md5) && token && *token; ++b) { + token = hex8(token, &buffer.md5[b]); + } + } else if (strcmp("sha1", fieldName) == 0) { + size_t b; + for (b = 0; b < sizeof(buffer.sha1) && token && *token; ++b) { + token = hex8(token, &buffer.sha1[b]); + } + } else if (strcmp("flags", fieldName) == 0) { + buffer.verified = strcmp("verified", fieldName) == 0; + } + } else if (currentDb >= 0) { + if (strcmp("name", fieldName) == 0) { + free((void*) buffer.name); + buffer.name = strdup(token); + } + } else { + if (strcmp("name", fieldName) == 0) { + free((void*) dbType); + dbType = strdup(token); + } else if (strcmp("version", fieldName) == 0) { + free((void*) dbVersion); + dbVersion = strdup(token); + } + } + free(fieldName); + fieldName = NULL; + } else { + fieldName = strdup(token); + } + break; + } + } + } + + free((void*) buffer.name); + free((void*) buffer.romName); + free((void*) dbType); + free((void*) dbVersion); + + sqlite3_finalize(gameTable); + sqlite3_finalize(romTable); + + sqlite3_exec(db->db, "VACUUM", NULL, NULL, NULL); + + return true; +} + +void NoIntroDBDestroy(struct NoIntroDB* db) { + sqlite3_close(db->db); + free(db); +} + +bool NoIntroDBLookupGameByCRC(const struct NoIntroDB* db, uint32_t crc32, struct NoIntroGame* game) { + if (!db) { + return false; + } + sqlite3_clear_bindings(db->crc32); + sqlite3_reset(db->crc32); + sqlite3_bind_int(db->crc32, 1, crc32); + if (sqlite3_step(db->crc32) != SQLITE_ROW) { + return false; + } + game->name = (const char*) sqlite3_column_text(db->crc32, 1); + game->romName = (const char*) sqlite3_column_text(db->crc32, 3); + game->size = sqlite3_column_int(db->crc32, 4); + game->crc32 = sqlite3_column_int(db->crc32, 5); + // TODO: md5/sha1 + game->verified = sqlite3_column_int(db->crc32, 8); + return true; +}
@@ -19,11 +19,12 @@
#include <mgba/core/version.h> #include <mgba/internal/gba/video.h> #include <mgba-util/socket.h> -#include <mgba-util/nointro.h> #include <mgba-util/vfs.h> -/* -#include "feature/commandline.h" -*/ + +#ifdef USE_SQLITE3 +#include "feature/sqlite3/no-intro.h" +#endif + using namespace QGBA; static GBAApp* g_app = nullptr;@@ -208,19 +209,22 @@ return path;
} bool GBAApp::reloadGameDB() { +#ifdef USE_SQLITE3 NoIntroDB* db = nullptr; - VFile* vf = VFileDevice::open(dataDir() + "/nointro.dat", O_RDONLY); - if (vf) { - db = NoIntroDBLoad(vf); - vf->close(vf); - } + db = NoIntroDBLoad((m_configController.configDir() + "/nointro.sqlite3").toLocal8Bit().constData()); if (db && m_db) { NoIntroDBDestroy(m_db); } if (db) { + VFile* vf = VFileDevice::open(dataDir() + "/nointro.dat", O_RDONLY); + if (vf) { + NoIntroDBLoadClrMamePro(db, vf); + vf->close(vf); + } m_db = db; return true; } +#endif return false; }
@@ -15,7 +15,9 @@ #endif
#ifdef M_CORE_GBA #include <mgba/internal/gba/gba.h> #endif -#include <mgba-util/nointro.h> +#ifdef USE_SQLITE3 +#include "feature/sqlite3/no-intro.h" +#endif using namespace QGBA;@@ -28,7 +30,9 @@ if (!controller->isLoaded()) {
return; } +#ifdef USE_SQLITE3 const NoIntroDB* db = GBAApp::app()->gameDB(); +#endif uint32_t crc32 = 0; GameController::Interrupter interrupter(controller);@@ -67,6 +71,7 @@ break;
} if (crc32) { m_ui.crc->setText(QString::number(crc32, 16)); +#ifdef USE_SQLITE3 if (db) { NoIntroGame game{}; if (NoIntroDBLookupGameByCRC(db, crc32, &game)) {@@ -77,6 +82,9 @@ }
} else { m_ui.name->setText(tr("(no database present)")); } +#else + m_ui.name->hide(); +#endif } else { m_ui.crc->setText(tr("(unknown)")); m_ui.name->setText(tr("(unknown)"));
@@ -48,7 +48,7 @@ #include <mgba/internal/gb/gb.h>
#include <mgba/internal/gb/video.h> #endif #include "feature/commandline.h" -#include <mgba-util/nointro.h> +#include "feature/sqlite3/no-intro.h" #include <mgba-util/vfs.h> using namespace QGBA;@@ -851,15 +851,16 @@ default:
break; } - if (db && crc32) { - NoIntroDBLookupGameByCRC(db, crc32, &game); + char gameTitle[17] = { '\0' }; + mCore* core = m_controller->thread()->core; + core->getGameTitle(core, gameTitle); + title = gameTitle; + +#ifdef USE_SQLITE3 + if (db && crc32 && NoIntroDBLookupGameByCRC(db, crc32, &game)) { title = QLatin1String(game.name); - } else { - char gameTitle[17] = { '\0' }; - mCore* core = m_controller->thread()->core; - core->getGameTitle(core, gameTitle); - title = gameTitle; } +#endif } MultiplayerController* multiplayer = m_controller->multiplayerController(); if (multiplayer && multiplayer->attached() > 1) {
@@ -1,279 +0,0 @@
-/* Copyright (c) 2013-2015 Jeffrey Pfau - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include <mgba-util/nointro.h> - -#include <mgba-util/table.h> -#include <mgba-util/vector.h> -#include <mgba-util/vfs.h> - -#define KEY_STACK_SIZE 8 - -struct NoIntroDB { - struct Table categories; - struct Table gameCrc; -}; - -struct NoIntroItem { - union { - struct Table hash; - char* string; - }; - enum NoIntroItemType { - NI_HASH, - NI_STRING - } type; -}; - -DECLARE_VECTOR(NoIntroCategory, struct NoIntroItem*); -DEFINE_VECTOR(NoIntroCategory, struct NoIntroItem*); - -static void _indexU32x(struct NoIntroDB* db, struct Table* table, const char* categoryKey, const char* key) { - struct NoIntroCategory* category = HashTableLookup(&db->categories, categoryKey); - if (!category) { - return; - } - TableInit(table, 256, 0); - char* tmpKey = strdup(key); - const char* keyStack[KEY_STACK_SIZE] = { tmpKey }; - size_t i; - for (i = 1; i < KEY_STACK_SIZE; ++i) { - char* next = strchr(keyStack[i - 1], '.'); - if (!next) { - break; - } - next[0] = '\0'; - keyStack[i] = next + 1; - } - for (i = 0; i < NoIntroCategorySize(category); ++i) { - struct NoIntroItem* item = *NoIntroCategoryGetPointer(category, i); - if (!item) { - continue; - } - struct NoIntroItem* keyloc = item; - size_t s; - for (s = 0; s < KEY_STACK_SIZE && keyStack[s]; ++s) { - if (keyloc->type != NI_HASH) { - keyloc = 0; - break; - } - keyloc = HashTableLookup(&keyloc->hash, keyStack[s]); - if (!keyloc) { - break; - } - } - if (!keyloc || keyloc->type != NI_STRING) { - continue; - } - char* end; - uint32_t key = strtoul(keyloc->string, &end, 16); - if (!end || *end) { - continue; - } - TableInsert(table, key, item); - } - free(tmpKey); -} - -static void _itemDeinit(void* value) { - struct NoIntroItem* item = value; - switch (item->type) { - case NI_STRING: - free(item->string); - break; - case NI_HASH: - HashTableDeinit(&item->hash); - break; - } - free(item); -} - -static void _dbDeinit(void* value) { - struct NoIntroCategory* category = value; - size_t i; - for (i = 0; i < NoIntroCategorySize(category); ++i) { - struct NoIntroItem* item = *NoIntroCategoryGetPointer(category, i); - switch (item->type) { - case NI_STRING: - free(item->string); - break; - case NI_HASH: - HashTableDeinit(&item->hash); - break; - } - free(item); - } - NoIntroCategoryDeinit(category); -} - -static bool _itemToGame(const struct NoIntroItem* item, struct NoIntroGame* game) { - if (item->type != NI_HASH) { - return false; - } - struct NoIntroItem* subitem; - struct NoIntroItem* rom; - - memset(game, 0, sizeof(*game)); - subitem = HashTableLookup(&item->hash, "name"); - if (subitem && subitem->type == NI_STRING) { - game->name = subitem->string; - } - subitem = HashTableLookup(&item->hash, "description"); - if (subitem && subitem->type == NI_STRING) { - game->description = subitem->string; - } - - rom = HashTableLookup(&item->hash, "rom"); - if (!rom || rom->type != NI_HASH) { - return false; - } - subitem = HashTableLookup(&rom->hash, "name"); - if (subitem && subitem->type == NI_STRING) { - game->romName = subitem->string; - } - subitem = HashTableLookup(&rom->hash, "size"); - if (subitem && subitem->type == NI_STRING) { - char* end; - game->size = strtoul(subitem->string, &end, 0); - if (!end || *end) { - game->size = 0; - } - } - // TODO: md5, sha1 - subitem = HashTableLookup(&rom->hash, "flags"); - if (subitem && subitem->type == NI_STRING && strcmp(subitem->string, "verified")) { - game->verified = true; - } - - return true; -} - -struct NoIntroDB* NoIntroDBLoad(struct VFile* vf) { - struct NoIntroDB* db = malloc(sizeof(*db)); - HashTableInit(&db->categories, 0, _dbDeinit); - char line[512]; - struct { - char* key; - struct NoIntroItem* item; - } keyStack[KEY_STACK_SIZE]; - memset(keyStack, 0, sizeof(keyStack)); - struct Table* parent = 0; - - size_t stackDepth = 0; - while (true) { - ssize_t bytesRead = vf->readline(vf, line, sizeof(line)); - if (!bytesRead) { - break; - } - ssize_t i; - const char* token; - for (i = 0; i < bytesRead; ++i) { - while (isspace((int) line[i]) && i < bytesRead) { - ++i; - } - if (i >= bytesRead) { - break; - } - token = &line[i]; - while (!isspace((int) line[i]) && i < bytesRead) { - ++i; - } - if (i >= bytesRead) { - break; - } - switch (token[0]) { - case '(': - if (!keyStack[stackDepth].key) { - goto error; - } - keyStack[stackDepth].item = malloc(sizeof(*keyStack[stackDepth].item)); - keyStack[stackDepth].item->type = NI_HASH; - HashTableInit(&keyStack[stackDepth].item->hash, 8, _itemDeinit); - if (parent) { - HashTableInsert(parent, keyStack[stackDepth].key, keyStack[stackDepth].item); - } else { - struct NoIntroCategory* category = HashTableLookup(&db->categories, keyStack[stackDepth].key); - if (!category) { - category = malloc(sizeof(*category)); - NoIntroCategoryInit(category, 0); - HashTableInsert(&db->categories, keyStack[stackDepth].key, category); - } - *NoIntroCategoryAppend(category) = keyStack[stackDepth].item; - } - parent = &keyStack[stackDepth].item->hash; - ++stackDepth; - if (stackDepth >= KEY_STACK_SIZE) { - goto error; - } - keyStack[stackDepth].key = 0; - break; - case ')': - if (keyStack[stackDepth].key || !stackDepth) { - goto error; - } - --stackDepth; - if (stackDepth) { - parent = &keyStack[stackDepth - 1].item->hash; - } else { - parent = 0; - } - free(keyStack[stackDepth].key); - keyStack[stackDepth].key = 0; - break; - case '"': - ++token; - for (; line[i] != '"' && i < bytesRead; ++i); - // Fall through - default: - line[i] = '\0'; - if (!keyStack[stackDepth].key) { - keyStack[stackDepth].key = strdup(token); - } else { - struct NoIntroItem* item = malloc(sizeof(*keyStack[stackDepth].item)); - item->type = NI_STRING; - item->string = strdup(token); - if (parent) { - HashTableInsert(parent, keyStack[stackDepth].key, item); - } else { - struct NoIntroCategory* category = HashTableLookup(&db->categories, keyStack[stackDepth].key); - if (!category) { - category = malloc(sizeof(*category)); - NoIntroCategoryInit(category, 0); - HashTableInsert(&db->categories, keyStack[stackDepth].key, category); - } - *NoIntroCategoryAppend(category) = item; - } - free(keyStack[stackDepth].key); - keyStack[stackDepth].key = 0; - } - break; - } - } - } - - _indexU32x(db, &db->gameCrc, "game", "rom.crc"); - - return db; - -error: - HashTableDeinit(&db->categories); - free(db); - return 0; -} - -void NoIntroDBDestroy(struct NoIntroDB* db) { - HashTableDeinit(&db->categories); -} - -bool NoIntroDBLookupGameByCRC(const struct NoIntroDB* db, uint32_t crc32, struct NoIntroGame* game) { - if (!db) { - return false; - } - struct NoIntroItem* item = TableLookup(&db->gameCrc, crc32); - if (item) { - return _itemToGame(item, game); - } - return false; -}