Feature: Move game database from flatfile to SQLite3

This commit is contained in:
Jeffrey Pfau 2017-01-10 00:30:36 -08:00
parent 246142fd55
commit d6e5283b9e
10 changed files with 335 additions and 302 deletions

View File

@ -41,6 +41,7 @@ Misc:
- 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:

View File

@ -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 @@ if(USE_EPOXY)
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 @@ if(NOT QUIET)
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}")

View File

@ -125,6 +125,7 @@ mGBA has no hard dependencies, however, the following optional dependencies are
- 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.

View File

@ -87,6 +87,10 @@
#cmakedefine USE_PTHREADS
#endif
#ifndef USE_SQLITE3
#cmakedefine USE_SQLITE3
#endif
#ifndef USE_ZLIB
#cmakedefine USE_ZLIB
#endif

View File

@ -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;
}

View File

@ -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 @@ CXX_GUARD_START
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 NoIntroGame {
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);

View File

@ -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 @@ QString GBAApp::dataDir() {
}
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;
}

View File

@ -15,7 +15,9 @@
#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 @@ ROMInfo::ROMInfo(GameController* controller, QWidget* parent)
return;
}
#ifdef USE_SQLITE3
const NoIntroDB* db = GBAApp::app()->gameDB();
#endif
uint32_t crc32 = 0;
GameController::Interrupter interrupter(controller);
@ -67,6 +71,7 @@ ROMInfo::ROMInfo(GameController* controller, QWidget* parent)
}
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 @@ ROMInfo::ROMInfo(GameController* controller, QWidget* parent)
} 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)"));

View File

@ -48,7 +48,7 @@
#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 @@ void Window::updateTitle(float fps) {
break;
}
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;
#ifdef USE_SQLITE3
if (db && crc32 && NoIntroDBLookupGameByCRC(db, crc32, &game)) {
title = QLatin1String(game.name);
}
#endif
}
MultiplayerController* multiplayer = m_controller->multiplayerController();
if (multiplayer && multiplayer->attached() > 1) {

View File

@ -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;
}