Files
Champions-Reborn-Server-Mirror/Source/Database/Database.cpp
2026-03-02 12:37:07 +00:00

625 lines
17 KiB
C++

#include "Database/Database.hpp"
#include "Crypto/PasswordHash.hpp"
#include "Game/RealmCharacter.hpp"
#include "Game/RealmCharacterMetaKV.hpp"
#include "logging.hpp"
#include <filesystem>
namespace fs = std::filesystem;
Database::Database()
{
const fs::path dbPath = "./database/game.db";
const fs::path dbDir = dbPath.parent_path();
std::error_code ec;
if( !dbDir.empty() && !fs::exists( dbDir ) )
{
if( !fs::create_directories( dbDir, ec ) )
{
throw std::runtime_error(
"Failed to create database directory: " + ec.message()
);
}
}
if( sqlite3_open( dbPath.string().c_str(), &m_db ) != SQLITE_OK )
{
throw std::runtime_error(
"Failed to open DB: " + std::string( sqlite3_errmsg( m_db ) )
);
}
CreateTables();
PrepareStatements();
}
Database::~Database()
{
FinalizeStatements();
if( m_db )
sqlite3_close( m_db );
}
void Database::CreateTables()
{
Execute( "CREATE TABLE IF NOT EXISTS RealmUsers ("
"account_id INTEGER PRIMARY KEY AUTOINCREMENT,"
"username TEXT UNIQUE NOT NULL,"
"password TEXT NOT NULL,"
"email_address TEXT NOT NULL,"
"date_of_birth NOT NULL,"
"chat_handle TEXT NOT NULL)" );
Execute( "CREATE TABLE IF NOT EXISTS RealmCharacters ("
"account_id INTEGER NOT NULL,"
"character_id INTEGER PRIMARY KEY AUTOINCREMENT,"
"meta_data BLOB NOT NULL,"
"character_data BLOB NOT NULL)" );
Execute( "CREATE TABLE IF NOT EXISTS RealmSession ("
"account_id INTEGER NOT NULL,"
"session_id TEXT NOT NULL UNIQUE,"
"character_id INTEGER NOT NULL,"
"ip_address TEXT NOT NULL,"
"expire_time INTEGER NOT NULL,"
"PRIMARY KEY(session_id))" );
Execute( "CREATE TABLE IF NOT EXISTS UserFriendList ("
"account_id INTEGER NOT NULL,"
"friend_handle TEXT NOT NULL,"
"PRIMARY KEY(account_id, friend_handle))" );
Execute( "CREATE TABLE IF NOT EXISTS UserIgnoredList ("
"account_id INTEGER NOT NULL,"
"ignore_handle TEXT NOT NULL,"
"PRIMARY KEY(account_id, ignore_handle))" );
}
void Database::PrepareStatements()
{
const std::vector<std::pair<QueryID, const char *>> queries = {
{ QueryID::CreateAccount,
"INSERT INTO RealmUsers ( username, password, email_address, date_of_birth, chat_handle ) VALUES ( ?, ?, ?, ?, ? );" },
{ QueryID::VerifyAccount,
"SELECT account_id, username, password, chat_handle FROM RealmUsers WHERE username = ?;" },
{ QueryID::LoadAccount,
"SELECT chat_handle FROM RealmUsers WHERE account_id = ?;" },
{ QueryID::CreateNewCharacter,
"INSERT INTO RealmCharacters ( account_id, meta_data, character_data ) VALUES ( ?, ?, ? );" },
{ QueryID::SaveCharacter,
"UPDATE RealmCharacters SET meta_data = ?, character_data = ? WHERE account_id = ? AND character_id = ?;" },
{ QueryID::LoadCharacter,
"SELECT character_id, meta_data, character_data FROM RealmCharacters WHERE account_id = ? AND character_id = ?;" },
{ QueryID::LoadCharacterSlots,
"SELECT character_id, meta_data FROM RealmCharacters WHERE account_id = ? ORDER BY character_id;" },
{ QueryID::SaveFriend,
"INSERT OR IGNORE INTO UserFriendList ( account_id, friend_handle ) VALUES ( ?, ? );" },
{ QueryID::RemoveFriend,
"DELETE FROM UserFriendList WHERE account_id = ? AND friend_handle = ?;" },
{ QueryID::LoadFriendList,
"SELECT friend_handle FROM UserFriendList WHERE account_id = ?;" },
{ QueryID::SaveIgnore,
"INSERT OR IGNORE INTO UserIgnoredList ( account_id, ignore_handle ) VALUES ( ?, ? );" },
{ QueryID::RemoveIgnore,
"DELETE FROM UserIgnoredList WHERE account_id = ? AND ignore_handle = ?;" },
{ QueryID::LoadIgnoreList,
"SELECT ignore_handle FROM UserIgnoredList WHERE account_id = ?;" }
};
try
{
for( const auto &[id, sql] : queries )
{
sqlite3_stmt *stmt = nullptr;
if( sqlite3_prepare_v2( m_db, sql, -1, &stmt, nullptr ) != SQLITE_OK )
{
throw std::runtime_error( "Failed to prepare statement: " + std::string( sqlite3_errmsg( m_db ) ) );
}
m_statements[ id ] = stmt;
}
}
catch( const std::exception &e )
{
FinalizeStatements();
Log::Error( "Database error: {}", std::string( e.what() ) );
}
}
void Database::FinalizeStatements()
{
if( m_db == nullptr )
return;
for( const auto &[id, stmt] : m_statements )
{
if( stmt ) sqlite3_finalize( stmt );
}
m_statements.clear();
}
void Database::Execute( const char *sql )
{
try
{
char *errMsg = nullptr;
if( sqlite3_exec( m_db, sql, nullptr, nullptr, &errMsg ) != SQLITE_OK )
{
std::string error = "SQL execution failed: " + std::string( errMsg );
sqlite3_free( errMsg );
throw std::runtime_error( error );
}
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
}
int64_t Database::CreateNewAccount( const std::string &username,
const std::string &password,
const std::string &email_address,
const std::string &date_of_birth,
const std::string &chat_handle )
{
try
{
auto hashedPassword = HashPassword( password, 1000, 32 );
auto stmt = m_statements[ QueryID::CreateAccount ];
SQLiteTransaction tx( m_db );
{
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_text( stmt, 1, username.c_str(), -1, SQLITE_TRANSIENT );
sqlite3_bind_text( stmt, 2, hashedPassword.c_str(), -1, SQLITE_TRANSIENT );
sqlite3_bind_text( stmt, 3, email_address.c_str(), -1, SQLITE_TRANSIENT );
sqlite3_bind_text( stmt, 4, date_of_birth.c_str(), -1, SQLITE_TRANSIENT );
sqlite3_bind_text( stmt, 5, chat_handle.c_str(), -1, SQLITE_TRANSIENT );
if( sqlite3_step( stmt ) != SQLITE_DONE )
{
throw std::runtime_error( "Insert failed: " + std::string( sqlite3_errmsg( m_db ) ) );
}
}
tx.commit();
return sqlite3_last_insert_rowid( m_db );
}
catch( const std::exception &e )
{
Log::Error( "Database error: " + std::string( e.what() ) );
}
return 0;
}
std::tuple< bool, int64_t, std::wstring >
Database::VerifyAccount( const std::wstring &username, const std::wstring &password )
{
try
{
auto stmt = m_statements[ QueryID::VerifyAccount ];
auto username_utf8 = Util::WideToUTF8( username );
auto password_utf8 = Util::WideToUTF8( password );
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_text( stmt, 1, username_utf8.c_str(), -1, SQLITE_TRANSIENT );
// Execute the statement
if( sqlite3_step( stmt ) == SQLITE_ROW )
{
int64_t accountId = sqlite3_column_int64( stmt, 0 );
const char *dbUsername = reinterpret_cast< const char * >( sqlite3_column_text( stmt, 1 ) );
const char *dbPassword = reinterpret_cast< const char * >( sqlite3_column_text( stmt, 2 ) );
const char *dbChatHandle = reinterpret_cast< const char * >( sqlite3_column_text( stmt, 3 ) );
if( username_utf8 == dbUsername && VerifyPassword( password_utf8, dbPassword ) )
{
Log::Debug( "Account verified: {} (ID: {})", username, accountId );
return std::make_tuple( true, accountId, Util::UTF8ToWide( dbChatHandle ) );
}
else
{
Log::Debug( "Invalid credentials for account ID: {}", accountId );
return std::make_tuple( false, -1, L"" );
}
}
else if( sqlite3_step( stmt ) == SQLITE_DONE )
{
return std::make_tuple( false, -1, L"" ); // No matching account found
}
else
{
throw std::runtime_error( "Query failed: " + std::string( sqlite3_errmsg( m_db ) ) );
}
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return std::make_tuple( false, -1, L"" );
}
uint32_t Database::CreateNewCharacter( const int64_t account_id, const CharacterSlotData meta, const std::vector< uint8_t > &blob )
{
if( account_id <= 0 || meta.empty() || blob.empty() )
{
Log::Error( "Invalid parameters for CreateNewCharacter" );
return 0;
}
try
{
auto stmt = m_statements[ QueryID::CreateNewCharacter ];
const auto meta_data = meta.Serialize();
SQLiteTransaction tx( m_db );
{
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
sqlite3_bind_blob( stmt, 2, meta_data.data(), static_cast< int >( meta_data.size() ), SQLITE_STATIC );
sqlite3_bind_blob( stmt, 3, blob.data(), static_cast< int >( blob.size() ), SQLITE_STATIC );
int rc = sqlite3_step( stmt );
if( rc != SQLITE_DONE )
{
Log::Error( "SQLite insert failed: {}", sqlite3_errmsg( m_db ) );
return 0;
}
}
tx.commit();
uint32_t character_id = static_cast< uint32_t >( sqlite3_last_insert_rowid( m_db ) );
return character_id;
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return 0;
}
bool Database::SaveCharacter( const int64_t account_id, const int32_t character_id, const CharacterSlotData meta, const std::vector< uint8_t > &blob )
{
if( account_id <= 0 || character_id <= 0 || meta.empty() || blob.empty() )
{
Log::Error( "Invalid parameters for SaveCharacter" );
return false;
}
try
{
auto stmt = m_statements[ QueryID::SaveCharacter ];
const auto meta_data = meta.Serialize();
SQLiteTransaction tx( m_db );
{
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_blob( stmt, 1, meta_data.data(), static_cast< int >( meta_data.size() ), SQLITE_STATIC );
sqlite3_bind_blob( stmt, 2, blob.data(), static_cast< int >( blob.size() ), SQLITE_STATIC );
sqlite3_bind_int64( stmt, 3, account_id );
sqlite3_bind_int( stmt, 4, character_id );
if( sqlite3_step( stmt ) != SQLITE_DONE )
{
throw std::runtime_error( "Update failed: " + std::string( sqlite3_errmsg( m_db ) ) );
}
}
tx.commit();
return true;
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return false;
}
std::map< uint32_t, CharacterSlotData > Database::LoadCharacterSlots( const int64_t account_id )
{
std::map< uint32_t, CharacterSlotData > result;
try
{
auto stmt = m_statements[ QueryID::LoadCharacterSlots ];
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
while( sqlite3_step( stmt ) == SQLITE_ROW )
{
const auto character_id = sqlite3_column_int( stmt, 0 );
const auto *slot_data_blob = static_cast< const uint8_t * >( sqlite3_column_blob( stmt, 1 ) );
const auto slot_data_size = sqlite3_column_bytes( stmt, 1 );
if( slot_data_blob == nullptr || slot_data_size == 0 )
{
Log::Error( "Character ID {} has no metadata", character_id );
continue;
}
CharacterSlotData slot_data( std::vector< uint8_t >( slot_data_blob, slot_data_blob + slot_data_size ) );
result.insert( { character_id, slot_data } );
}
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
result.clear();
}
return result;
}
sptr_realm_character Database::LoadCharacterData( const int64_t account_id, const int32_t character_id )
{
try
{
auto data = std::make_shared< RealmCharacter >();
auto stmt = m_statements[ QueryID::LoadCharacter ];
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
sqlite3_bind_int( stmt, 2, character_id );
if( sqlite3_step( stmt ) == SQLITE_ROW )
{
const auto character_id = sqlite3_column_int( stmt, 0 );
const auto *meta_data_blob = static_cast< const uint8_t * >( sqlite3_column_blob( stmt, 1 ) );
const auto meta_data_size = sqlite3_column_bytes( stmt, 1 );
if( meta_data_blob && meta_data_size > 0 )
{
data->Deserialize( std::vector< uint8_t >( meta_data_blob, meta_data_blob + meta_data_size ) );
}
const auto *character_blob = static_cast< const uint8_t * >( sqlite3_column_blob( stmt, 2 ) );
const auto character_size = sqlite3_column_bytes( stmt, 2 );
if( character_blob && character_size > 0 )
{
data->m_characterId = character_id;
data->m_data = std::vector< uint8_t >( character_blob, character_blob + character_size );
}
Log::Debug( "Character data loaded for account ID: {} and character ID: {}", account_id, character_id );
return data;
}
else
{
Log::Error( "No character data found for account ID: {} and character ID: {}", account_id, character_id );
}
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return nullptr;
}
bool Database::SaveFriend( const int64_t account_id, const std::wstring &friend_handle )
{
if( account_id <= 0 || friend_handle.empty() )
{
Log::Error( "Invalid parameters for SaveFriend" );
return false;
}
try
{
auto stmt = m_statements[ QueryID::SaveFriend ];
auto friendHandle = Util::WideToUTF8( friend_handle );
SQLiteTransaction tx( m_db );
{
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
sqlite3_bind_text( stmt, 2, friendHandle.c_str(), -1, SQLITE_TRANSIENT );
if( sqlite3_step( stmt ) != SQLITE_DONE )
{
Log::Error( "SQLite insert failed: {}", sqlite3_errmsg( m_db ) );
return false;
}
}
tx.commit();
return true;
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return false;
}
bool Database::RemoveFriend( const int64_t account_id, const std::wstring &friend_handle )
{
if( account_id <= 0 || friend_handle.empty() )
{
Log::Error( "Invalid parameters for RemoveFriend" );
return false;
}
try
{
auto stmt = m_statements[ QueryID::RemoveFriend ];
auto friendHandle = Util::WideToUTF8( friend_handle );
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
sqlite3_bind_text( stmt, 2, friendHandle.c_str(), -1, SQLITE_TRANSIENT );
if( sqlite3_step( stmt ) != SQLITE_DONE )
{
Log::Error( "SQLite delete failed: {}", sqlite3_errmsg( m_db ) );
return false;
}
return true;
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return false;
}
std::vector<std::wstring> Database::LoadFriends( const int64_t account_id )
{
std::vector<std::wstring> friend_list;
try
{
auto stmt = m_statements[ QueryID::LoadFriendList ];
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
while( sqlite3_step( stmt ) == SQLITE_ROW )
{
const char *friendHandle = reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) );
if( friendHandle )
{
friend_list.push_back( Util::UTF8ToWide( friendHandle ) );
}
}
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return friend_list;
}
bool Database::SaveIgnore( const int64_t account_id, const std::wstring &ignore_handle )
{
if( account_id <= 0 || ignore_handle.empty() )
{
Log::Error( "Invalid parameters for SaveIgnore" );
return false;
}
try
{
auto stmt = m_statements[ QueryID::SaveIgnore ];
auto friendHandle = Util::WideToUTF8( ignore_handle );
SQLiteTransaction tx( m_db );
{
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
sqlite3_bind_text( stmt, 2, friendHandle.c_str(), -1, SQLITE_TRANSIENT );
if( sqlite3_step( stmt ) != SQLITE_DONE )
{
Log::Error( "SQLite insert failed: {}", sqlite3_errmsg( m_db ) );
return false;
}
}
tx.commit();
return true;
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return false;
}
bool Database::RemoveIgnore( const int64_t account_id, const std::wstring &ignore_handle )
{
if( account_id <= 0 || ignore_handle.empty() )
{
Log::Error( "Invalid parameters for RemoveIgnore" );
return false;
}
try
{
auto stmt = m_statements[ QueryID::RemoveIgnore ];
auto ignoreHandle = Util::WideToUTF8( ignore_handle );
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
sqlite3_bind_text( stmt, 2, ignoreHandle.c_str(), -1, SQLITE_TRANSIENT );
if( sqlite3_step( stmt ) != SQLITE_DONE )
{
Log::Error( "SQLite delete failed: {}", sqlite3_errmsg( m_db ) );
return false;
}
return true;
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return false;
}
std::vector<std::wstring> Database::LoadIgnores( const int64_t account_id )
{
std::vector<std::wstring> ignore_list;
try
{
auto stmt = m_statements[ QueryID::LoadIgnoreList ];
sqlite3_reset( stmt );
sqlite3_clear_bindings( stmt );
sqlite3_bind_int64( stmt, 1, account_id );
while( sqlite3_step( stmt ) == SQLITE_ROW )
{
const char *ignoreHandle = reinterpret_cast< const char * >( sqlite3_column_text( stmt, 0 ) );
if( ignoreHandle )
{
ignore_list.push_back( Util::UTF8ToWide( ignoreHandle ) );
}
}
}
catch( const std::exception &e )
{
Log::Error( "Database error: {}", std::string( e.what() ) );
}
return ignore_list;
}