#if DATABASE_SQLITE #include "querymanager.hh" #include "sqlite3.h" #include #include // NOTE(fusion): SQLite's application id, used to identify an existing database. // It is currently being set to ASCII "TiDB" for "Tibia Database". #define SQLITE_APPLICATION_ID 0x54694442 // NOTE(fusion): SQLite's user version, used to track the current schema version. // It is hardcoded because schema changes will usually result in query changes. #define SQLITE_USER_VERSION 1 struct TCachedStatement{ sqlite3_stmt *Stmt; int LastUsed; uint32 Hash; }; struct TDatabase{ sqlite3 *Handle; int MaxCachedStatements; TCachedStatement *CachedStatements; }; // Statement Cache //============================================================================== // IMPORTANT(fusion): Prepared statements that are not reset after use may keep // transactions open in which case an older view to the database is held, making // changes from other processes, including the sqlite shell, not visible. I have // not found anything on the SQLite docs but here's a stack overflow question: // `https://stackoverflow.com/questions/43949228` struct AutoStmtReset{ private: sqlite3_stmt *m_Stmt; public: AutoStmtReset(sqlite3_stmt *Stmt){ m_Stmt = Stmt; } ~AutoStmtReset(void){ if(m_Stmt != NULL){ sqlite3_reset(m_Stmt); m_Stmt = NULL; } } }; static void EnsureStatementCache(TDatabase *Database){ ASSERT(Database != NULL); if(Database->CachedStatements == NULL){ ASSERT(g_Config.SQLite.MaxCachedStatements > 0); Database->MaxCachedStatements = g_Config.SQLite.MaxCachedStatements; Database->CachedStatements = (TCachedStatement*)calloc( Database->MaxCachedStatements, sizeof(TCachedStatement)); } } static void DeleteStatementCache(TDatabase *Database){ ASSERT(Database != NULL); if(Database->CachedStatements != NULL){ ASSERT(Database->MaxCachedStatements > 0); for(int i = 0; i < Database->MaxCachedStatements; i += 1){ TCachedStatement *Entry = &Database->CachedStatements[i]; if(Entry->Stmt != NULL){ sqlite3_finalize(Entry->Stmt); Entry->Stmt = NULL; } Entry->LastUsed = 0; Entry->Hash = 0; } free(Database->CachedStatements); Database->MaxCachedStatements = 0; Database->CachedStatements = NULL; } } static sqlite3_stmt *PrepareQuery(TDatabase *Database, const char *Text){ ASSERT(Database != NULL); EnsureStatementCache(Database); sqlite3_stmt *Stmt = NULL; int LeastRecentlyUsed = 0; int LeastRecentlyUsedTime = Database->CachedStatements[0].LastUsed; uint32 Hash = HashString(Text); for(int i = 0; i < Database->MaxCachedStatements; i += 1){ TCachedStatement *Entry = &Database->CachedStatements[i]; if(Entry->LastUsed < LeastRecentlyUsedTime){ LeastRecentlyUsed = i; LeastRecentlyUsedTime = Entry->LastUsed; } if(Entry->Stmt != NULL && Entry->Hash == Hash){ const char *EntryText = sqlite3_sql(Entry->Stmt); ASSERT(EntryText != NULL); if(StringEq(EntryText, Text)){ Stmt = Entry->Stmt; Entry->LastUsed = GetMonotonicUptime(); break; } } } if(Stmt == NULL){ if(sqlite3_prepare_v3(Database->Handle, Text, -1, SQLITE_PREPARE_PERSISTENT, &Stmt, NULL) != SQLITE_OK){ LOG_ERR("Failed to prepare query: %s", sqlite3_errmsg(Database->Handle)); return NULL; } TCachedStatement *Entry = &Database->CachedStatements[LeastRecentlyUsed]; if(Entry->Stmt != NULL){ sqlite3_finalize(Entry->Stmt); } Entry->Stmt = Stmt; Entry->LastUsed = GetMonotonicUptime(); Entry->Hash = Hash; #if DEBUG_STATEMENT_CACHE { char Preview[30]; StringBufCopyEllipsis(Preview, Text); LOG("New statement cached: \"%s\"", Preview); } #endif }else{ if(sqlite3_stmt_busy(Stmt) != 0){ char Preview[30]; StringBufCopyEllipsis(Preview, Text); LOG_WARN("Statement \"%s\" wasn't properly reset. Use the" " `AutoStmtReset` wrapper or manually reset it after usage" " to avoid it holding onto an older view of the database," " making changes from other processes not visible.", Preview); sqlite3_reset(Stmt); } sqlite3_clear_bindings(Stmt); } return Stmt; } // Database Management //============================================================================== // NOTE(fusion): From `https://www.sqlite.org/pragma.html`: // "Some pragmas take effect during the SQL compilation stage, not the execution // stage. This means if using the C-language sqlite3_prepare(), sqlite3_step(), // sqlite3_finalize() API (or similar in a wrapper interface), the pragma may run // during the sqlite3_prepare() call, not during the sqlite3_step() call as normal // SQL statements do. Or the pragma might run during sqlite3_step() just like normal // SQL statements. Whether or not the pragma runs during sqlite3_prepare() or // sqlite3_step() depends on the pragma and on the specific release of SQLite." // // Depending on the pragma, queries will fail in the sqlite3_prepare() stage when // using bound parameters. This means we need to assemble the entire query before // hand with snprintf or other similar formatting functions. In particular, this // rule apply for `application_id` and `user_version` which we modify. static bool ExecFile(TDatabase *Database, const char *FileName){ FILE *File = fopen(FileName, "rb"); if(File == NULL){ LOG_ERR("Failed to open file \"%s\"", FileName); return false; } fseek(File, 0, SEEK_END); usize FileSize = (usize)ftell(File); fseek(File, 0, SEEK_SET); bool Result = true; if(FileSize > 0){ char *Text = (char*)malloc(FileSize + 1); Text[FileSize] = 0; if(Result && fread(Text, 1, FileSize, File) != FileSize){ LOG_ERR("Failed to read \"%s\" (ferror: %d, feof: %d)", FileName, ferror(File), feof(File)); Result = false; } if(Result && sqlite3_exec(Database->Handle, Text, NULL, NULL, NULL) != SQLITE_OK){ LOG_ERR("Failed to execute \"%s\": %s", FileName, sqlite3_errmsg(Database->Handle)); Result = false; } free(Text); } fclose(File); return Result; } static bool ExecInternal(TDatabase *Database, const char *Format, ...) ATTR_PRINTF(2, 3); static bool ExecInternal(TDatabase *Database, const char *Format, ...){ va_list ap; va_start(ap, Format); char Text[1024]; int Written = vsnprintf(Text, sizeof(Text), Format, ap); va_end(ap); if(Written >= (int)sizeof(Text)){ LOG_ERR("Query is too long"); return false; } if(sqlite3_exec(Database->Handle, Text, NULL, NULL, NULL) != SQLITE_OK){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } static bool QueryInternal(TDatabase *Database, int *OutValue, const char *Format, ...) ATTR_PRINTF(3, 4); static bool QueryInternal(TDatabase *Database, int *OutValue, const char *Format, ...){ va_list ap; va_start(ap, Format); char Text[1024]; int Written = vsnprintf(Text, sizeof(Text), Format, ap); va_end(ap); if(Written >= (int)sizeof(Text)){ LOG_ERR("Query is too long"); return false; } sqlite3_stmt *Stmt; if(sqlite3_prepare_v2(Database->Handle, Text, -1, &Stmt, NULL) != SQLITE_OK){ LOG_ERR("Failed to prepare query \"%s\": %s", Text, sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query \"%s\": %s", Text, sqlite3_errmsg(Database->Handle)); sqlite3_finalize(Stmt); return false; } if(OutValue != NULL){ if(ErrorCode == SQLITE_DONE || sqlite3_data_count(Stmt) == 0){ LOG_ERR("Query \"%s\" returned no data", Text); sqlite3_finalize(Stmt); return false; } *OutValue = sqlite3_column_int(Stmt, 0); } sqlite3_finalize(Stmt); return true; } static bool InitDatabaseSchema(TDatabase *Database){ TransactionScope Tx("SchemaInit"); if(!Tx.Begin(Database)){ return false; } // IMPORTANT(fusion): The schema init script should set`application_id` and // `user_version` appropriately. if(!ExecFile(Database, "sqlite/schema.sql")){ LOG_ERR("Failed to execute \"sqlite/schema.sql\""); return false; } return Tx.Commit(); } static bool GetPatchTimestamp(TDatabase *Database, const char *FileName, int *Timestamp){ sqlite3_stmt *Stmt; const char *Text = "SELECT Timestamp FROM Patches WHERE FileName = ?1"; if(sqlite3_prepare_v2(Database->Handle, Text, -1, &Stmt, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 1, FileName, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to prepare query: %s", sqlite3_errmsg(Database->Handle)); sqlite3_finalize(Stmt); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); sqlite3_finalize(Stmt); return false; } *Timestamp = (ErrorCode == SQLITE_ROW ? sqlite3_column_int(Stmt, 0) : 0); sqlite3_finalize(Stmt); return true; } static bool InsertPatch(TDatabase *Database, const char *FileName){ sqlite3_stmt *Stmt = NULL; const char *Text = "INSERT INTO Patches (FileName, Timestamp) VALUES (?1, UNIXEPOCH())"; if(sqlite3_prepare_v2(Database->Handle, Text, -1, &Stmt, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 1, FileName, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to prepare query: %s", sqlite3_errmsg(Database->Handle)); sqlite3_finalize(Stmt); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); sqlite3_finalize(Stmt); return false; } sqlite3_finalize(Stmt); return true; } static bool ApplyDatabasePatches(TDatabase *Database, const char *DirName){ LOG("Looking for patches at \"%s\"...", DirName); TransactionScope Tx("SchemaPatches"); if(!Tx.Begin(Database)){ return false; } DIR *PatchDir = opendir(DirName); if(PatchDir == NULL && errno == ENOENT){ LOG("Directory \"%s\" not found, skipping...", DirName); return true; } if(PatchDir == NULL){ LOG_ERR("Failed to open directory \"%s\": (%d) %s", DirName, errno, strerrordesc_np(errno)); return false; } bool Abort = false; DynamicArray PatchList; while(struct dirent *DirEntry = readdir(PatchDir)){ if(DirEntry->d_type != DT_REG || !StringEndsWithCI(DirEntry->d_name, ".sql")){ continue; } int PatchTimestamp; if(!GetPatchTimestamp(Database, DirEntry->d_name, &PatchTimestamp)){ Abort = true; break; } if(PatchTimestamp > 0){ char DateString[256]; StringBufFormatTime(DateString, "%Y-%m-%d", PatchTimestamp); LOG("\"%s\": patch already applied on %s", DirEntry->d_name, DateString); }else{ PatchList.Push(*DirEntry); } } if(!Abort && !PatchList.Empty()){ std::sort(PatchList.begin(), PatchList.end(), [](const struct dirent &A, const struct dirent &B) -> bool { return strcmp(A.d_name, B.d_name) < 0; }); for(const struct dirent &DirEntry: PatchList){ LOG("\"%s\": applying patch...", DirEntry.d_name); char FilePath[4096]; StringBufFormat(FilePath, "%s/%s", DirName, DirEntry.d_name); if(!ExecFile(Database, FilePath) || !InsertPatch(Database, DirEntry.d_name)){ Abort = true; break; } } } closedir(PatchDir); return !Abort && Tx.Commit(); } static bool CheckDatabaseSchema(TDatabase *Database){ int ApplicationID, NumObjects; if(!QueryInternal(Database, &ApplicationID, "PRAGMA application_id") || !QueryInternal(Database, &NumObjects, "SELECT COUNT(*) FROM sqlite_master")){ return false; } // IMPORTANT(fusion): Only initialize the schema if the database has no // application id and has no objects defined (tables, indexes, views, or // triggers). if(ApplicationID == 0 && NumObjects == 0){ if(!InitDatabaseSchema(Database)){ LOG_ERR("Failed to initialize database schema"); return false; } // NOTE(fusion): Refresh database information after initalizing schema. if(!QueryInternal(Database, &ApplicationID, "PRAGMA application_id") || !QueryInternal(Database, &NumObjects, "SELECT COUNT(*) FROM sqlite_master")){ return false; } } if(ApplicationID != SQLITE_APPLICATION_ID){ LOG_ERR("Application ID mismatch (expected %08X, got %08X)", SQLITE_APPLICATION_ID, ApplicationID); return false; } // IMPORTANT(fusion): We want to apply patches before checking user version, // just in case some migration needs to take place. if(!ApplyDatabasePatches(Database, "sqlite/patches")){ LOG_ERR("Failed to apply database patches"); return false; } int UserVersion; if(!QueryInternal(Database, &UserVersion, "PRAGMA user_version")){ return false; } if(UserVersion != SQLITE_USER_VERSION){ LOG_ERR("User Version mismatch (expected %d, got %d)", SQLITE_USER_VERSION, UserVersion); return false; } return true; } void DatabaseClose(TDatabase *Database){ if(Database != NULL){ DeleteStatementCache(Database); // NOTE(fusion): `sqlite3_close` can only fail if there are associated // prepared statements, blob handles, or backup objects that have not // been finalized. It should NEVER happen unless there is a BUG. if(Database->Handle != NULL){ if(sqlite3_close(Database->Handle) != SQLITE_OK){ PANIC("Failed to close database: %s", sqlite3_errmsg(Database->Handle)); } Database->Handle = NULL; } free(Database); } } TDatabase *DatabaseOpen(void){ TDatabase *Database = (TDatabase*)calloc(1, sizeof(TDatabase)); int Flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX; if(sqlite3_open_v2(g_Config.SQLite.File, &Database->Handle, Flags, NULL) != SQLITE_OK){ LOG_ERR("Failed to open database at \"%s\": %s\n", g_Config.SQLite.File, sqlite3_errmsg(Database->Handle)); DatabaseClose(Database); return NULL; } if(sqlite3_db_readonly(Database->Handle, NULL)){ LOG_ERR("Failed to open database file \"%s\" with WRITE PERMISSIONS." " Make sure it has the appropriate permissions and is owned" " by the same user running the query manager.", g_Config.SQLite.File); DatabaseClose(Database); return NULL; } if(!CheckDatabaseSchema(Database)){ LOG_ERR("Failed to check database schema"); DatabaseClose(Database); return NULL; } return Database; } bool DatabaseCheckpoint(TDatabase *Database){ // IMPORTANT(fusion): Since SQLite is a local database, we don't need to check // whether the connection is still valid or needs reconnecting. ASSERT(Database != NULL); return true; } int DatabaseMaxConcurrency(void){ // IMPORTANT(fusion): Running queries from separate threads is possible with // different database handles, but there is an inherent limit because access // to the underlying database file must be synchronized by the operating system. // Also, there can only be one writer, which may cause spurious `SQLITE_BUSY` // errors to happen if the database wasn't available for reading/writing. return 1; } // TransactionScope //============================================================================== TransactionScope::TransactionScope(const char *Context){ m_Context = (Context != NULL ? Context : "NOCONTEXT"); m_Database = NULL; } TransactionScope::~TransactionScope(void){ if(m_Database != NULL){ if(!ExecInternal(m_Database, "ROLLBACK")){ LOG_ERR("Failed to rollback transaction (%s)", m_Context); } m_Database = NULL; } } bool TransactionScope::Begin(TDatabase *Database){ if(m_Database != NULL){ LOG_ERR("Transaction (%s) already running", m_Context); return false; } if(!ExecInternal(Database, "BEGIN")){ LOG_ERR("Failed to begin transaction (%s)", m_Context); return false; } m_Database = Database; return true; } bool TransactionScope::Commit(void){ if(m_Database == NULL){ LOG_ERR("Transaction (%s) not running", m_Context); return false; } if(!ExecInternal(m_Database, "COMMIT")){ LOG_ERR("Failed to commit transaction (%s)", m_Context); return false; } m_Database = NULL; return true; } // Primary Tables //============================================================================== bool GetWorldID(TDatabase *Database, const char *World, int *WorldID){ ASSERT(Database != NULL && World != NULL && WorldID != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT WorldID FROM Worlds WHERE Name = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_text(Stmt, 1, World, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind WorldName: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *WorldID = (ErrorCode == SQLITE_ROW ? sqlite3_column_int(Stmt, 0) : 0); return true; } bool GetWorlds(TDatabase *Database, DynamicArray *Worlds){ ASSERT(Database != NULL && Worlds != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "WITH N (WorldID, NumPlayers) AS (" "SELECT WorldID, COUNT(*) FROM OnlineCharacters GROUP BY WorldID" ")" " SELECT W.Name, W.Type, COALESCE(N.NumPlayers, 0), W.MaxPlayers," " W.OnlinePeak, W.OnlinePeakTimestamp, W.LastStartup, W.LastShutdown" " FROM Worlds AS W" " LEFT JOIN N ON W.WorldID = N.WorldID"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); while(sqlite3_step(Stmt) == SQLITE_ROW){ TWorld World = {}; StringBufCopy(World.Name, (const char*)sqlite3_column_text(Stmt, 0)); World.Type = sqlite3_column_int(Stmt, 1); World.NumPlayers = sqlite3_column_int(Stmt, 2); World.MaxPlayers = sqlite3_column_int(Stmt, 3); World.OnlinePeak = sqlite3_column_int(Stmt, 4); World.OnlinePeakTimestamp = sqlite3_column_int(Stmt, 5); World.LastStartup = sqlite3_column_int(Stmt, 6); World.LastShutdown = sqlite3_column_int(Stmt, 7); Worlds->Push(World); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetWorldConfig(TDatabase *Database, int WorldID, TWorldConfig *WorldConfig){ ASSERT(Database != NULL && WorldConfig != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT WorldID, Type, RebootTime, Host, Port, MaxPlayers," " PremiumPlayerBuffer, MaxNewbies, PremiumNewbieBuffer" " FROM Worlds WHERE WorldID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } memset(WorldConfig, 0, sizeof(TWorldConfig)); if(ErrorCode == SQLITE_ROW){ WorldConfig->WorldID = sqlite3_column_int(Stmt, 0); WorldConfig->Type = sqlite3_column_int(Stmt, 1); WorldConfig->RebootTime = sqlite3_column_int(Stmt, 2); StringBufCopy(WorldConfig->HostName, (const char*)sqlite3_column_text(Stmt, 3)); WorldConfig->Port = sqlite3_column_int(Stmt, 4); WorldConfig->MaxPlayers = sqlite3_column_int(Stmt, 5); WorldConfig->PremiumPlayerBuffer = sqlite3_column_int(Stmt, 6); WorldConfig->MaxNewbies = sqlite3_column_int(Stmt, 7); WorldConfig->PremiumNewbieBuffer = sqlite3_column_int(Stmt, 8); } return true; } bool AccountExists(TDatabase *Database, int AccountID, const char *Email, bool *Exists){ ASSERT(Database != NULL && Email != NULL && Exists != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM Accounts WHERE AccountID = ?1 OR Email = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK || sqlite3_bind_text(Stmt, 2, Email, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Exists = (ErrorCode == SQLITE_ROW); return true; } bool AccountNumberExists(TDatabase *Database, int AccountID, bool *Exists){ ASSERT(Database != NULL && Exists != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM Accounts WHERE AccountID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID)!= SQLITE_OK){ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Exists = (ErrorCode == SQLITE_ROW); return true; } bool AccountEmailExists(TDatabase *Database, const char *Email, bool *Exists){ ASSERT(Database != NULL && Email != NULL && Exists != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM Accounts WHERE Email = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_text(Stmt, 1, Email, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind Email: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Exists = (ErrorCode == SQLITE_ROW); return true; } bool CreateAccount(TDatabase *Database, int AccountID, const char *Email, const uint8 *Auth, int AuthSize){ ASSERT(Database != NULL && Email != NULL && Auth != NULL && AuthSize > 0); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO Accounts (AccountID, Email, Auth)" " VALUES (?1, ?2, ?3)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK || sqlite3_bind_text(Stmt, 2, Email, -1, NULL) != SQLITE_OK || sqlite3_bind_blob(Stmt, 3, Auth, AuthSize, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_DONE && ErrorCode != SQLITE_CONSTRAINT){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } // TODO(fusion): Maybe have a `ContraintError` output param? return (ErrorCode == SQLITE_DONE); } bool GetAccountData(TDatabase *Database, int AccountID, TAccount *Account){ ASSERT(Database != NULL && Account != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT AccountID, Email, Auth," " MAX(PremiumEnd - UNIXEPOCH(), 0)," " PendingPremiumDays, Deleted" " FROM Accounts WHERE AccountID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } memset(Account, 0, sizeof(TAccount)); if(ErrorCode == SQLITE_ROW){ Account->AccountID = sqlite3_column_int(Stmt, 0); StringBufCopy(Account->Email, (const char*)sqlite3_column_text(Stmt, 1)); if(sqlite3_column_bytes(Stmt, 2) == sizeof(Account->Auth)){ memcpy(Account->Auth, sqlite3_column_blob(Stmt, 2), sizeof(Account->Auth)); } Account->PremiumDays = RoundSecondsToDays(sqlite3_column_int(Stmt, 3)); Account->PendingPremiumDays = sqlite3_column_int(Stmt, 4); Account->Deleted = (sqlite3_column_int(Stmt, 5) != 0); } return true; } bool GetAccountOnlineCharacters(TDatabase *Database, int AccountID, int *OnlineCharacters){ ASSERT(Database != NULL && OnlineCharacters != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM Characters" " WHERE AccountID = ?1 AND IsOnline != 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_ROW){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *OnlineCharacters = sqlite3_column_int(Stmt, 0); return true; } bool IsCharacterOnline(TDatabase *Database, int CharacterID, bool *Online){ ASSERT(Database != NULL && Online != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT IsOnline FROM Characters WHERE CharacterID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind CharacterID: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Online = (ErrorCode == SQLITE_ROW && sqlite3_column_int(Stmt, 0) != 0); return true; } bool ActivatePendingPremiumDays(TDatabase *Database, int AccountID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Accounts" " SET PremiumEnd = MAX(PremiumEnd, UNIXEPOCH())" " + PendingPremiumDays * 86400," " PendingPremiumDays = 0" " WHERE AccountID = ?1 AND PendingPremiumDays > 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetCharacterEndpoints(TDatabase *Database, int AccountID, DynamicArray *Characters){ ASSERT(Database != NULL && Characters != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT C.Name, W.Name, W.Host, W.Port" " FROM Characters AS C" " INNER JOIN Worlds AS W ON W.WorldID = C.WorldID" " WHERE C.AccountID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ TCharacterEndpoint Character = {}; StringBufCopy(Character.Name, (const char*)sqlite3_column_text(Stmt, 0)); StringBufCopy(Character.WorldName, (const char*)sqlite3_column_text(Stmt, 1)); StringBufCopy(Character.WorldHost, (const char*)sqlite3_column_text(Stmt, 2)); Character.WorldPort = sqlite3_column_int(Stmt, 3); Characters->Push(Character); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetCharacterSummaries(TDatabase *Database, int AccountID, DynamicArray *Characters){ ASSERT(Database != NULL && Characters != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT C.Name, W.Name, C.Level, C.Profession, C.IsOnline, C.Deleted" " FROM Characters AS C" " LEFT JOIN Worlds AS W ON W.WorldID = C.WorldID" " WHERE C.AccountID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ TCharacterSummary Character = {}; StringBufCopy(Character.Name, (const char*)sqlite3_column_text(Stmt, 0)); StringBufCopy(Character.World, (const char*)sqlite3_column_text(Stmt, 1)); Character.Level = sqlite3_column_int(Stmt, 2); StringBufCopy(Character.Profession, (const char*)sqlite3_column_text(Stmt, 3)); Character.Online = (sqlite3_column_int(Stmt, 4) != 0); Character.Deleted = (sqlite3_column_int(Stmt, 5) != 0); Characters->Push(Character); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool CharacterNameExists(TDatabase *Database, const char *Name, bool *Exists){ ASSERT(Database != NULL && Exists != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM Characters WHERE Name = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_text(Stmt, 1, Name, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind Email: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Exists = (ErrorCode == SQLITE_ROW); return true; } bool CreateCharacter(TDatabase *Database, int WorldID, int AccountID, const char *Name, int Sex){ ASSERT(Database != NULL && Name != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO Characters (WorldID, AccountID, Name, Sex)" " VALUES (?1, ?2, ?3, ?4)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, AccountID) != SQLITE_OK || sqlite3_bind_text(Stmt, 3, Name, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, Sex) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_DONE && ErrorCode != SQLITE_CONSTRAINT){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } // TODO(fusion): Same as `CreateAccount`? return (ErrorCode == SQLITE_DONE); } bool GetCharacterID(TDatabase *Database, int WorldID, const char *CharacterName, int *CharacterID){ ASSERT(Database != NULL && CharacterName != NULL && CharacterID != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT CharacterID FROM Characters" " WHERE WorldID = ?1 AND Name = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_text(Stmt, 2, CharacterName, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *CharacterID = (ErrorCode == SQLITE_ROW ? sqlite3_column_int(Stmt, 0) : 0); return true; } bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){ ASSERT(Database != NULL && CharacterName != NULL && Character != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT WorldID, CharacterID, AccountID, Name, Sex, Deleted" " FROM Characters WHERE Name = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_text(Stmt, 1, CharacterName, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind CharacterName: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } memset(Character, 0, sizeof(TCharacterLoginData)); if(ErrorCode == SQLITE_ROW){ Character->WorldID = sqlite3_column_int(Stmt, 0); Character->CharacterID = sqlite3_column_int(Stmt, 1); Character->AccountID = sqlite3_column_int(Stmt, 2); StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 3)); Character->Sex = sqlite3_column_int(Stmt, 4); Character->Deleted = (sqlite3_column_int(Stmt, 5) != 0); } return true; } bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){ ASSERT(Database != NULL && CharacterName != NULL && Character != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT C.CharacterID, C.Name, W.Name, C.Sex, C.Level," " C.Profession, C.Residence, C.LastLoginTime, C.IsOnline," " C.Deleted, MAX(A.PremiumEnd - UNIXEPOCH(), 0)" " FROM Characters AS C" " LEFT JOIN Worlds AS W ON W.WorldID = C.WorldID" " LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID" " LEFT JOIN CharacterRights AS R" " ON R.CharacterID = C.CharacterID" " AND R.Name = 'NO_STATISTICS'" " WHERE C.Name = ?1 AND R.Name IS NULL"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_text(Stmt, 1, CharacterName, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind CharacterName: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } memset(Character, 0, sizeof(TCharacterProfile)); if(ErrorCode == SQLITE_ROW){ Character->CharacterID = sqlite3_column_int(Stmt, 0); StringBufCopy(Character->Name, (const char*)sqlite3_column_text(Stmt, 1)); StringBufCopy(Character->World, (const char*)sqlite3_column_text(Stmt, 2)); Character->Sex = sqlite3_column_int(Stmt, 3); Character->Level = sqlite3_column_int(Stmt, 4); StringBufCopy(Character->Profession, (const char*)sqlite3_column_text(Stmt, 5)); StringBufCopy(Character->Residence, (const char*)sqlite3_column_text(Stmt, 6)); Character->LastLogin = sqlite3_column_int(Stmt, 7); Character->Online = (sqlite3_column_int(Stmt, 8) != 0); Character->Deleted = (sqlite3_column_int(Stmt, 9) != 0); Character->PremiumDays = RoundSecondsToDays(sqlite3_column_int(Stmt, 10)); } return true; } bool GetCharacterRight(TDatabase *Database, int CharacterID, const char *Right, bool *HasRight){ ASSERT(Database != NULL && Right != NULL && HasRight != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM CharacterRights" " WHERE CharacterID = ?1 AND Name = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK || sqlite3_bind_text(Stmt, 2, Right, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *HasRight = (ErrorCode == SQLITE_ROW); return true; } bool GetCharacterRights(TDatabase *Database, int CharacterID, DynamicArray *Rights){ ASSERT(Database != NULL && Rights != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT Name FROM CharacterRights WHERE CharacterID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind CharacterID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ TCharacterRight Right = {}; StringBufCopy(Right.Name, (const char*)sqlite3_column_text(Stmt, 0)); Rights->Push(Right); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, bool *GuildLeader){ ASSERT(Database != NULL && GuildLeader != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM Guilds" " WHERE WorldID = ?1 AND LeaderID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *GuildLeader = (ErrorCode == SQLITE_ROW); return true; } bool IncrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){ ASSERT(Database != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Characters SET IsOnline = IsOnline + 1" " WHERE WorldID = ?1 AND CharacterID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool DecrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){ ASSERT(Database != NULL); // NOTE(fusion): A character is uniquely identified by its id. The world id // check is purely to avoid a world from modifying a character from another // world. sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Characters SET IsOnline = IsOnline - 1" " WHERE WorldID = ?1 AND CharacterID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool ClearIsOnline(TDatabase *Database, int WorldID, int *NumAffectedCharacters){ ASSERT(Database != NULL && NumAffectedCharacters != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Characters SET IsOnline = 0" " WHERE WorldID = ?1 AND IsOnline != 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *NumAffectedCharacters = sqlite3_changes(Database->Handle); return true; } bool LogoutCharacter(TDatabase *Database, int WorldID, int CharacterID, int Level, const char *Profession, const char *Residence, int LastLoginTime, int TutorActivities){ ASSERT(Database != NULL && Profession != NULL && Residence != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Characters" " SET Level = ?3," " Profession = ?4," " Residence = ?5," " LastLoginTime = ?6," " TutorActivities = ?7," " IsOnline = IsOnline - 1" " WHERE WorldID = ?1 AND CharacterID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Level) != SQLITE_OK || sqlite3_bind_text(Stmt, 4, Profession, -1, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Residence, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 6, LastLoginTime) != SQLITE_OK || sqlite3_bind_int(Stmt, 7, TutorActivities) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetCharacterIndexEntries(TDatabase *Database, int WorldID, int MinimumCharacterID, int MaxEntries, int *NumEntries, TCharacterIndexEntry *Entries){ ASSERT(Database != NULL && MaxEntries > 0 && NumEntries != NULL && Entries != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT CharacterID, Name FROM Characters" " WHERE WorldID = ?1 AND CharacterID >= ?2" " ORDER BY CharacterID ASC LIMIT ?3"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, MinimumCharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, MaxEntries) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } // NOTE(fusion): We shouldn't get more than `MaxEntries` rows but it's // always better to be safe. int EntryIndex = 0; while(sqlite3_step(Stmt) == SQLITE_ROW && EntryIndex < MaxEntries){ Entries[EntryIndex].CharacterID = sqlite3_column_int(Stmt, 0); StringBufCopy(Entries[EntryIndex].Name, (const char*)sqlite3_column_text(Stmt, 1)); EntryIndex += 1; } int ErrorCode = sqlite3_errcode(Database->Handle); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *NumEntries = EntryIndex; return true; } bool InsertCharacterDeath(TDatabase *Database, int WorldID, int CharacterID, int Level, int OffenderID, const char *Remark, bool Unjustified, int Timestamp){ ASSERT(Database != NULL && Remark != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO CharacterDeaths (CharacterID, Level," " OffenderID, Remark, Unjustified, Timestamp)" " SELECT ?2, ?3, ?4, ?5, ?6, ?7 FROM Characters" " WHERE WorldID = ?1 AND CharacterID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Level) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, OffenderID) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Remark, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 6, (Unjustified ? 1 : 0)) != SQLITE_OK || sqlite3_bind_int(Stmt, 7, Timestamp) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool InsertBuddy(TDatabase *Database, int WorldID, int AccountID, int BuddyID){ ASSERT(Database != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. // NOTE(fusion): Use the `IGNORE` conflict resolution to make duplicate row // errors appear as successful insertions. sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT OR IGNORE INTO Buddies (WorldID, AccountID, BuddyID)" " SELECT ?1, ?2, ?3 FROM Characters" " WHERE WorldID = ?1 AND CharacterID = ?3"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, AccountID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, BuddyID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool DeleteBuddy(TDatabase *Database, int WorldID, int AccountID, int BuddyID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "DELETE FROM Buddies" " WHERE WorldID = ?1 AND AccountID = ?2 AND BuddyID = ?3"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, AccountID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, BuddyID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } // NOTE(fusion): Always return true here even if there were no deleted rows // to make them appear as successful deletions. return true; } bool GetBuddies(TDatabase *Database, int WorldID, int AccountID, DynamicArray *Buddies){ ASSERT(Database != NULL && Buddies != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT B.BuddyID, C.Name" " FROM Buddies AS B" " INNER JOIN Characters AS C" " ON C.WorldID = B.WorldID AND C.CharacterID = B.BuddyID" " WHERE B.WorldID = ?1 AND B.AccountID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, AccountID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ TAccountBuddy Buddy = {}; Buddy.CharacterID = sqlite3_column_int(Stmt, 0); StringBufCopy(Buddy.Name, (const char*)sqlite3_column_text(Stmt, 1)); Buddies->Push(Buddy); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetWorldInvitation(TDatabase *Database, int WorldID, int CharacterID, bool *Invited){ ASSERT(Database != NULL && Invited != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM WorldInvitations" " WHERE WorldID = ?1 AND CharacterID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Invited = (ErrorCode == SQLITE_ROW); return true; } bool InsertLoginAttempt(TDatabase *Database, int AccountID, int IPAddress, bool Failed){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO LoginAttempts (AccountID, IPAddress, Timestamp, Failed)" " VALUES (?1, ?2, UNIXEPOCH(), ?3)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, IPAddress) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, (Failed ? 1 : 0)) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *FailedAttempts){ ASSERT(Database != NULL && FailedAttempts != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM LoginAttempts" " WHERE AccountID = ?1" " AND (UNIXEPOCH() - Timestamp) <= ?2" " AND Failed != 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, TimeWindow) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_ROW){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *FailedAttempts = sqlite3_column_int(Stmt, 0); return true; } bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *FailedAttempts){ ASSERT(Database != NULL && FailedAttempts != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM LoginAttempts" " WHERE IPAddress = ?1" " AND (UNIXEPOCH() - Timestamp) <= ?2" " AND Failed != 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, IPAddress) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, TimeWindow) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_ROW){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *FailedAttempts = sqlite3_column_int(Stmt, 0); return true; } // Guild Tables //============================================================================== bool GetCharacterGuildData(TDatabase *Database, int CharacterID, TCharacterGuildData *GuildData){ ASSERT(Database != NULL && GuildData != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT G.GuildID, R.Rank, G.Name, R.Name, M.Title" " FROM Characters AS C" " LEFT JOIN GuildMembers AS M ON M.CharacterID = C.CharacterID" " LEFT JOIN Guilds AS G ON G.GuildID = M.GuildID" " LEFT JOIN GuildRanks AS R" " ON R.GuildID = M.GuildID AND R.Rank = M.Rank" " WHERE C.CharacterID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind CharacterID: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } memset(GuildData, 0, sizeof(TCharacterGuildData)); if(ErrorCode == SQLITE_ROW){ GuildData->GuildID = sqlite3_column_int(Stmt, 0); GuildData->Rank = sqlite3_column_int(Stmt, 1); StringBufCopy(GuildData->GuildName, (const char*)sqlite3_column_text(Stmt, 2)); StringBufCopy(GuildData->RankName, (const char*)sqlite3_column_text(Stmt, 3)); StringBufCopy(GuildData->Title, (const char*)sqlite3_column_text(Stmt, 4)); } return true; } // House Tables //============================================================================== bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray *Auctions){ ASSERT(Database != NULL && Auctions != NULL); // TODO(fusion): If the application crashes while processing finished auctions, // non processed auctions will be lost but with no other side-effects. It could // be an inconvenience but it's not a big problem. sqlite3_stmt *Stmt = PrepareQuery(Database, "DELETE FROM HouseAuctions" " WHERE WorldID = ?1" " AND FinishTime IS NOT NULL" " AND FinishTime <= UNIXEPOCH()" " RETURNING HouseID, BidderID, BidAmount, FinishTime," " (SELECT Name FROM Characters WHERE CharacterID = BidderID)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ THouseAuction Auction = {}; Auction.HouseID = sqlite3_column_int(Stmt, 0); Auction.BidderID = sqlite3_column_int(Stmt, 1); Auction.BidAmount = sqlite3_column_int(Stmt, 2); Auction.FinishTime = sqlite3_column_int(Stmt, 3); StringBufCopy(Auction.BidderName, (const char*)sqlite3_column_text(Stmt, 4)); Auctions->Push(Auction); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray *Transfers){ ASSERT(Database != NULL && Transfers != NULL); // TODO(fusion): Same as `FinishHouseAuctions` but with house transfers. sqlite3_stmt *Stmt = PrepareQuery(Database, "DELETE FROM HouseTransfers" " WHERE WorldID = ?1" " RETURNING HouseID, NewOwnerID, Price," " (SELECT Name FROM Characters WHERE CharacterID = NewOwnerID)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ THouseTransfer Transfer = {}; Transfer.HouseID = sqlite3_column_int(Stmt, 0); Transfer.NewOwnerID = sqlite3_column_int(Stmt, 1); Transfer.Price = sqlite3_column_int(Stmt, 2); StringBufCopy(Transfer.NewOwnerName, (const char*)sqlite3_column_text(Stmt, 3)); Transfers->Push(Transfer); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetFreeAccountEvictions(TDatabase *Database, int WorldID, DynamicArray *Evictions){ ASSERT(Database != NULL && Evictions != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT O.HouseID, O.OwnerID" " FROM HouseOwners AS O" " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID" " LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID" " WHERE O.WorldID = ?1" " AND (A.PremiumEnd IS NULL OR A.PremiumEnd < UNIXEPOCH())"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ THouseEviction Eviction = {}; Eviction.HouseID = sqlite3_column_int(Stmt, 0); Eviction.OwnerID = sqlite3_column_int(Stmt, 1); Evictions->Push(Eviction); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetDeletedCharacterEvictions(TDatabase *Database, int WorldID, DynamicArray *Evictions){ ASSERT(Database != NULL && Evictions != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT O.HouseID, O.OwnerID" " FROM HouseOwners AS O" " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID" " WHERE O.WorldID = ?1" " AND (C.CharacterID IS NULL OR C.Deleted != 0)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ THouseEviction Eviction = {}; Eviction.HouseID = sqlite3_column_int(Stmt, 0); Eviction.OwnerID = sqlite3_column_int(Stmt, 1); Evictions->Push(Eviction); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool InsertHouseOwner(TDatabase *Database, int WorldID, int HouseID, int OwnerID, int PaidUntil){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO HouseOwners (WorldID, HouseID, OwnerID, PaidUntil)" " VALUES (?1, ?2, ?3, ?4)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, HouseID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, OwnerID) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, PaidUntil) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool UpdateHouseOwner(TDatabase *Database, int WorldID, int HouseID, int OwnerID, int PaidUntil){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE HouseOwners" " SET OwnerID = ?3, PaidUntil = ?4" " WHERE WorldID = ?1 AND HouseID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, HouseID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, OwnerID) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, PaidUntil) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool DeleteHouseOwner(TDatabase *Database, int WorldID, int HouseID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "DELETE FROM HouseOwners" " WHERE WorldID = ?1 AND HouseID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, HouseID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetHouseOwners(TDatabase *Database, int WorldID, DynamicArray *Owners){ ASSERT(Database != NULL && Owners != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT O.HouseID, O.OwnerID, C.Name, O.PaidUntil" " FROM HouseOwners AS O" " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID" " WHERE O.WorldID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ THouseOwner Owner = {}; Owner.HouseID = sqlite3_column_int(Stmt, 0); Owner.OwnerID = sqlite3_column_int(Stmt, 1); StringBufCopy(Owner.OwnerName, (const char*)sqlite3_column_text(Stmt, 2)); Owner.PaidUntil = sqlite3_column_int(Stmt, 3); Owners->Push(Owner); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool GetHouseAuctions(TDatabase *Database, int WorldID, DynamicArray *Auctions){ ASSERT(Database != NULL && Auctions != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT HouseID FROM HouseAuctions WHERE WorldID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ Auctions->Push(sqlite3_column_int(Stmt, 0)); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool StartHouseAuction(TDatabase *Database, int WorldID, int HouseID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO HouseAuctions (WorldID, HouseID) VALUES (?1, ?2)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, HouseID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool DeleteHouses(TDatabase *Database, int WorldID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "DELETE FROM Houses WHERE WorldID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool InsertHouses(TDatabase *Database, int WorldID, int NumHouses, THouse *Houses){ ASSERT(Database != NULL && NumHouses > 0 && Houses != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO Houses (WorldID, HouseID, Name, Rent, Description," " Size, PositionX, PositionY, PositionZ, Town, GuildHouse)" " VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } for(int i = 0; i < NumHouses; i += 1){ if(sqlite3_bind_int(Stmt, 2, Houses[i].HouseID) != SQLITE_OK || sqlite3_bind_text(Stmt, 3, Houses[i].Name, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, Houses[i].Rent) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Houses[i].Description, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 6, Houses[i].Size) != SQLITE_OK || sqlite3_bind_int(Stmt, 7, Houses[i].PositionX) != SQLITE_OK || sqlite3_bind_int(Stmt, 8, Houses[i].PositionY) != SQLITE_OK || sqlite3_bind_int(Stmt, 9, Houses[i].PositionZ) != SQLITE_OK || sqlite3_bind_text(Stmt, 10, Houses[i].Town, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 11, Houses[i].GuildHouse ? 1: 0) != SQLITE_OK){ LOG_ERR("Failed to bind parameters for house %d: %s", Houses[i].HouseID, sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to insert house %d: %s", Houses[i].HouseID, sqlite3_errmsg(Database->Handle)); return false; } sqlite3_reset(Stmt); } return true; } bool ExcludeFromAuctions(TDatabase *Database, int WorldID, int CharacterID, int Duration, int BanishmentID){ ASSERT(Database != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO HouseAuctionExclusions (CharacterID, Issued, Until, BanishmentID)" " SELECT ?2, UNIXEPOCH(), (UNIXEPOCH() + ?3), ?4" " FROM Characters" " WHERE WorldID = ?1 AND CharacterID = ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Duration) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, BanishmentID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } // Banishment Tables //============================================================================== bool IsCharacterNamelocked(TDatabase *Database, int CharacterID, bool *Namelocked){ ASSERT(Database != NULL && Namelocked != NULL); TNamelockStatus Status; if(!GetNamelockStatus(Database, CharacterID, &Status)){ return false; } *Namelocked = Status.Namelocked && !Status.Approved; return true; } bool GetNamelockStatus(TDatabase *Database, int CharacterID, TNamelockStatus *Status){ ASSERT(Database != NULL && Status != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT Approved FROM Namelocks WHERE CharacterID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } memset(Status, 0, sizeof(TNamelockStatus)); Status->Namelocked = (ErrorCode == SQLITE_ROW); if(Status->Namelocked){ Status->Approved = (sqlite3_column_int(Stmt, 0) != 0); } return true; } bool InsertNamelock(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO Namelocks (CharacterID, IPAddress, GamemasterID, Reason, Comment)" " VALUES (?1, ?2, ?3, ?4, ?5)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, IPAddress) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, GamemasterID) != SQLITE_OK || sqlite3_bind_text(Stmt, 4, Reason, -1, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Comment, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool IsAccountBanished(TDatabase *Database, int AccountID, bool *Banished){ ASSERT(Database != NULL && Banished != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM Banishments" " WHERE AccountID = ?1" " AND (Until = Issued OR Until > UNIXEPOCH())"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Banished = (ErrorCode == SQLITE_ROW); return true; } bool GetBanishmentStatus(TDatabase *Database, int CharacterID, TBanishmentStatus *Status){ ASSERT(Database != NULL && Status != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT B.FinalWarning, (B.Until = B.Issued OR B.Until > UNIXEPOCH())" " FROM Banishments AS B" " LEFT JOIN Characters AS C ON C.AccountID = B.AccountID" " WHERE C.CharacterID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } memset(Status, 0, sizeof(TBanishmentStatus)); while(sqlite3_step(Stmt) == SQLITE_ROW){ Status->TimesBanished += 1; if(sqlite3_column_int(Stmt, 0) != 0){ Status->FinalWarning = true; } if(sqlite3_column_int(Stmt, 1) != 0){ Status->Banished = true; } } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool InsertBanishment(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment, bool FinalWarning, int Duration, int *BanishmentID){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL && BanishmentID != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO Banishments (AccountID, IPAddress, GamemasterID," " Reason, Comment, FinalWarning, Issued, Until)" " SELECT AccountID, ?2, ?3, ?4, ?5, ?6, UNIXEPOCH(), UNIXEPOCH() + ?7" " FROM Characters WHERE CharacterID = ?1" " RETURNING BanishmentID"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, IPAddress) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, GamemasterID) != SQLITE_OK || sqlite3_bind_text(Stmt, 4, Reason, -1, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Comment, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 6, FinalWarning) != SQLITE_OK || sqlite3_bind_int(Stmt, 7, Duration) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_ROW){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *BanishmentID = sqlite3_column_int(Stmt, 0); return true; } bool GetNotationCount(TDatabase *Database, int CharacterID, int *Notations){ ASSERT(Database != NULL && Notations != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM Notations WHERE CharacterID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_ROW){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Notations = sqlite3_column_int(Stmt, 0); return true; } bool InsertNotation(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO Notations (CharacterID, IPAddress," " GamemasterID, Reason, Comment)" " VALUES (?1, ?2, ?3, ?4, ?5)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, IPAddress) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, GamemasterID) != SQLITE_OK || sqlite3_bind_text(Stmt, 4, Reason, -1, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Comment, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool IsIPBanished(TDatabase *Database, int IPAddress, bool *Banished){ ASSERT(Database != NULL && Banished != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM IPBanishments" " WHERE IPAddress = ?1" " AND (Until = Issued OR Until > UNIXEPOCH())"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, IPAddress) != SQLITE_OK){ LOG_ERR("Failed to bind IPAddress: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Banished = (ErrorCode == SQLITE_ROW); return true; } bool InsertIPBanishment(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment, int Duration){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO IPBanishments (CharacterID, IPAddress," " GamemasterID, Reason, Comment, Issued, Until)" " VALUES (?1, ?2, ?3, ?4, ?5, UNIXEPOCH(), UNIXEPOCH() + ?6)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, IPAddress) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, GamemasterID) != SQLITE_OK || sqlite3_bind_text(Stmt, 4, Reason, -1, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Comment, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 6, Duration) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool IsStatementReported(TDatabase *Database, int WorldID, TStatement *Statement, bool *Reported){ ASSERT(Database != NULL && Statement != NULL && Reported != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT 1 FROM Statements" " WHERE WorldID = ?1 AND Timestamp = ?2 AND StatementID = ?3"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, Statement->Timestamp) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Statement->StatementID) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } int ErrorCode = sqlite3_step(Stmt); if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *Reported = (ErrorCode == SQLITE_ROW); return true; } bool InsertStatements(TDatabase *Database, int WorldID, int NumStatements, TStatement *Statements){ // NOTE(fusion): Use the `IGNORE` conflict resolution because different // reports may include the same statements for context and I assume it's // not uncommon to see overlaps. ASSERT(Database != NULL && NumStatements > 0 && Statements != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT OR IGNORE INTO Statements (WorldID, Timestamp," " StatementID, CharacterID, Channel, Text)" " VALUES (?1, ?2, ?3, ?4, ?5, ?6)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } for(int i = 0; i < NumStatements; i += 1){ if(Statements[i].StatementID == 0){ LOG_WARN("Skipping statement without id"); continue; } if(sqlite3_bind_int(Stmt, 2, Statements[i].Timestamp) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Statements[i].StatementID) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, Statements[i].CharacterID) != SQLITE_OK || sqlite3_bind_text(Stmt, 5, Statements[i].Channel, -1, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 6, Statements[i].Text, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters for statement %d: %s", Statements[i].StatementID, sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to insert statement %d: %s", Statements[i].StatementID, sqlite3_errmsg(Database->Handle)); return false; } sqlite3_reset(Stmt); } return true; } bool InsertReportedStatement(TDatabase *Database, int WorldID, TStatement *Statement, int BanishmentID, int ReporterID, const char *Reason, const char *Comment){ ASSERT(Database != NULL && Statement != NULL && Reason != NULL && Comment != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO ReportedStatements (WorldID, Timestamp," " StatementID, CharacterID, BanishmentID, ReporterID," " Reason, Comment)" " VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, Statement->Timestamp) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Statement->StatementID) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, Statement->CharacterID) != SQLITE_OK || sqlite3_bind_int(Stmt, 5, BanishmentID) != SQLITE_OK || sqlite3_bind_int(Stmt, 6, ReporterID) != SQLITE_OK || sqlite3_bind_text(Stmt, 7, Reason, -1, NULL) != SQLITE_OK || sqlite3_bind_text(Stmt, 8, Comment, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } // Info Tables //============================================================================== bool GetKillStatistics(TDatabase *Database, int WorldID, DynamicArray *Stats){ ASSERT(Database != NULL && Stats != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT RaceName, TimesKilled, PlayersKilled" " FROM KillStatistics WHERE WorldID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ TKillStatistics Entry = {}; StringBufCopy(Entry.RaceName, (const char*)sqlite3_column_text(Stmt, 0)); Entry.TimesKilled = sqlite3_column_int(Stmt, 1); Entry.PlayersKilled = sqlite3_column_int(Stmt, 2); Stats->Push(Entry); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool MergeKillStatistics(TDatabase *Database, int WorldID, int NumStats, TKillStatistics *Stats){ ASSERT(Database != NULL && NumStats > 0 && Stats != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO KillStatistics (WorldID, RaceName, TimesKilled, PlayersKilled)" " VALUES (?1, ?2, ?3, ?4)" " ON CONFLICT DO UPDATE SET TimesKilled = TimesKilled + EXCLUDED.TimesKilled," " PlayersKilled = PlayersKilled + EXCLUDED.PlayersKilled"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } for(int i = 0; i < NumStats; i += 1){ if(sqlite3_bind_text(Stmt, 2, Stats[i].RaceName, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Stats[i].TimesKilled) != SQLITE_OK || sqlite3_bind_int(Stmt, 4, Stats[i].PlayersKilled) != SQLITE_OK){ LOG_ERR("Failed to bind parameters for \"%s\" stats: %s", Stats[i].RaceName, sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to merge \"%s\" stats: %s", Stats[i].RaceName, sqlite3_errmsg(Database->Handle)); return false; } sqlite3_reset(Stmt); } return true; } bool GetOnlineCharacters(TDatabase *Database, int WorldID, DynamicArray *Characters){ ASSERT(Database != NULL && Characters != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "SELECT Name, Level, Profession" " FROM OnlineCharacters WHERE WorldID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } while(sqlite3_step(Stmt) == SQLITE_ROW){ TOnlineCharacter Character = {}; StringBufCopy(Character.Name, (const char*)sqlite3_column_text(Stmt, 0)); Character.Level = sqlite3_column_int(Stmt, 1); StringBufCopy(Character.Profession, (const char*)sqlite3_column_text(Stmt, 2)); Characters->Push(Character); } if(sqlite3_errcode(Database->Handle) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool DeleteOnlineCharacters(TDatabase *Database, int WorldID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "DELETE FROM OnlineCharacters WHERE WorldID = ?1"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool InsertOnlineCharacters(TDatabase *Database, int WorldID, int NumCharacters, TOnlineCharacter *Characters){ ASSERT(Database != NULL && Characters != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "INSERT INTO OnlineCharacters (WorldID, Name, Level, Profession)" " VALUES (?1, ?2, ?3, ?4)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } for(int i = 0; i < NumCharacters; i += 1){ if(sqlite3_bind_text(Stmt, 2, Characters[i].Name, -1, NULL) != SQLITE_OK || sqlite3_bind_int(Stmt, 3, Characters[i].Level) != SQLITE_OK || sqlite3_bind_text(Stmt, 4, Characters[i].Profession, -1, NULL) != SQLITE_OK){ LOG_ERR("Failed to bind parameters for character \"%s\": %s", Characters[i].Name, sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to insert character \"%s\": %s", Characters[i].Name, sqlite3_errmsg(Database->Handle)); return false; } sqlite3_reset(Stmt); } return true; } bool CheckOnlinePeak(TDatabase *Database, int WorldID, int NumCharacters, bool *NewPeak){ ASSERT(Database != NULL && NewPeak != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Worlds SET OnlinePeak = ?2," " OnlinePeakTimestamp = UNIXEPOCH()" " WHERE WorldID = ?1 AND OnlinePeak < ?2"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK || sqlite3_bind_int(Stmt, 2, NumCharacters) != SQLITE_OK){ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } *NewPeak = (sqlite3_changes(Database->Handle) > 0); return true; } bool CheckWorldStartupTime(TDatabase *Database, int WorldID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Worlds SET LastStartup = UNIXEPOCH()" " WHERE WorldID = ?1 AND LastStartup <= LastShutdown"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } bool CheckWorldShutdownTime(TDatabase *Database, int WorldID){ ASSERT(Database != NULL); sqlite3_stmt *Stmt = PrepareQuery(Database, "UPDATE Worlds SET LastShutdown = UNIXEPOCH()" " WHERE WorldID = ?1 AND LastShutdown <= LastStartup"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } AutoStmtReset StmtReset(Stmt); if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){ LOG_ERR("Failed to bind WorldID: %s", sqlite3_errmsg(Database->Handle)); return false; } if(sqlite3_step(Stmt) != SQLITE_DONE){ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(Database->Handle)); return false; } return true; } #endif //DATABASE_SQLITE