mirror of
https://github.com/OpenFusionProject/OpenFusion.git
synced 2025-07-13 04:50:10 +00:00

This fixes a UX issue, where if you accidentally capitalized a letter in the username when logging in, it would instead create a new account. The behavior was confusing, since to the user it looks as if their characters were deleted or progress was not saved. In order for this to work, duplicate accounts (e.g. username and USERNAME) need to be deleted/renamed. The server will *detect* if any duplicates exist. If any are found, it will direct the server operator to a pruning script, or they can choose to resolve the duplicates manually.
276 lines
8.9 KiB
C++
276 lines
8.9 KiB
C++
#include "db/internal.hpp"
|
|
#include "settings.hpp"
|
|
|
|
#include <string>
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
std::mutex dbCrit;
|
|
sqlite3 *db;
|
|
|
|
/*
|
|
* When migrating from DB version 3 to 4, we change the username column
|
|
* to be case-insensitive. This function ensures there aren't any
|
|
* duplicates, e.g. username and USERNAME, before doing the migration.
|
|
* I handled this in the code itself rather than the migration file just so
|
|
* we can have a more detailed error message than what SQLite provides.
|
|
*/
|
|
static void checkCaseSensitiveDupes() {
|
|
const char* sql = "SELECT Login, COUNT(*) FROM Accounts GROUP BY LOWER(Login) HAVING COUNT(*) > 1;";
|
|
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
int stat = sqlite3_step(stmt);
|
|
|
|
if (stat == SQLITE_DONE) {
|
|
// no rows returned, so we're good
|
|
sqlite3_finalize(stmt);
|
|
return;
|
|
} else if (stat != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check for duplicate accounts: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
std::cout << "[FATAL] Case-sensitive duplicates detected in the Login column." << std::endl;
|
|
std::cout << "Either manually delete/rename the offending accounts, or run the pruning script:" << std::endl;
|
|
std::cout << "https://github.com/OpenFusionProject/scripts/tree/main/db_migration/caseinsens.py" << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
static void createMetaTable() {
|
|
std::lock_guard<std::mutex> lock(dbCrit); // XXX
|
|
|
|
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
|
|
|
|
const char* sql = R"(
|
|
CREATE TABLE Meta(
|
|
Key TEXT NOT NULL UNIQUE,
|
|
Value INTEGER NOT NULL
|
|
);
|
|
)";
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
|
exit(1);
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
sql = R"(
|
|
INSERT INTO Meta (Key, Value)
|
|
VALUES (?, ?);
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
sqlite3_bind_text(stmt, 1, "ProtocolVersion", -1, NULL);
|
|
sqlite3_bind_int(stmt, 2, PROTOCOL_VERSION);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE) {
|
|
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
|
exit(1);
|
|
}
|
|
|
|
sqlite3_reset(stmt);
|
|
sqlite3_bind_text(stmt, 1, "DatabaseVersion", -1, NULL);
|
|
sqlite3_bind_int(stmt, 2, DATABASE_VERSION);
|
|
int rc = sqlite3_step(stmt);
|
|
sqlite3_finalize(stmt);
|
|
if (rc != SQLITE_DONE) {
|
|
std::cout << "[FATAL] Failed to create meta table: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_exec(db, "ROLLBACK TRANSACTION;", NULL, NULL, NULL);
|
|
exit(1);
|
|
}
|
|
|
|
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);
|
|
std::cout << "[INFO] Created new meta table" << std::endl;
|
|
}
|
|
|
|
static void checkMetaTable() {
|
|
// first check if meta table exists
|
|
const char* sql = R"(
|
|
SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='Meta';
|
|
)";
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check meta table" << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
int count = sqlite3_column_int(stmt, 0);
|
|
if (count == 0) {
|
|
sqlite3_finalize(stmt);
|
|
// check if there's other non-internal tables first
|
|
sql = R"(
|
|
SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%';
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (sqlite3_step(stmt) != SQLITE_ROW || sqlite3_column_int(stmt, 0) != 0) {
|
|
sqlite3_finalize(stmt);
|
|
std::cout << "[FATAL] Existing DB is outdated" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
// create meta table
|
|
sqlite3_finalize(stmt);
|
|
return createMetaTable();
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
// check protocol version
|
|
sql = R"(
|
|
SELECT Value FROM Meta WHERE Key = 'ProtocolVersion';
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check DB Protocol Version: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
if (sqlite3_column_int(stmt, 0) != PROTOCOL_VERSION) {
|
|
sqlite3_finalize(stmt);
|
|
std::cout << "[FATAL] DB Protocol Version doesn't match Server Build" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
|
|
sql = R"(
|
|
SELECT Value FROM Meta WHERE Key = 'DatabaseVersion';
|
|
)";
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_ROW) {
|
|
std::cout << "[FATAL] Failed to check DB Version: " << sqlite3_errmsg(db) << std::endl;
|
|
sqlite3_finalize(stmt);
|
|
exit(1);
|
|
}
|
|
|
|
int dbVersion = sqlite3_column_int(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
|
|
if (dbVersion > DATABASE_VERSION) {
|
|
std::cout << "[FATAL] Server Build is incompatible with DB Version" << std::endl;
|
|
exit(1);
|
|
} else if (dbVersion < DATABASE_VERSION) {
|
|
// we're gonna migrate; back up the DB
|
|
std::cout << "[INFO] Backing up database" << std::endl;
|
|
// copy db file over using binary streams
|
|
std::ifstream src(settings::DBPATH, std::ios::binary);
|
|
std::ofstream dst(settings::DBPATH + ".old." + std::to_string(dbVersion), std::ios::binary);
|
|
dst << src.rdbuf();
|
|
src.close();
|
|
dst.close();
|
|
}
|
|
|
|
while (dbVersion != DATABASE_VERSION) {
|
|
// need to run this before we do any migration logic
|
|
if (dbVersion == 3)
|
|
checkCaseSensitiveDupes();
|
|
|
|
// db migrations
|
|
std::cout << "[INFO] Migrating Database to Version " << dbVersion + 1 << std::endl;
|
|
|
|
std::string path = "sql/migration" + std::to_string(dbVersion) + ".sql";
|
|
std::ifstream file(path);
|
|
if (!file.is_open()) {
|
|
std::cout << "[FATAL] Failed to migrate database: Couldn't open migration file" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
std::ostringstream stream;
|
|
stream << file.rdbuf();
|
|
std::string sql = stream.str();
|
|
int rc = sqlite3_exec(db, sql.c_str(), NULL, NULL, NULL);
|
|
|
|
if (rc != SQLITE_OK) {
|
|
std::cout << "[FATAL] Failed to migrate database: " << sqlite3_errmsg(db) << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
dbVersion++;
|
|
std::cout << "[INFO] Successful Database Migration to Version " << dbVersion << std::endl;
|
|
}
|
|
}
|
|
|
|
static void createTables() {
|
|
std::ifstream file("sql/tables.sql");
|
|
if (!file.is_open()) {
|
|
std::cout << "[FATAL] Failed to open database scheme" << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
std::ostringstream stream;
|
|
stream << file.rdbuf();
|
|
std::string read = stream.str();
|
|
const char* sql = read.c_str();
|
|
|
|
char* errMsg = 0;
|
|
int rc = sqlite3_exec(db, sql, NULL, NULL, &errMsg);
|
|
if (rc != SQLITE_OK) {
|
|
std::cout << "[FATAL] Database failed to create tables: " << errMsg << std::endl;
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static int getTableSize(std::string tableName) {
|
|
std::lock_guard<std::mutex> lock(dbCrit); // XXX
|
|
|
|
const char* sql = "SELECT COUNT(*) FROM ?";
|
|
sqlite3_stmt* stmt;
|
|
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
sqlite3_bind_text(stmt, 1, tableName.c_str(), -1, NULL);
|
|
sqlite3_step(stmt);
|
|
int result = sqlite3_column_int(stmt, 0);
|
|
sqlite3_finalize(stmt);
|
|
return result;
|
|
}
|
|
|
|
void Database::open() {
|
|
// XXX: move locks here
|
|
int rc = sqlite3_open(settings::DBPATH.c_str(), &db);
|
|
if (rc != SQLITE_OK) {
|
|
std::cout << "[FATAL] Cannot open database: " << sqlite3_errmsg(db) << std::endl;
|
|
exit(1);
|
|
}
|
|
|
|
// foreign keys in sqlite are off by default; enable them
|
|
sqlite3_exec(db, "PRAGMA foreign_keys=ON;", NULL, NULL, NULL);
|
|
|
|
// just in case a DB operation collides with an external manual modification
|
|
sqlite3_busy_timeout(db, 2000);
|
|
|
|
checkMetaTable();
|
|
createTables();
|
|
|
|
std::cout << "[INFO] Database in operation ";
|
|
int accounts = getTableSize("Accounts");
|
|
int players = getTableSize("Players");
|
|
std::string message = "";
|
|
if (accounts > 0) {
|
|
message += ": Found " + std::to_string(accounts) + " Account";
|
|
if (accounts > 1)
|
|
message += "s";
|
|
}
|
|
if (players > 0) {
|
|
message += " and " + std::to_string(players) + " Player Character";
|
|
if (players > 1)
|
|
message += "s";
|
|
}
|
|
std::cout << message << std::endl;
|
|
}
|
|
|
|
void Database::close() {
|
|
sqlite3_close(db);
|
|
}
|