mirror of https://github.com/mgba-emu/mgba.git
Feature: Move game database from flatfile to SQLite3
This commit is contained in:
parent
246142fd55
commit
d6e5283b9e
1
CHANGES
1
CHANGES
|
@ -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:
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -87,6 +87,10 @@
|
|||
#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;
|
||||
}
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)"));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue