From a60865cd19c8c1dab48036847e6f74d9bef1a20a Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:07:07 -0800 Subject: [PATCH] feat: allow SQLite database backend (#1663) * simplify leaderboard code, fully abstract database * update exception catching * update exception catching and sql references, remove ugc from gamemessages fix deleting model remove unrelated changes Update GameMessages.cpp * remove ugc from gamemessages * Update GameMessages.cpp * Update Leaderboard.cpp * bug fixes * fix racing leaderboard * remove extra stuff * update * add sqlite * use a default for optimizations * update sqlite * Fix limits on update and delete * fix bugs * use definition to switch between databases * add switch for different backends * fix include guard and includes * always build both * add mysql if block * Update Database.cpp * add new options and add check to prevent overriding mysql * correct config names * Update README.md * Update README.md * merge to 1 sql file for sqlite database * move to sqlite folder * add back mysql migrations * Update README.md * add migration to correct the folder name or mysql * yes aron * updates * Update CMakeLists.txt * dont use paths at all, add where check to only update if folder name still exist check also doesnt check for slashes and assumes one will be there since it will be. * default dont auto create account for releases we can change this flag * default 0 * add times played query * fix leaderboard not incrementing on a not better score * add env vars with defaults for docker * use an "enum" * default to mariadb * Update .env.example --- .env.example | 8 + .gitignore | 1 - CMakeLists.txt | 33 +-- README.md | 36 ++- dCommon/CMakeLists.txt | 1 - dDatabase/GameDatabase/CMakeLists.txt | 13 +- dDatabase/GameDatabase/Database.cpp | 28 +- dDatabase/GameDatabase/Database.h | 2 + dDatabase/GameDatabase/GameDatabase.h | 8 +- dDatabase/GameDatabase/ITables/IAccounts.h | 2 + .../GameDatabase/MySQL/MySQLDatabase.cpp | 5 +- dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 3 +- .../GameDatabase/MySQL/Tables/Accounts.cpp | 5 + dDatabase/GameDatabase/SQLite/CMakeLists.txt | 11 + .../GameDatabase/SQLite/SQLiteDatabase.cpp | 73 +++++ .../GameDatabase/SQLite/SQLiteDatabase.h | 270 ++++++++++++++++++ .../GameDatabase/SQLite/Tables/Accounts.cpp | 49 ++++ .../SQLite/Tables/AccountsRewardCodes.cpp | 17 ++ .../SQLite/Tables/ActivityLog.cpp | 6 + .../GameDatabase/SQLite/Tables/Behaviors.cpp | 19 ++ .../GameDatabase/SQLite/Tables/BugReports.cpp | 6 + .../GameDatabase/SQLite/Tables/CMakeLists.txt | 26 ++ .../GameDatabase/SQLite/Tables/CharInfo.cpp | 79 +++++ .../GameDatabase/SQLite/Tables/CharXml.cpp | 19 ++ .../GameDatabase/SQLite/Tables/CommandLog.cpp | 5 + .../GameDatabase/SQLite/Tables/Friends.cpp | 73 +++++ .../GameDatabase/SQLite/Tables/IgnoreList.cpp | 22 ++ .../SQLite/Tables/Leaderboard.cpp | 91 ++++++ dDatabase/GameDatabase/SQLite/Tables/Mail.cpp | 83 ++++++ .../SQLite/Tables/MigrationHistory.cpp | 13 + .../SQLite/Tables/ObjectIdTracker.cpp | 17 ++ .../GameDatabase/SQLite/Tables/PetNames.cpp | 26 ++ .../GameDatabase/SQLite/Tables/PlayKeys.cpp | 11 + .../SQLite/Tables/PlayerCheatDetections.cpp | 7 + .../GameDatabase/SQLite/Tables/Property.cpp | 195 +++++++++++++ .../SQLite/Tables/PropertyContents.cpp | 65 +++++ .../GameDatabase/SQLite/Tables/Servers.cpp | 23 ++ dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp | 72 +++++ .../SQLite/Tables/UgcModularBuild.cpp | 9 + .../GameDatabase/TestSQL/TestSQLDatabase.cpp | 4 - .../GameDatabase/TestSQL/TestSQLDatabase.h | 2 +- dDatabase/MigrationRunner.cpp | 27 +- dGame/CMakeLists.txt | 1 - dGame/dComponents/CMakeLists.txt | 1 - dMasterServer/MasterServer.cpp | 10 +- dMasterServer/Start.cpp | 6 +- dNet/AuthPackets.cpp | 4 +- dNet/CMakeLists.txt | 1 - docker-compose.yml | 3 + .../cdserver/7_migration_for_migrations.sql | 0 migrations/dlu/{ => mysql}/0_initial.sql | 0 .../dlu/{ => mysql}/10_Security_updates.sql | 0 .../11_fix_cheat_detection_table.sql | 0 .../dlu/{ => mysql}/12_modular_build_ugc.sql | 0 migrations/dlu/{ => mysql}/13_ignore_list.sql | 0 .../dlu/{ => mysql}/14_reward_codes.sql | 0 .../dlu/{ => mysql}/15_behavior_owner.sql | 0 .../dlu/{ => mysql}/16_big_behaviors.sql | 0 .../dlu/mysql/17_migration_for_migrations.sql | 1 + .../{ => mysql}/1_unique_charinfo_names.sql | 0 migrations/dlu/{ => mysql}/2_reporter_id.sql | 0 .../{ => mysql}/3_add_performance_cost.sql | 0 .../{ => mysql}/4_friends_list_objectids.sql | 0 .../dlu/{ => mysql}/5_brick_model_sd0.sql | 0 .../dlu/{ => mysql}/6_property_behaviors.sql | 0 .../7_make_play_key_id_nullable.sql | 0 .../dlu/{ => mysql}/8_foreign_play_key.sql | 0 .../9_Update_Leaderboard_Storage.sql | 0 migrations/dlu/sqlite/0_initial.sql | 198 +++++++++++++ resources/sharedconfig.ini | 9 +- thirdparty/SQLite/CMakeLists.txt | 2 +- thirdparty/SQLite/CppSQLite3.cpp | 20 ++ thirdparty/SQLite/CppSQLite3.h | 2 + versions.txt | 1 + 74 files changed, 1651 insertions(+), 73 deletions(-) create mode 100644 dDatabase/GameDatabase/SQLite/CMakeLists.txt create mode 100644 dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp create mode 100644 dDatabase/GameDatabase/SQLite/SQLiteDatabase.h create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/AccountsRewardCodes.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/ActivityLog.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Behaviors.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/BugReports.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt create mode 100644 dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/CharXml.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/CommandLog.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Friends.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/IgnoreList.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Leaderboard.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Mail.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/MigrationHistory.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/PlayKeys.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/PlayerCheatDetections.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Property.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Servers.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp create mode 100644 migrations/cdserver/7_migration_for_migrations.sql rename migrations/dlu/{ => mysql}/0_initial.sql (100%) rename migrations/dlu/{ => mysql}/10_Security_updates.sql (100%) rename migrations/dlu/{ => mysql}/11_fix_cheat_detection_table.sql (100%) rename migrations/dlu/{ => mysql}/12_modular_build_ugc.sql (100%) rename migrations/dlu/{ => mysql}/13_ignore_list.sql (100%) rename migrations/dlu/{ => mysql}/14_reward_codes.sql (100%) rename migrations/dlu/{ => mysql}/15_behavior_owner.sql (100%) rename migrations/dlu/{ => mysql}/16_big_behaviors.sql (100%) create mode 100644 migrations/dlu/mysql/17_migration_for_migrations.sql rename migrations/dlu/{ => mysql}/1_unique_charinfo_names.sql (100%) rename migrations/dlu/{ => mysql}/2_reporter_id.sql (100%) rename migrations/dlu/{ => mysql}/3_add_performance_cost.sql (100%) rename migrations/dlu/{ => mysql}/4_friends_list_objectids.sql (100%) rename migrations/dlu/{ => mysql}/5_brick_model_sd0.sql (100%) rename migrations/dlu/{ => mysql}/6_property_behaviors.sql (100%) rename migrations/dlu/{ => mysql}/7_make_play_key_id_nullable.sql (100%) rename migrations/dlu/{ => mysql}/8_foreign_play_key.sql (100%) rename migrations/dlu/{ => mysql}/9_Update_Leaderboard_Storage.sql (100%) create mode 100644 migrations/dlu/sqlite/0_initial.sql diff --git a/.env.example b/.env.example index 22661252..6ea77deb 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,16 @@ NET_VERSION=171022 ACCOUNT_MANAGER_SECRET= # Should be the externally facing IP of your server host EXTERNAL_IP=localhost + +# The database type that will be used. +# Acceptable values are `sqlite`, `mysql`, `mariadb`, `maria`. +# Case insensitive. +DATABASE_TYPE=mariadb +SQLITE_DATABASE_PATH=resServer/dlu.sqlite + # Database values # Be careful with special characters here. It is more safe to use normal characters and/or numbers. MARIADB_USER=darkflame MARIADB_PASSWORD= MARIADB_DATABASE=darkflame +SKIP_ACCOUNT_CREATION=1 diff --git a/.gitignore b/.gitignore index 3ad1009e..d7af5d1f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ docker/configs # Third party libraries thirdparty/mysql/ thirdparty/mysql_linux/ -CMakeVariables.txt # Build folders build/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ff4e6b3..be72a3a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,16 +175,18 @@ foreach(resource_file ${RESOURCE_FILES}) list(GET line_split 0 variable_name) if(NOT ${parsed_current_file_contents} MATCHES ${variable_name}) - message(STATUS "Adding missing config option " ${variable_name} " to " ${resource_file}) - set(line_to_add ${line_to_add} ${line}) + # For backwards compatibility with older setup versions, dont add this option. + if(NOT ${variable_name} MATCHES "database_type") + message(STATUS "Adding missing config option " ${variable_name} " to " ${resource_file}) + set(line_to_add ${line_to_add} ${line}) - foreach(line_to_append ${line_to_add}) - file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n" ${line_to_append}) - endforeach() + foreach(line_to_append ${line_to_add}) + file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n" ${line_to_append}) + endforeach() - file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n") + file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n") + endif() endif() - set(line_to_add "") else() set(line_to_add ${line_to_add} ${line}) @@ -214,21 +216,8 @@ foreach(file ${VANITY_FILES}) endforeach() # Move our migrations for MasterServer to run -file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/migrations/dlu/) -file(GLOB SQL_FILES ${CMAKE_SOURCE_DIR}/migrations/dlu/*.sql) - -foreach(file ${SQL_FILES}) - get_filename_component(file ${file} NAME) - configure_file(${CMAKE_SOURCE_DIR}/migrations/dlu/${file} ${PROJECT_BINARY_DIR}/migrations/dlu/${file}) -endforeach() - -file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/migrations/cdserver/) -file(GLOB SQL_FILES ${CMAKE_SOURCE_DIR}/migrations/cdserver/*.sql) - -foreach(file ${SQL_FILES}) - get_filename_component(file ${file} NAME) - configure_file(${CMAKE_SOURCE_DIR}/migrations/cdserver/${file} ${PROJECT_BINARY_DIR}/migrations/cdserver/${file}) -endforeach() +file(REMOVE_RECURSE ${PROJECT_BINARY_DIR}/migrations) +file(COPY ${CMAKE_SOURCE_DIR}/migrations DESTINATION ${CMAKE_BINARY_DIR}) # Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux) if (APPLE) diff --git a/README.md b/README.md index 487b68ca..a402a163 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,32 @@ Darkflame Universe is licensed under AGPLv3, please read [LICENSE](LICENSE). Som * You must disclose any changes you make to the code when you distribute it * Hosting a server for others counts as distribution -## Disclaimers -### Setup difficulty -Throughout the entire build and setup process a level of familiarity with the command line and preferably a Unix-like development environment is greatly advantageous. - ### Hosting a server We do not recommend hosting public servers. Darkflame Universe is intended for small scale deployment, for example within a group of friends. It has not been tested for large scale deployment which comes with additional security risks. ### Supply of resource files Darkflame Universe is a server emulator and does not distribute any LEGO® Universe files. A separate game client is required to setup this server emulator and play the game, which we cannot supply. Users are strongly suggested to refer to the safe checksums listed [here](#verifying-your-client-files) to see if a client will work. -## Step by step walkthrough for a single-player server -If you would like a setup for a single player server only on a Windows machine, use the [Native Windows Setup Guide by HailStorm](https://gist.github.com/HailStorm32/169df65a47a104199b5cc57d10fa57de) and skip this README. +## Setting up a single player server +* If you don't know what WSL is, skip this warning. + Warning: WSL version 1 does NOT support using sqlite as a database due to how it handles filesystem synchronization. + You must use Version 2 if you must run the server under WSL. Not doing so will result in save data loss. +* Single player installs now no longer require building the server from source or installing development tools. +* Download the [latest release](https://github.com/DarkflameUniverse/DarkflameServer/releases) and extract the files into a folder inside your client. +* You should be able to see the folder with the server executables in the same folder as `legouniverse.exe`. +* Open `sharedconfig.ini` and find the line that says `client_location` and put `..` after it so the line reads `client_location=..`. +* To run the server, double-click `MasterServer.exe`. +* You will be asked to create an account the first time you run the server. +* When shutting down the server, it is highly recommended to click the `MasterServer.exe` window and hold `ctrl` while pressing `c` to stop the server. +* We are working on a way to make it so when you close the game, the server saves automatically alongside when you open the game, the server starts automatically. -## Steps to setup server +**If you are not planning on hosting a server for others, working in the codebase or wanting to use MariaDB for a database, you can stop reading here.** + +If you would like to use a MariaDB as a database instead of the default of sqlite, follow the steps [here](#database-setup). + +# Steps to setup a development environment * [Clone this repository](#clone-the-repository) +* [Setting up a development environment](#setting-up-a-development-environment) * [Install dependencies](#install-dependencies) * [Database setup](#database-setup) * [Build the server](#build-the-server) @@ -39,6 +50,13 @@ If you would like a setup for a single player server only on a Windows machine, * [User Guide](#user-guide) * [Docker](#docker) +## Disclaimers +### Setup difficulty +Throughout the entire build and setup process a level of familiarity with the command line and preferably a Unix-like development environment is greatly advantageous. + +## Step by step walkthrough for building a single-player Windows server from source +If you would like a setup for a single player server only on a Windows machine built from source, use the [Native Windows Setup Guide by HailStorm](https://gist.github.com/HailStorm32/169df65a47a104199b5cc57d10fa57de) and skip this README. + ## Clone the repository If you are on Windows, you will need to download and install git from [here](https://git-scm.com/download/win) @@ -266,8 +284,8 @@ systemctl stop darkflame.service journalctl -xeu darkflame.service ``` -### First admin user -Run `MasterServer -a` to get prompted to create an admin account. This method is only intended for the system administrator as a means to get started, do NOT use this method to create accounts for other users! +### First user or adding more users. +The first time you run `MasterServer`, you will be prompted to create an account. To create more accounts from the command line, `MasterServer -a` to get prompted to create an admin account. This method is only intended for the system administrator as a means to get started, do NOT use this method to create accounts for other users! ### Account management tool (Nexus Dashboard) **If you are just using this server for yourself, you can skip setting up Nexus Dashboard** diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index d020ff72..18fda0ed 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -37,7 +37,6 @@ target_include_directories(dCommon "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase" - "${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include" ) if (UNIX) diff --git a/dDatabase/GameDatabase/CMakeLists.txt b/dDatabase/GameDatabase/CMakeLists.txt index 32fe414a..fc5500ec 100644 --- a/dDatabase/GameDatabase/CMakeLists.txt +++ b/dDatabase/GameDatabase/CMakeLists.txt @@ -8,6 +8,12 @@ foreach(file ${DDATABSE_DATABSES_MYSQL_SOURCES}) set(DDATABASE_GAMEDATABASE_SOURCES ${DDATABASE_GAMEDATABASE_SOURCES} "MySQL/${file}") endforeach() +add_subdirectory(SQLite) + +foreach(file ${DDATABSE_DATABSES_SQLITE_SOURCES}) + set(DDATABASE_GAMEDATABASE_SOURCES ${DDATABASE_GAMEDATABASE_SOURCES} "SQLite/${file}") +endforeach() + add_subdirectory(TestSQL) foreach(file ${DDATABSE_DATABSES_TEST_SQL_SOURCES}) @@ -16,13 +22,14 @@ endforeach() add_library(dDatabaseGame STATIC ${DDATABASE_GAMEDATABASE_SOURCES}) target_include_directories(dDatabaseGame PUBLIC "." - "ITables" PRIVATE "MySQL" "TestSQL" + "ITables" PRIVATE "MySQL" "SQLite" "TestSQL" "${PROJECT_SOURCE_DIR}/dCommon" "${PROJECT_SOURCE_DIR}/dCommon/dEnums" ) + target_link_libraries(dDatabaseGame - PUBLIC MariaDB::ConnCpp - INTERFACE dCommon) + INTERFACE dCommon + PRIVATE sqlite3 MariaDB::ConnCpp) # Glob together all headers that need to be precompiled file( diff --git a/dDatabase/GameDatabase/Database.cpp b/dDatabase/GameDatabase/Database.cpp index fef9ab39..73626988 100644 --- a/dDatabase/GameDatabase/Database.cpp +++ b/dDatabase/GameDatabase/Database.cpp @@ -2,22 +2,46 @@ #include "Game.h" #include "dConfig.h" #include "Logger.h" -#include "MySQLDatabase.h" #include "DluAssert.h" +#include "SQLiteDatabase.h" +#include "MySQLDatabase.h" + +#include + #pragma warning (disable:4251) //Disables SQL warnings namespace { GameDatabase* database = nullptr; } +std::string Database::GetMigrationFolder() { + const std::set validMysqlTypes = { "mysql", "mariadb", "maria" }; + auto databaseType = Game::config->GetValue("database_type"); + std::ranges::transform(databaseType, databaseType.begin(), ::tolower); + if (databaseType == "sqlite") return "sqlite"; + else if (validMysqlTypes.contains(databaseType)) return "mysql"; + else { + LOG("No database specified, using MySQL"); + return "mysql"; + } +} + void Database::Connect() { if (database) { LOG("Tried to connect to database when it's already connected!"); return; } - database = new MySQLDatabase(); + const auto databaseType = GetMigrationFolder(); + + if (databaseType == "sqlite") database = new SQLiteDatabase(); + else if (databaseType == "mysql") database = new MySQLDatabase(); + else { + LOG("Invalid database type specified in config, using MySQL"); + database = new MySQLDatabase(); + } + database->Connect(); } diff --git a/dDatabase/GameDatabase/Database.h b/dDatabase/GameDatabase/Database.h index cd0e93e3..cb74431c 100644 --- a/dDatabase/GameDatabase/Database.h +++ b/dDatabase/GameDatabase/Database.h @@ -12,4 +12,6 @@ namespace Database { // Used for assigning a test database as the handler for database logic. // Do not use in production code. void _setDatabase(GameDatabase* const db); + + std::string GetMigrationFolder(); }; diff --git a/dDatabase/GameDatabase/GameDatabase.h b/dDatabase/GameDatabase/GameDatabase.h index dcfac4a2..d0b5c866 100644 --- a/dDatabase/GameDatabase/GameDatabase.h +++ b/dDatabase/GameDatabase/GameDatabase.h @@ -26,13 +26,8 @@ #include "IBehaviors.h" #include "IUgcModularBuild.h" -namespace sql { - class Statement; - class PreparedStatement; -}; - #ifdef _DEBUG -# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (sql::SQLException& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0) +# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0) #else # define DLU_SQL_TRY_CATCH_RETHROW(x) x #endif // _DEBUG @@ -50,7 +45,6 @@ public: virtual void Connect() = 0; virtual void Destroy(std::string source = "") = 0; virtual void ExecuteCustomQuery(const std::string_view query) = 0; - virtual sql::PreparedStatement* CreatePreppedStmt(const std::string& query) = 0; virtual void Commit() = 0; virtual bool GetAutoCommit() = 0; virtual void SetAutoCommit(bool value) = 0; diff --git a/dDatabase/GameDatabase/ITables/IAccounts.h b/dDatabase/GameDatabase/ITables/IAccounts.h index a0377f4b..13ecf29b 100644 --- a/dDatabase/GameDatabase/ITables/IAccounts.h +++ b/dDatabase/GameDatabase/ITables/IAccounts.h @@ -36,6 +36,8 @@ public: // Update the GameMaster level of an account. virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0; + + virtual uint32_t GetAccountCount() = 0; }; #endif //!__IACCOUNTS__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp b/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp index 20e92677..26693631 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.cpp @@ -14,6 +14,7 @@ namespace { }; void MySQLDatabase::Connect() { + LOG("Using MySQL database"); driver = sql::mariadb::get_driver_instance(); // The mariadb connector is *supposed* to handle unix:// and pipe:// prefixes to hostName, but there are bugs where @@ -67,7 +68,7 @@ void MySQLDatabase::ExecuteCustomQuery(const std::string_view query) { sql::PreparedStatement* MySQLDatabase::CreatePreppedStmt(const std::string& query) { if (!con) { - Connect(); + Database::Get()->Connect(); LOG("Trying to reconnect to MySQL"); } @@ -76,7 +77,7 @@ sql::PreparedStatement* MySQLDatabase::CreatePreppedStmt(const std::string& quer con = nullptr; - Connect(); + Database::Get()->Connect(); LOG("Trying to reconnect to MySQL from invalid or closed connection"); } diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 29fd7ea8..08168141 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -30,7 +30,6 @@ public: void Connect() override; void Destroy(std::string source = "") override; - sql::PreparedStatement* CreatePreppedStmt(const std::string& query) override; void Commit() override; bool GetAutoCommit() override; void SetAutoCommit(bool value) override; @@ -125,6 +124,8 @@ public: void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override; void DeleteUgcBuild(const LWOOBJID bigId) override; + sql::PreparedStatement* CreatePreppedStmt(const std::string& query); + uint32_t GetAccountCount() override; private: // Generic query functions that can be used for any query. diff --git a/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp b/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp index 9e9812f3..f4310dd8 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp @@ -39,3 +39,8 @@ void MySQLDatabase::InsertNewAccount(const std::string_view username, const std: void MySQLDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) { ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast(gmLevel), accountId); } + +uint32_t MySQLDatabase::GetAccountCount() { + auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;"); + return res->next() ? res->getUInt("count") : 0; +} diff --git a/dDatabase/GameDatabase/SQLite/CMakeLists.txt b/dDatabase/GameDatabase/SQLite/CMakeLists.txt new file mode 100644 index 00000000..6553ad01 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/CMakeLists.txt @@ -0,0 +1,11 @@ +SET(DDATABSE_DATABSES_SQLITE_SOURCES + "SQLiteDatabase.cpp" +) + +add_subdirectory(Tables) + +foreach(file ${DDATABASES_DATABASES_SQLITE_TABLES_SOURCES}) + set(DDATABSE_DATABSES_SQLITE_SOURCES ${DDATABSE_DATABSES_SQLITE_SOURCES} "Tables/${file}") +endforeach() + +set(DDATABSE_DATABSES_SQLITE_SOURCES ${DDATABSE_DATABSES_SQLITE_SOURCES} PARENT_SCOPE) diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp new file mode 100644 index 00000000..635ca8fb --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.cpp @@ -0,0 +1,73 @@ +#include "SQLiteDatabase.h" + +#include "Database.h" +#include "Game.h" +#include "dConfig.h" +#include "Logger.h" +#include "dPlatforms.h" + +// Static Variables + +// Status Variables +namespace { + CppSQLite3DB* con = nullptr; + bool isConnected = false; +}; + +void SQLiteDatabase::Connect() { + LOG("Using SQLite database"); + con = new CppSQLite3DB(); + con->open(Game::config->GetValue("sqlite_database_path").c_str()); + isConnected = true; + + // Make sure wal is enabled for the database. + con->execQuery("PRAGMA journal_mode = WAL;"); +} + +void SQLiteDatabase::Destroy(std::string source) { + if (!con) return; + + if (source.empty()) LOG("Destroying SQLite connection!"); + else LOG("Destroying SQLite connection from %s!", source.c_str()); + + con->close(); + delete con; + con = nullptr; +} + +void SQLiteDatabase::ExecuteCustomQuery(const std::string_view query) { + con->compileStatement(query.data()).execDML(); +} + +CppSQLite3Statement SQLiteDatabase::CreatePreppedStmt(const std::string& query) { + return con->compileStatement(query.c_str()); +} + +void SQLiteDatabase::Commit() { + if (!con->IsAutoCommitOn()) con->compileStatement("COMMIT;").execDML(); +} + +bool SQLiteDatabase::GetAutoCommit() { + return con->IsAutoCommitOn(); +} + +void SQLiteDatabase::SetAutoCommit(bool value) { + if (value) { + if (GetAutoCommit()) con->compileStatement("BEGIN;").execDML(); + } else { + if (!GetAutoCommit()) con->compileStatement("COMMIT;").execDML(); + } +} + +void SQLiteDatabase::DeleteCharacter(const uint32_t characterId) { + ExecuteDelete("DELETE FROM charxml WHERE id=?;", characterId); + ExecuteDelete("DELETE FROM command_log WHERE character_id=?;", characterId); + ExecuteDelete("DELETE FROM friends WHERE player_id=? OR friend_id=?;", characterId, characterId); + ExecuteDelete("DELETE FROM leaderboard WHERE character_id=?;", characterId); + ExecuteDelete("DELETE FROM properties_contents WHERE property_id IN (SELECT id FROM properties WHERE owner_id=?);", characterId); + ExecuteDelete("DELETE FROM properties WHERE owner_id=?;", characterId); + ExecuteDelete("DELETE FROM ugc WHERE character_id=?;", characterId); + ExecuteDelete("DELETE FROM activity_log WHERE character_id=?;", characterId); + ExecuteDelete("DELETE FROM mail WHERE receiver_id=?;", characterId); + ExecuteDelete("DELETE FROM charinfo WHERE id=?;", characterId); +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h new file mode 100644 index 00000000..a09c72c9 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -0,0 +1,270 @@ +#ifndef SQLITEDATABASE_H +#define SQLITEDATABASE_H + +#include "CppSQLite3.h" + +#include "GameDatabase.h" + +using PreppedStmtRef = CppSQLite3Statement&; + +// Purposefully no definition for this to provide linker errors in the case someone tries to +// bind a parameter to a type that isn't defined. +template +inline void SetParam(PreppedStmtRef stmt, const int index, const ParamType param); + +// This is a function to set each parameter in a prepared statement. +// This is accomplished with a combination of parameter packing and Fold Expressions. +// The constexpr if statement is used to prevent the compiler from trying to call SetParam with 0 arguments. +template +void SetParams(PreppedStmtRef stmt, Args&&... args) { + if constexpr (sizeof...(args) != 0) { + int i = 1; + (SetParam(stmt, i++, args), ...); + } +} + +class SQLiteDatabase : public GameDatabase { +public: + void Connect() override; + void Destroy(std::string source = "") override; + + void Commit() override; + bool GetAutoCommit() override; + void SetAutoCommit(bool value) override; + void ExecuteCustomQuery(const std::string_view query) override; + + // Overloaded queries + std::optional GetMasterInfo() override; + + std::vector GetApprovedCharacterNames() override; + + std::vector GetFriendsList(uint32_t charID) override; + + std::optional GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) override; + void SetBestFriendStatus(const uint32_t playerAccountId, const uint32_t friendAccountId, const uint32_t bestFriendStatus) override; + void AddFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; + void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override; + void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override; + void DeleteUgcModelData(const LWOOBJID& modelId) override; + void UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) override; + std::vector GetAllUgcModels() override; + void CreateMigrationHistoryTable() override; + bool IsMigrationRun(const std::string_view str) override; + void InsertMigration(const std::string_view str) override; + std::optional GetCharacterInfo(const uint32_t charId) override; + std::optional GetCharacterInfo(const std::string_view charId) override; + std::string GetCharacterXml(const uint32_t accountId) override; + void UpdateCharacterXml(const uint32_t characterId, const std::string_view lxfml) override; + std::optional GetAccountInfo(const std::string_view username) override; + void InsertNewCharacter(const ICharInfo::Info info) override; + void InsertCharacterXml(const uint32_t accountId, const std::string_view lxfml) override; + std::vector GetAccountCharacterIds(uint32_t accountId) override; + void DeleteCharacter(const uint32_t characterId) override; + void SetCharacterName(const uint32_t characterId, const std::string_view name) override; + void SetPendingCharacterName(const uint32_t characterId, const std::string_view name) override; + void UpdateLastLoggedInCharacter(const uint32_t characterId) override; + void SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) override; + std::optional GetPetNameInfo(const LWOOBJID& petId) override; + std::optional GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) override; + void UpdatePropertyModerationInfo(const IProperty::Info& info) override; + void UpdatePropertyDetails(const IProperty::Info& info) override; + void InsertNewProperty(const IProperty::Info& info, const uint32_t templateId, const LWOZONEID& zoneId) override; + std::vector GetPropertyModels(const LWOOBJID& propertyId) override; + void RemoveUnreferencedUgcModels() override; + void InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) override; + void UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) override; + void RemoveModel(const LWOOBJID& modelId) override; + void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override; + void InsertNewBugReport(const IBugReports::Info& info) override; + void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override; + void InsertNewMail(const IMail::MailInfo& mail) override; + void InsertNewUgcModel( + std::istringstream& sd0Data, + const uint32_t blueprintId, + const uint32_t accountId, + const uint32_t characterId) override; + std::vector GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override; + std::optional GetMail(const uint64_t mailId) override; + uint32_t GetUnreadMailCount(const uint32_t characterId) override; + void MarkMailRead(const uint64_t mailId) override; + void DeleteMail(const uint64_t mailId) override; + void ClaimMailItem(const uint64_t mailId) override; + void InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) override; + void UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) override; + void UpdateAccountBan(const uint32_t accountId, const bool banned) override; + void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override; + void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override; + void SetMasterIp(const std::string_view ip, const uint32_t port) override; + std::optional GetCurrentPersistentId() override; + void InsertDefaultPersistentId() override; + void UpdatePersistentId(const uint32_t id) override; + std::optional GetDonationTotal(const uint32_t activityId) override; + std::optional IsPlaykeyActive(const int32_t playkeyId) override; + std::vector GetUgcModels(const LWOOBJID& propertyId) override; + void AddIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) override; + void RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) override; + std::vector GetIgnoreList(const uint32_t playerId) override; + void InsertRewardCode(const uint32_t account_id, const uint32_t reward_code) override; + std::vector GetRewardCodesByAccountID(const uint32_t account_id) override; + void AddBehavior(const IBehaviors::Info& info) override; + std::string GetBehavior(const int32_t behaviorId) override; + void RemoveBehavior(const int32_t characterId) override; + void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; + std::optional GetProperties(const IProperty::PropertyLookup& params) override; + std::vector GetDescendingLeaderboard(const uint32_t activityId) override; + std::vector GetAscendingLeaderboard(const uint32_t activityId) override; + std::vector GetNsLeaderboard(const uint32_t activityId) override; + std::vector GetAgsLeaderboard(const uint32_t activityId) override; + void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override; + void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override; + std::optional GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override; + void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override; + void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override; + void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override; + void DeleteUgcBuild(const LWOOBJID bigId) override; + uint32_t GetAccountCount() override; +private: + CppSQLite3Statement CreatePreppedStmt(const std::string& query); + + // Generic query functions that can be used for any query. + // Return type may be different depending on the query, so it is up to the caller to check the return type. + // The first argument is the query string, and the rest are the parameters to bind to the query. + // The return type is a unique_ptr to the result set, which is deleted automatically when it goes out of scope + template + inline std::pair ExecuteSelect(const std::string& query, Args&&... args) { + std::pair toReturn; + toReturn.first = CreatePreppedStmt(query); + SetParams(toReturn.first, std::forward(args)...); + DLU_SQL_TRY_CATCH_RETHROW(toReturn.second = toReturn.first.execQuery()); + return toReturn; + } + + template + inline void ExecuteDelete(const std::string& query, Args&&... args) { + auto preppedStmt = CreatePreppedStmt(query); + SetParams(preppedStmt, std::forward(args)...); + DLU_SQL_TRY_CATCH_RETHROW(preppedStmt.execDML()); + } + + template + inline int32_t ExecuteUpdate(const std::string& query, Args&&... args) { + auto preppedStmt = CreatePreppedStmt(query); + SetParams(preppedStmt, std::forward(args)...); + DLU_SQL_TRY_CATCH_RETHROW(return preppedStmt.execDML()); + } + + template + inline int ExecuteInsert(const std::string& query, Args&&... args) { + auto preppedStmt = CreatePreppedStmt(query); + SetParams(preppedStmt, std::forward(args)...); + DLU_SQL_TRY_CATCH_RETHROW(return preppedStmt.execDML()); + } +}; + +// Below are each of the definitions of SetParam for each supported type. + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const std::string_view param) { + LOG("%s", param.data()); + stmt.bind(index, param.data()); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const char* param) { + LOG("%s", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const std::string param) { + LOG("%s", param.c_str()); + stmt.bind(index, param.c_str()); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const int8_t param) { + LOG("%u", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const uint8_t param) { + LOG("%d", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const int16_t param) { + LOG("%u", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const uint16_t param) { + LOG("%d", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const uint32_t param) { + LOG("%u", param); + stmt.bind(index, static_cast(param)); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const int32_t param) { + LOG("%d", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const int64_t param) { + LOG("%llu", param); + stmt.bind(index, static_cast(param)); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const uint64_t param) { + LOG("%llu", param); + stmt.bind(index, static_cast(param)); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const float param) { + LOG("%f", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const double param) { + LOG("%f", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const bool param) { + LOG("%d", param); + stmt.bind(index, param); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const std::istream* param) { + LOG("Blob"); + // This is the one time you will ever see me use const_cast. + std::stringstream stream; + stream << param->rdbuf(); + stmt.bind(index, reinterpret_cast(stream.str().c_str()), stream.str().size()); +} + +template<> +inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional param) { + if (param) { + LOG("%d", param.value()); + stmt.bind(index, static_cast(param.value())); + } else { + LOG("Null"); + stmt.bindNull(index); + } +} + +#endif //!SQLITEDATABASE_H diff --git a/dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp b/dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp new file mode 100644 index 00000000..9431d407 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp @@ -0,0 +1,49 @@ +#include "SQLiteDatabase.h" + +#include "eGameMasterLevel.h" +#include "Database.h" + +std::optional SQLiteDatabase::GetAccountInfo(const std::string_view username) { + auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE name = ? LIMIT 1", username); + + if (result.eof()) { + return std::nullopt; + } + + IAccounts::Info toReturn; + toReturn.id = result.getIntField("id"); + toReturn.maxGmLevel = static_cast(result.getIntField("gm_level")); + toReturn.bcryptPassword = result.getStringField("password"); + toReturn.banned = result.getIntField("banned"); + toReturn.locked = result.getIntField("locked"); + toReturn.playKeyId = result.getIntField("play_key_id"); + + return toReturn; +} + +void SQLiteDatabase::UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) { + ExecuteUpdate("UPDATE accounts SET mute_expire = ? WHERE id = ?;", timeToUnmute, accountId); +} + +void SQLiteDatabase::UpdateAccountBan(const uint32_t accountId, const bool banned) { + ExecuteUpdate("UPDATE accounts SET banned = ? WHERE id = ?;", banned, accountId); +} + +void SQLiteDatabase::UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) { + ExecuteUpdate("UPDATE accounts SET password = ? WHERE id = ?;", bcryptpassword, accountId); +} + +void SQLiteDatabase::InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) { + ExecuteInsert("INSERT INTO accounts (name, password, gm_level) VALUES (?, ?, ?);", username, bcryptpassword, static_cast(eGameMasterLevel::OPERATOR)); +} + +void SQLiteDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) { + ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast(gmLevel), accountId); +} + +uint32_t SQLiteDatabase::GetAccountCount() { + auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;"); + if (res.eof()) return 0; + + return res.getIntField("count"); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/AccountsRewardCodes.cpp b/dDatabase/GameDatabase/SQLite/Tables/AccountsRewardCodes.cpp new file mode 100644 index 00000000..0359ee69 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/AccountsRewardCodes.cpp @@ -0,0 +1,17 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::InsertRewardCode(const uint32_t account_id, const uint32_t reward_code) { + ExecuteInsert("INSERT OR IGNORE INTO accounts_rewardcodes (account_id, rewardcode) VALUES (?, ?);", account_id, reward_code); +} + +std::vector SQLiteDatabase::GetRewardCodesByAccountID(const uint32_t account_id) { + auto [_, result] = ExecuteSelect("SELECT rewardcode FROM accounts_rewardcodes WHERE account_id = ?;", account_id); + + std::vector toReturn; + while (!result.eof()) { + toReturn.push_back(result.getIntField("rewardcode")); + result.nextRow(); + } + + return toReturn; +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/ActivityLog.cpp b/dDatabase/GameDatabase/SQLite/Tables/ActivityLog.cpp new file mode 100644 index 00000000..33f81429 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/ActivityLog.cpp @@ -0,0 +1,6 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) { + ExecuteInsert("INSERT INTO activity_log (character_id, activity, time, map_id) VALUES (?, ?, ?, ?);", + characterId, static_cast(activityType), static_cast(time(NULL)), mapId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Behaviors.cpp b/dDatabase/GameDatabase/SQLite/Tables/Behaviors.cpp new file mode 100644 index 00000000..05cadbcd --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Behaviors.cpp @@ -0,0 +1,19 @@ +#include "IBehaviors.h" + +#include "SQLiteDatabase.h" + +void SQLiteDatabase::AddBehavior(const IBehaviors::Info& info) { + ExecuteInsert( + "INSERT INTO behaviors (behavior_info, character_id, behavior_id) VALUES (?, ?, ?) ON CONFLICT(behavior_id) DO UPDATE SET behavior_info = ?", + info.behaviorInfo, info.characterId, info.behaviorId, info.behaviorInfo + ); +} + +void SQLiteDatabase::RemoveBehavior(const int32_t behaviorId) { + ExecuteDelete("DELETE FROM behaviors WHERE behavior_id = ?", behaviorId); +} + +std::string SQLiteDatabase::GetBehavior(const int32_t behaviorId) { + auto [_, result] = ExecuteSelect("SELECT behavior_info FROM behaviors WHERE behavior_id = ?", behaviorId); + return !result.eof() ? result.getStringField("behavior_info") : ""; +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/BugReports.cpp b/dDatabase/GameDatabase/SQLite/Tables/BugReports.cpp new file mode 100644 index 00000000..f4960941 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/BugReports.cpp @@ -0,0 +1,6 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::InsertNewBugReport(const IBugReports::Info& info) { + ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)", + info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt new file mode 100644 index 00000000..91d5b5e2 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt @@ -0,0 +1,26 @@ +set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES + "Accounts.cpp" + "AccountsRewardCodes.cpp" + "ActivityLog.cpp" + "Behaviors.cpp" + "BugReports.cpp" + "CharInfo.cpp" + "CharXml.cpp" + "CommandLog.cpp" + "Friends.cpp" + "IgnoreList.cpp" + "Leaderboard.cpp" + "Mail.cpp" + "MigrationHistory.cpp" + "ObjectIdTracker.cpp" + "PetNames.cpp" + "PlayerCheatDetections.cpp" + "PlayKeys.cpp" + "Property.cpp" + "PropertyContents.cpp" + "Servers.cpp" + "Ugc.cpp" + "UgcModularBuild.cpp" + PARENT_SCOPE +) + diff --git a/dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp b/dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp new file mode 100644 index 00000000..27ae3611 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/CharInfo.cpp @@ -0,0 +1,79 @@ +#include "SQLiteDatabase.h" + +std::vector SQLiteDatabase::GetApprovedCharacterNames() { + auto [_, result] = ExecuteSelect("SELECT name FROM charinfo;"); + + std::vector toReturn; + + while (!result.eof()) { + toReturn.push_back(result.getStringField("name")); + result.nextRow(); + } + + return toReturn; +} + +std::optional CharInfoFromQueryResult(CppSQLite3Query stmt) { + if (stmt.eof()) { + return std::nullopt; + } + + ICharInfo::Info toReturn; + + toReturn.id = stmt.getIntField("id"); + toReturn.name = stmt.getStringField("name"); + toReturn.pendingName = stmt.getStringField("pending_name"); + toReturn.needsRename = stmt.getIntField("needs_rename"); + toReturn.cloneId = stmt.getInt64Field("prop_clone_id"); + toReturn.accountId = stmt.getIntField("account_id"); + toReturn.permissionMap = static_cast(stmt.getIntField("permission_map")); + + return toReturn; +} + +std::optional SQLiteDatabase::GetCharacterInfo(const uint32_t charId) { + return CharInfoFromQueryResult( + ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId).second + ); +} + +std::optional SQLiteDatabase::GetCharacterInfo(const std::string_view name) { + return CharInfoFromQueryResult( + ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE name = ? LIMIT 1;", name).second + ); +} + +std::vector SQLiteDatabase::GetAccountCharacterIds(const uint32_t accountId) { + auto [_, result] = ExecuteSelect("SELECT id FROM charinfo WHERE account_id = ? ORDER BY last_login DESC LIMIT 4;", accountId); + + std::vector toReturn; + while (!result.eof()) { + toReturn.push_back(result.getIntField("id")); + result.nextRow(); + } + + return toReturn; +} + +void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) { + ExecuteInsert( + "INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))", + info.id, + info.accountId, + info.name, + info.pendingName, + false, + static_cast(time(NULL))); +} + +void SQLiteDatabase::SetCharacterName(const uint32_t characterId, const std::string_view name) { + ExecuteUpdate("UPDATE charinfo SET name = ?, pending_name = '', needs_rename = 0, last_login = ? WHERE id = ?;", name, static_cast(time(NULL)), characterId); +} + +void SQLiteDatabase::SetPendingCharacterName(const uint32_t characterId, const std::string_view name) { + ExecuteUpdate("UPDATE charinfo SET pending_name = ?, needs_rename = 0, last_login = ? WHERE id = ?;", name, static_cast(time(NULL)), characterId); +} + +void SQLiteDatabase::UpdateLastLoggedInCharacter(const uint32_t characterId) { + ExecuteUpdate("UPDATE charinfo SET last_login = ? WHERE id = ?;", static_cast(time(NULL)), characterId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/CharXml.cpp b/dDatabase/GameDatabase/SQLite/Tables/CharXml.cpp new file mode 100644 index 00000000..56085101 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/CharXml.cpp @@ -0,0 +1,19 @@ +#include "SQLiteDatabase.h" + +std::string SQLiteDatabase::GetCharacterXml(const uint32_t charId) { + auto [_, result] = ExecuteSelect("SELECT xml_data FROM charxml WHERE id = ? LIMIT 1;", charId); + + if (result.eof()) { + return ""; + } + + return result.getStringField("xml_data"); +} + +void SQLiteDatabase::UpdateCharacterXml(const uint32_t charId, const std::string_view lxfml) { + ExecuteUpdate("UPDATE charxml SET xml_data = ? WHERE id = ?;", lxfml, charId); +} + +void SQLiteDatabase::InsertCharacterXml(const uint32_t characterId, const std::string_view lxfml) { + ExecuteInsert("INSERT INTO `charxml` (`id`, `xml_data`) VALUES (?,?)", characterId, lxfml); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/CommandLog.cpp b/dDatabase/GameDatabase/SQLite/Tables/CommandLog.cpp new file mode 100644 index 00000000..db39046f --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/CommandLog.cpp @@ -0,0 +1,5 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) { + ExecuteInsert("INSERT INTO command_log (character_id, command) VALUES (?, ?);", characterId, command); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Friends.cpp b/dDatabase/GameDatabase/SQLite/Tables/Friends.cpp new file mode 100644 index 00000000..7ac41459 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Friends.cpp @@ -0,0 +1,73 @@ +#include "SQLiteDatabase.h" + +std::vector SQLiteDatabase::GetFriendsList(const uint32_t charId) { + auto [_, friendsList] = ExecuteSelect( + R"QUERY( + SELECT fr.requested_player AS player, best_friend AS bff, ci.name AS name FROM + ( + SELECT CASE + WHEN player_id = ? THEN friend_id + WHEN friend_id = ? THEN player_id + END AS requested_player, best_friend FROM friends + ) AS fr + JOIN charinfo AS ci ON ci.id = fr.requested_player + WHERE fr.requested_player IS NOT NULL AND fr.requested_player != ?; + )QUERY", charId, charId, charId); + + std::vector toReturn; + + while (!friendsList.eof()) { + FriendData fd; + fd.friendID = friendsList.getIntField("player"); + fd.isBestFriend = friendsList.getIntField("bff") == 3; // 0 = friends, 1 = left_requested, 2 = right_requested, 3 = both_accepted - are now bffs + fd.friendName = friendsList.getStringField("name"); + + toReturn.push_back(fd); + friendsList.nextRow(); + } + + return toReturn; +} + +std::optional SQLiteDatabase::GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) { + auto [_, result] = ExecuteSelect("SELECT * FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;", + playerCharacterId, + friendCharacterId, + friendCharacterId, + playerCharacterId + ); + + if (result.eof()) { + return std::nullopt; + } + + IFriends::BestFriendStatus toReturn; + toReturn.playerCharacterId = result.getIntField("player_id"); + toReturn.friendCharacterId = result.getIntField("friend_id"); + toReturn.bestFriendStatus = result.getIntField("best_friend"); + + return toReturn; +} + +void SQLiteDatabase::SetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId, const uint32_t bestFriendStatus) { + ExecuteUpdate("UPDATE friends SET best_friend = ? WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?);", + bestFriendStatus, + playerCharacterId, + friendCharacterId, + friendCharacterId, + playerCharacterId + ); +} + +void SQLiteDatabase::AddFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) { + ExecuteInsert("INSERT OR IGNORE INTO friends (player_id, friend_id, best_friend) VALUES (?, ?, 0);", playerCharacterId, friendCharacterId); +} + +void SQLiteDatabase::RemoveFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) { + ExecuteDelete("DELETE FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?);", + playerCharacterId, + friendCharacterId, + friendCharacterId, + playerCharacterId + ); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/IgnoreList.cpp b/dDatabase/GameDatabase/SQLite/Tables/IgnoreList.cpp new file mode 100644 index 00000000..e7f5a3e0 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/IgnoreList.cpp @@ -0,0 +1,22 @@ +#include "SQLiteDatabase.h" + +std::vector SQLiteDatabase::GetIgnoreList(const uint32_t playerId) { + auto [_, result] = ExecuteSelect("SELECT ci.name AS name, il.ignored_player_id AS ignore_id FROM ignore_list AS il JOIN charinfo AS ci ON il.ignored_player_id = ci.id WHERE il.player_id = ?", playerId); + + std::vector ignoreList; + + while (!result.eof()) { + ignoreList.push_back(IIgnoreList::Info{ result.getStringField("name"), static_cast(result.getIntField("ignore_id")) }); + result.nextRow(); + } + + return ignoreList; +} + +void SQLiteDatabase::AddIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) { + ExecuteInsert("INSERT OR IGNORE INTO ignore_list (player_id, ignored_player_id) VALUES (?, ?)", playerId, ignoredPlayerId); +} + +void SQLiteDatabase::RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) { + ExecuteDelete("DELETE FROM ignore_list WHERE player_id = ? AND ignored_player_id = ?", playerId, ignoredPlayerId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Leaderboard.cpp b/dDatabase/GameDatabase/SQLite/Tables/Leaderboard.cpp new file mode 100644 index 00000000..ee0423dd --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Leaderboard.cpp @@ -0,0 +1,91 @@ +#include "SQLiteDatabase.h" + +#include "Game.h" +#include "Logger.h" +#include "dConfig.h" + +std::optional SQLiteDatabase::GetDonationTotal(const uint32_t activityId) { + auto [_, donation_total] = ExecuteSelect("SELECT SUM(primaryScore) as donation_total FROM leaderboard WHERE game_id = ?;", activityId); + + if (donation_total.eof()) { + return std::nullopt; + } + + return donation_total.getIntField("donation_total"); +} + +std::vector ProcessQuery(CppSQLite3Query& rows) { + std::vector entries; + + while (!rows.eof()) { + auto& entry = entries.emplace_back(); + + entry.charId = rows.getIntField("character_id"); + entry.lastPlayedTimestamp = rows.getIntField("lp_unix"); + entry.primaryScore = rows.getFloatField("primaryScore"); + entry.secondaryScore = rows.getFloatField("secondaryScore"); + entry.tertiaryScore = rows.getFloatField("tertiaryScore"); + entry.numWins = rows.getIntField("numWins"); + entry.numTimesPlayed = rows.getIntField("timesPlayed"); + entry.name = rows.getStringField("char_name"); + // entry.ranking is never set because its calculated in leaderboard in code. + rows.nextRow(); + } + + return entries; +} + +std::vector SQLiteDatabase::GetDescendingLeaderboard(const uint32_t activityId) { + auto [_, result] = ExecuteSelect("SELECT *, CAST(strftime('%s', last_played) as INT) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore DESC, tertiaryScore DESC, last_played ASC;", activityId); + return ProcessQuery(result); +} + +std::vector SQLiteDatabase::GetAscendingLeaderboard(const uint32_t activityId) { + auto [_, result] = ExecuteSelect("SELECT *, CAST(strftime('%s', last_played) as INT) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore ASC, secondaryscore ASC, tertiaryScore ASC, last_played ASC;", activityId); + return ProcessQuery(result); +} + +std::vector SQLiteDatabase::GetAgsLeaderboard(const uint32_t activityId) { + auto query = Game::config->GetValue("classic_survival_scoring") != "1" ? + "SELECT *, CAST(strftime('%s', last_played) as INT) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore DESC, tertiaryScore DESC, last_played ASC;" : + "SELECT *, CAST(strftime('%s', last_played) as INT) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY secondaryscore DESC, primaryscore DESC, tertiaryScore DESC, last_played ASC;"; + auto [_, result] = ExecuteSelect(query, activityId); + return ProcessQuery(result); +} + +std::vector SQLiteDatabase::GetNsLeaderboard(const uint32_t activityId) { + auto [_, result] = ExecuteSelect("SELECT *, CAST(strftime('%s', last_played) as INT) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore ASC, tertiaryScore DESC, last_played ASC;", activityId); + return ProcessQuery(result); +} + +void SQLiteDatabase::SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) { + ExecuteInsert("INSERT INTO leaderboard (primaryScore, secondaryScore, tertiaryScore, character_id, game_id, last_played) VALUES (?,?,?,?,?,CURRENT_TIMESTAMP) ;", + score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId); +} + +void SQLiteDatabase::UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) { + ExecuteInsert("UPDATE leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, timesPlayed = timesPlayed + 1, last_played = CURRENT_TIMESTAMP WHERE character_id = ? AND game_id = ?;", + score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId); +} + +std::optional SQLiteDatabase::GetPlayerScore(const uint32_t playerId, const uint32_t gameId) { + std::optional toReturn = std::nullopt; + auto [_, res] = ExecuteSelect("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;", playerId, gameId); + if (!res.eof()) { + toReturn = ILeaderboard::Score{ + .primaryScore = static_cast(res.getFloatField("primaryScore")), + .secondaryScore = static_cast(res.getFloatField("secondaryScore")), + .tertiaryScore = static_cast(res.getFloatField("tertiaryScore")) + }; + } + + return toReturn; +} + +void SQLiteDatabase::IncrementNumWins(const uint32_t playerId, const uint32_t gameId) { + ExecuteUpdate("UPDATE leaderboard SET numWins = numWins + 1, last_played = CURRENT_TIMESTAMP WHERE character_id = ? AND game_id = ?;", playerId, gameId); +} + +void SQLiteDatabase::IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) { + ExecuteUpdate("UPDATE leaderboard SET timesPlayed = timesPlayed + 1, last_played = CURRENT_TIMESTAMP WHERE character_id = ? AND game_id = ?;", playerId, gameId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp b/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp new file mode 100644 index 00000000..48c1e320 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Mail.cpp @@ -0,0 +1,83 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::InsertNewMail(const IMail::MailInfo& mail) { + ExecuteInsert( + "INSERT INTO `mail` " + "(`sender_id`, `sender_name`, `receiver_id`, `receiver_name`, `time_sent`, `subject`, `body`, `attachment_id`, `attachment_lot`, `attachment_subkey`, `attachment_count`, `was_read`)" + " VALUES (?,?,?,?,?,?,?,?,?,?,?,0)", + mail.senderId, + mail.senderUsername, + mail.receiverId, + mail.recipient, + static_cast(time(NULL)), + mail.subject, + mail.body, + mail.itemID, + mail.itemLOT, + 0, + mail.itemCount); +} + +std::vector SQLiteDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) { + auto [_, res] = ExecuteSelect( + "SELECT id, subject, body, sender_name, attachment_id, attachment_lot, attachment_subkey, attachment_count, was_read, time_sent" + " FROM mail WHERE receiver_id=? limit ?;", + characterId, numberOfMail); + + std::vector toReturn; + + while (!res.eof()) { + IMail::MailInfo mail; + mail.id = res.getInt64Field("id"); + mail.subject = res.getStringField("subject"); + mail.body = res.getStringField("body"); + mail.senderUsername = res.getStringField("sender_name"); + mail.itemID = res.getIntField("attachment_id"); + mail.itemLOT = res.getIntField("attachment_lot"); + mail.itemSubkey = res.getIntField("attachment_subkey"); + mail.itemCount = res.getIntField("attachment_count"); + mail.timeSent = res.getInt64Field("time_sent"); + mail.wasRead = res.getIntField("was_read"); + + toReturn.push_back(std::move(mail)); + res.nextRow(); + } + + return toReturn; +} + +std::optional SQLiteDatabase::GetMail(const uint64_t mailId) { + auto [_, res] = ExecuteSelect("SELECT attachment_lot, attachment_count FROM mail WHERE id=? LIMIT 1;", mailId); + + if (res.eof()) { + return std::nullopt; + } + + IMail::MailInfo toReturn; + toReturn.itemLOT = res.getIntField("attachment_lot"); + toReturn.itemCount = res.getIntField("attachment_count"); + + return toReturn; +} + +uint32_t SQLiteDatabase::GetUnreadMailCount(const uint32_t characterId) { + auto [_, res] = ExecuteSelect("SELECT COUNT(*) AS number_unread FROM mail WHERE receiver_id=? AND was_read=0;", characterId); + + if (res.eof()) { + return 0; + } + + return res.getIntField("number_unread"); +} + +void SQLiteDatabase::MarkMailRead(const uint64_t mailId) { + ExecuteUpdate("UPDATE mail SET was_read=1 WHERE id=?;", mailId); +} + +void SQLiteDatabase::ClaimMailItem(const uint64_t mailId) { + ExecuteUpdate("UPDATE mail SET attachment_lot=0 WHERE id=?;", mailId); +} + +void SQLiteDatabase::DeleteMail(const uint64_t mailId) { + ExecuteDelete("DELETE FROM mail WHERE id=?;", mailId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/MigrationHistory.cpp b/dDatabase/GameDatabase/SQLite/Tables/MigrationHistory.cpp new file mode 100644 index 00000000..dbb1c268 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/MigrationHistory.cpp @@ -0,0 +1,13 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::CreateMigrationHistoryTable() { + ExecuteInsert("CREATE TABLE IF NOT EXISTS migration_history (name TEXT NOT NULL, date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);"); +} + +bool SQLiteDatabase::IsMigrationRun(const std::string_view str) { + return !ExecuteSelect("SELECT name FROM migration_history WHERE name = ?;", str).second.eof(); +} + +void SQLiteDatabase::InsertMigration(const std::string_view str) { + ExecuteInsert("INSERT INTO migration_history (name) VALUES (?);", str); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp b/dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp new file mode 100644 index 00000000..af8014dd --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/ObjectIdTracker.cpp @@ -0,0 +1,17 @@ +#include "SQLiteDatabase.h" + +std::optional SQLiteDatabase::GetCurrentPersistentId() { + auto [_, result] = ExecuteSelect("SELECT last_object_id FROM object_id_tracker"); + if (result.eof()) { + return std::nullopt; + } + return result.getIntField("last_object_id"); +} + +void SQLiteDatabase::InsertDefaultPersistentId() { + ExecuteInsert("INSERT INTO object_id_tracker VALUES (1);"); +} + +void SQLiteDatabase::UpdatePersistentId(const uint32_t newId) { + ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = ?;", newId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp b/dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp new file mode 100644 index 00000000..2216e1d0 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp @@ -0,0 +1,26 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) { + ExecuteInsert( + "INSERT INTO `pet_names` (`id`, `pet_name`, `approved`) VALUES (?, ?, ?) " + "ON CONFLICT(id) DO UPDATE SET pet_name = ?, approved = ?;", + petId, + info.petName, + info.approvalStatus, + info.petName, + info.approvalStatus); +} + +std::optional SQLiteDatabase::GetPetNameInfo(const LWOOBJID& petId) { + auto [_, result] = ExecuteSelect("SELECT pet_name, approved FROM pet_names WHERE id = ? LIMIT 1;", petId); + + if (result.eof()) { + return std::nullopt; + } + + IPetNames::Info toReturn; + toReturn.petName = result.getStringField("pet_name"); + toReturn.approvalStatus = result.getIntField("approved"); + + return toReturn; +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/PlayKeys.cpp b/dDatabase/GameDatabase/SQLite/Tables/PlayKeys.cpp new file mode 100644 index 00000000..1900de97 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/PlayKeys.cpp @@ -0,0 +1,11 @@ +#include "SQLiteDatabase.h" + +std::optional SQLiteDatabase::IsPlaykeyActive(const int32_t playkeyId) { + auto [_, keyCheckRes] = ExecuteSelect("SELECT active FROM `play_keys` WHERE id=?", playkeyId); + + if (keyCheckRes.eof()) { + return std::nullopt; + } + + return keyCheckRes.getIntField("active"); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/PlayerCheatDetections.cpp b/dDatabase/GameDatabase/SQLite/Tables/PlayerCheatDetections.cpp new file mode 100644 index 00000000..a47ae340 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/PlayerCheatDetections.cpp @@ -0,0 +1,7 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::InsertCheatDetection(const IPlayerCheatDetections::Info& info) { + ExecuteInsert( + "INSERT INTO player_cheat_detections (account_id, name, violation_msg, violation_system_address) VALUES (?, ?, ?, ?)", + info.userId, info.username, info.extraMessage, info.systemAddress); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Property.cpp b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp new file mode 100644 index 00000000..7374e941 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Property.cpp @@ -0,0 +1,195 @@ +#include "SQLiteDatabase.h" +#include "ePropertySortType.h" + +std::optional SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) { + std::optional result; + std::string query; + std::pair propertiesRes; + + if (params.sortChoice == SORT_TYPE_FEATURED || params.sortChoice == SORT_TYPE_FRIENDS) { + query = R"QUERY( + FROM properties as p + JOIN charinfo as ci + ON ci.prop_clone_id = p.clone_id + where p.zone_id = ? + AND ( + p.description LIKE ? + OR p.name LIKE ? + OR ci.name LIKE ? + ) + AND p.privacy_option >= ? + AND p.owner_id IN ( + SELECT fr.requested_player AS player FROM ( + SELECT CASE + WHEN player_id = ? THEN friend_id + WHEN friend_id = ? THEN player_id + END AS requested_player FROM friends + ) AS fr + JOIN charinfo AS ci ON ci.id = fr.requested_player + WHERE fr.requested_player IS NOT NULL AND fr.requested_player != ? + ) ORDER BY ci.name ASC + )QUERY"; + const auto completeQuery = "SELECT p.* " + query + " LIMIT ? OFFSET ?;"; + propertiesRes = ExecuteSelect( + completeQuery, + params.mapId, + "%" + params.searchString + "%", + "%" + params.searchString + "%", + "%" + params.searchString + "%", + params.playerSort, + params.playerId, + params.playerId, + params.playerId, + params.numResults, + params.startIndex + ); + const auto countQuery = "SELECT COUNT(*) as count" + query + ";"; + auto [_, count] = ExecuteSelect( + countQuery, + params.mapId, + "%" + params.searchString + "%", + "%" + params.searchString + "%", + "%" + params.searchString + "%", + params.playerSort, + params.playerId, + params.playerId, + params.playerId + ); + if (!count.eof()) { + result = IProperty::PropertyEntranceResult(); + result->totalEntriesMatchingQuery = count.getIntField("count"); + } + } else { + if (params.sortChoice == SORT_TYPE_REPUTATION) { + query = R"QUERY( + FROM properties as p + JOIN charinfo as ci + ON ci.prop_clone_id = p.clone_id + where p.zone_id = ? + AND ( + p.description LIKE ? + OR p.name LIKE ? + OR ci.name LIKE ? + ) + AND p.privacy_option >= ? + ORDER BY p.reputation DESC, p.last_updated DESC + )QUERY"; + } else { + query = R"QUERY( + FROM properties as p + JOIN charinfo as ci + ON ci.prop_clone_id = p.clone_id + where p.zone_id = ? + AND ( + p.description LIKE ? + OR p.name LIKE ? + OR ci.name LIKE ? + ) + AND p.privacy_option >= ? + ORDER BY p.last_updated DESC + )QUERY"; + } + const auto completeQuery = "SELECT p.* " + query + " LIMIT ? OFFSET ?;"; + propertiesRes = ExecuteSelect( + completeQuery, + params.mapId, + "%" + params.searchString + "%", + "%" + params.searchString + "%", + "%" + params.searchString + "%", + params.playerSort, + params.numResults, + params.startIndex + ); + const auto countQuery = "SELECT COUNT(*) as count" + query + ";"; + auto [_, count] = ExecuteSelect( + countQuery, + params.mapId, + "%" + params.searchString + "%", + "%" + params.searchString + "%", + "%" + params.searchString + "%", + params.playerSort + ); + if (!count.eof()) { + result = IProperty::PropertyEntranceResult(); + result->totalEntriesMatchingQuery = count.getIntField("count"); + } + } + + auto& [_, properties] = propertiesRes; + if (!properties.eof() && !result.has_value()) result = IProperty::PropertyEntranceResult(); + while (!properties.eof()) { + auto& entry = result->entries.emplace_back(); + entry.id = properties.getInt64Field("id"); + entry.ownerId = properties.getInt64Field("owner_id"); + entry.cloneId = properties.getInt64Field("clone_id"); + entry.name = properties.getStringField("name"); + entry.description = properties.getStringField("description"); + entry.privacyOption = properties.getIntField("privacy_option"); + entry.rejectionReason = properties.getStringField("rejection_reason"); + entry.lastUpdatedTime = properties.getIntField("last_updated"); + entry.claimedTime = properties.getIntField("time_claimed"); + entry.reputation = properties.getIntField("reputation"); + entry.modApproved = properties.getIntField("mod_approved"); + entry.performanceCost = properties.getFloatField("performance_cost"); + properties.nextRow(); + } + + return result; +} + +std::optional SQLiteDatabase::GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) { + auto [_, propertyEntry] = ExecuteSelect( + "SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost " + "FROM properties WHERE zone_id = ? AND clone_id = ?;", mapId, cloneId); + + if (propertyEntry.eof()) { + return std::nullopt; + } + + IProperty::Info toReturn; + toReturn.id = propertyEntry.getInt64Field("id"); + toReturn.ownerId = propertyEntry.getInt64Field("owner_id"); + toReturn.cloneId = propertyEntry.getInt64Field("clone_id"); + toReturn.name = propertyEntry.getStringField("name"); + toReturn.description = propertyEntry.getStringField("description"); + toReturn.privacyOption = propertyEntry.getIntField("privacy_option"); + toReturn.rejectionReason = propertyEntry.getStringField("rejection_reason"); + toReturn.lastUpdatedTime = propertyEntry.getIntField("last_updated"); + toReturn.claimedTime = propertyEntry.getIntField("time_claimed"); + toReturn.reputation = propertyEntry.getIntField("reputation"); + toReturn.modApproved = propertyEntry.getIntField("mod_approved"); + toReturn.performanceCost = propertyEntry.getFloatField("performance_cost"); + + return toReturn; +} + +void SQLiteDatabase::UpdatePropertyModerationInfo(const IProperty::Info& info) { + ExecuteUpdate("UPDATE properties SET privacy_option = ?, rejection_reason = ?, mod_approved = ? WHERE id = ?;", + info.privacyOption, + info.rejectionReason, + info.modApproved, + info.id); +} + +void SQLiteDatabase::UpdatePropertyDetails(const IProperty::Info& info) { + ExecuteUpdate("UPDATE properties SET name = ?, description = ? WHERE id = ?;", info.name, info.description, info.id); +} + +void SQLiteDatabase::UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) { + ExecuteUpdate("UPDATE properties SET performance_cost = ? WHERE zone_id = ? AND clone_id = ?;", performanceCost, zoneId.GetMapID(), zoneId.GetCloneID()); +} + +void SQLiteDatabase::InsertNewProperty(const IProperty::Info& info, const uint32_t templateId, const LWOZONEID& zoneId) { + auto insertion = ExecuteInsert( + "INSERT INTO properties" + " (id, owner_id, template_id, clone_id, name, description, zone_id, rent_amount, rent_due, privacy_option, last_updated, time_claimed, rejection_reason, reputation, performance_cost)" + " VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, 0, CAST(strftime('%s', 'now') as INT), CAST(strftime('%s', 'now') as INT), '', 0, 0.0)", + info.id, + info.ownerId, + templateId, + zoneId.GetCloneID(), + info.name, + info.description, + zoneId.GetMapID() + ); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp new file mode 100644 index 00000000..6a8d7028 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/PropertyContents.cpp @@ -0,0 +1,65 @@ +#include "SQLiteDatabase.h" + +std::vector SQLiteDatabase::GetPropertyModels(const LWOOBJID& propertyId) { + auto [_, result] = ExecuteSelect( + "SELECT id, lot, x, y, z, rx, ry, rz, rw, ugc_id, " + "behavior_1, behavior_2, behavior_3, behavior_4, behavior_5 " + "FROM properties_contents WHERE property_id = ?;", propertyId); + + std::vector toReturn; + while (!result.eof()) { + IPropertyContents::Model model; + model.id = result.getInt64Field("id"); + model.lot = static_cast(result.getIntField("lot")); + model.position.x = result.getFloatField("x"); + model.position.y = result.getFloatField("y"); + model.position.z = result.getFloatField("z"); + model.rotation.w = result.getFloatField("rw"); + model.rotation.x = result.getFloatField("rx"); + model.rotation.y = result.getFloatField("ry"); + model.rotation.z = result.getFloatField("rz"); + model.ugcId = result.getInt64Field("ugc_id"); + model.behaviors[0] = result.getIntField("behavior_1"); + model.behaviors[1] = result.getIntField("behavior_2"); + model.behaviors[2] = result.getIntField("behavior_3"); + model.behaviors[3] = result.getIntField("behavior_4"); + model.behaviors[4] = result.getIntField("behavior_5"); + + toReturn.push_back(std::move(model)); + result.nextRow(); + } + return toReturn; +} + +void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPropertyContents::Model& model, const std::string_view name) { + try { + ExecuteInsert( + "INSERT INTO properties_contents" + "(id, property_id, ugc_id, lot, x, y, z, rx, ry, rz, rw, model_name, model_description, behavior_1, behavior_2, behavior_3, behavior_4, behavior_5)" + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 18 + model.id, propertyId, model.ugcId == 0 ? std::nullopt : std::optional(model.ugcId), static_cast(model.lot), + model.position.x, model.position.y, model.position.z, model.rotation.x, model.rotation.y, model.rotation.z, model.rotation.w, + name, "", // Model description. TODO implement this. + model.behaviors[0], // behavior 1 + model.behaviors[1], // behavior 2 + model.behaviors[2], // behavior 3 + model.behaviors[3], // behavior 4 + model.behaviors[4] // behavior 5 + ); + } catch (std::exception& e) { + LOG("Error inserting new property model: %s", e.what()); + } +} + +void SQLiteDatabase::UpdateModel(const LWOOBJID& propertyId, const NiPoint3& position, const NiQuaternion& rotation, const std::array, 5>& behaviors) { + ExecuteUpdate( + "UPDATE properties_contents SET x = ?, y = ?, z = ?, rx = ?, ry = ?, rz = ?, rw = ?, " + "behavior_1 = ?, behavior_2 = ?, behavior_3 = ?, behavior_4 = ?, behavior_5 = ? WHERE id = ?;", + position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, rotation.w, + behaviors[0].first, behaviors[1].first, behaviors[2].first, behaviors[3].first, behaviors[4].first, propertyId); +} + +void SQLiteDatabase::RemoveModel(const LWOOBJID& modelId) { + ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Servers.cpp b/dDatabase/GameDatabase/SQLite/Tables/Servers.cpp new file mode 100644 index 00000000..8c136a30 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Servers.cpp @@ -0,0 +1,23 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::SetMasterIp(const std::string_view ip, const uint32_t port) { + // We only want our 1 entry anyways, so we can just delete all and reinsert the one we want + // since it would be two queries anyways. + ExecuteDelete("DELETE FROM servers;"); + ExecuteInsert("INSERT INTO `servers` (`name`, `ip`, `port`, `state`, `version`) VALUES ('master', ?, ?, 0, 171022)", ip, port); +} + +std::optional SQLiteDatabase::GetMasterInfo() { + auto [_, result] = ExecuteSelect("SELECT ip, port FROM servers WHERE name='master' LIMIT 1;"); + + if (result.eof()) { + return std::nullopt; + } + + MasterInfo toReturn; + + toReturn.ip = result.getStringField("ip"); + toReturn.port = result.getIntField("port"); + + return toReturn; +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp new file mode 100644 index 00000000..048b53ab --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/Ugc.cpp @@ -0,0 +1,72 @@ +#include "SQLiteDatabase.h" + +std::vector SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId) { + auto [_, result] = ExecuteSelect( + "SELECT lxfml, u.id FROM ugc AS u JOIN properties_contents AS pc ON u.id = pc.ugc_id WHERE lot = 14 AND property_id = ? AND pc.ugc_id IS NOT NULL;", + propertyId); + + std::vector toReturn; + + while (!result.eof()) { + IUgc::Model model; + + int blobSize{}; + const auto* blob = result.getBlobField("lxfml", blobSize); + model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); + model.id = result.getInt64Field("id"); + toReturn.push_back(std::move(model)); + result.nextRow(); + } + + return toReturn; +} + +std::vector SQLiteDatabase::GetAllUgcModels() { + auto [_, result] = ExecuteSelect("SELECT id, lxfml FROM ugc;"); + + std::vector models; + while (!result.eof()) { + IUgc::Model model; + model.id = result.getInt64Field("id"); + + int blobSize{}; + const auto* blob = result.getBlobField("lxfml", blobSize); + model.lxfmlData << std::string(reinterpret_cast(blob), blobSize); + models.push_back(std::move(model)); + result.nextRow(); + } + + return models; +} + +void SQLiteDatabase::RemoveUnreferencedUgcModels() { + ExecuteDelete("DELETE FROM ugc WHERE id NOT IN (SELECT ugc_id FROM properties_contents WHERE ugc_id IS NOT NULL);"); +} + +void SQLiteDatabase::InsertNewUgcModel( + std::istringstream& sd0Data, // cant be const sad + const uint32_t blueprintId, + const uint32_t accountId, + const uint32_t characterId) { + const std::istream stream(sd0Data.rdbuf()); + ExecuteInsert( + "INSERT INTO `ugc`(`id`, `account_id`, `character_id`, `is_optimized`, `lxfml`, `bake_ao`, `filename`) VALUES (?,?,?,?,?,?,?)", + blueprintId, + accountId, + characterId, + 0, + &stream, + false, + "weedeater.lxfml" + ); +} + +void SQLiteDatabase::DeleteUgcModelData(const LWOOBJID& modelId) { + ExecuteDelete("DELETE FROM ugc WHERE id = ?;", modelId); + ExecuteDelete("DELETE FROM properties_contents WHERE ugc_id = ?;", modelId); +} + +void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::istringstream& lxfml) { + const std::istream stream(lxfml.rdbuf()); + ExecuteUpdate("UPDATE ugc SET lxfml = ? WHERE id = ?;", &stream, modelId); +} diff --git a/dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp b/dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp new file mode 100644 index 00000000..4e806384 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/UgcModularBuild.cpp @@ -0,0 +1,9 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) { + ExecuteInsert("INSERT INTO ugc_modular_build (ugc_id, ldf_config, character_id) VALUES (?,?,?)", bigId, modules, characterId); +} + +void SQLiteDatabase::DeleteUgcBuild(const LWOOBJID bigId) { + ExecuteDelete("DELETE FROM ugc_modular_build WHERE ugc_id = ?;", bigId); +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp index e44cd1f7..0263a6e3 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.cpp @@ -8,10 +8,6 @@ void TestSQLDatabase::Destroy(std::string source) { } -sql::PreparedStatement* TestSQLDatabase::CreatePreppedStmt(const std::string& query) { - return nullptr; -} - void TestSQLDatabase::Commit() { } diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 49e954ae..9d4b184f 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -7,7 +7,6 @@ class TestSQLDatabase : public GameDatabase { void Connect() override; void Destroy(std::string source = "") override; - sql::PreparedStatement* CreatePreppedStmt(const std::string& query) override; void Commit() override; bool GetAutoCommit() override; void SetAutoCommit(bool value) override; @@ -102,6 +101,7 @@ class TestSQLDatabase : public GameDatabase { void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override {}; void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) override {}; void DeleteUgcBuild(const LWOOBJID bigId) override {}; + uint32_t GetAccountCount() override { return 0; }; }; #endif //!TESTSQLDATABASE_H diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp index 44ccaa99..e6dfb042 100644 --- a/dDatabase/MigrationRunner.cpp +++ b/dDatabase/MigrationRunner.cpp @@ -10,9 +10,9 @@ #include -Migration LoadMigration(std::string path) { +Migration LoadMigration(std::string folder, std::string path) { Migration migration{}; - std::ifstream file(BinaryPathFinder::GetBinaryDir() / "migrations/" / path); + std::ifstream file(BinaryPathFinder::GetBinaryDir() / "migrations/" / folder / path); if (file.is_open()) { std::string line; @@ -34,10 +34,19 @@ Migration LoadMigration(std::string path) { void MigrationRunner::RunMigrations() { Database::Get()->CreateMigrationHistoryTable(); + // has to be here because when moving the files to the new folder, the migration_history table is not updated so it will run them all again. + + const auto migrationFolder = Database::GetMigrationFolder(); + if (!Database::Get()->IsMigrationRun("17_migration_for_migrations.sql") && migrationFolder == "mysql") { + LOG("Running migration: 17_migration_for_migrations.sql"); + Database::Get()->ExecuteCustomQuery("UPDATE `migration_history` SET `name` = SUBSTR(`name`, 5) WHERE `name` LIKE \"dlu%\";"); + Database::Get()->InsertMigration("17_migration_for_migrations.sql"); + } + std::string finalSQL = ""; bool runSd0Migrations = false; - for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/").string())) { - auto migration = LoadMigration("dlu/" + entry); + for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) { + auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry); if (migration.data.empty()) { continue; @@ -46,7 +55,7 @@ void MigrationRunner::RunMigrations() { if (Database::Get()->IsMigrationRun(migration.name)) continue; LOG("Running migration: %s", migration.name.c_str()); - if (migration.name == "dlu/5_brick_model_sd0.sql") { + if (migration.name == "5_brick_model_sd0.sql") { runSd0Migrations = true; } else { finalSQL.append(migration.data.c_str()); @@ -86,10 +95,14 @@ void MigrationRunner::RunSQLiteMigrations() { cdstmt.execQuery().finalize(); cdstmt.finalize(); - Database::Get()->CreateMigrationHistoryTable(); + if (CDClientDatabase::ExecuteQuery("select * from migration_history where name = \"7_migration_for_migrations.sql\";").eof()) { + LOG("Running migration: 7_migration_for_migrations.sql"); + CDClientDatabase::ExecuteQuery("UPDATE `migration_history` SET `name` = SUBSTR(`name`, 10) WHERE `name` LIKE \"cdserver%\";"); + CDClientDatabase::ExecuteQuery("INSERT INTO migration_history (name) VALUES (\"7_migration_for_migrations.sql\");"); + } for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "migrations/cdserver/").string())) { - auto migration = LoadMigration("cdserver/" + entry); + auto migration = LoadMigration("cdserver/", entry); if (migration.data.empty()) continue; diff --git a/dGame/CMakeLists.txt b/dGame/CMakeLists.txt index 26eb859a..661c3688 100644 --- a/dGame/CMakeLists.txt +++ b/dGame/CMakeLists.txt @@ -26,7 +26,6 @@ target_include_directories(dGameBase PUBLIC "." "dEntity" "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" - "${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include" # dPhysics "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Recast/Include" "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Detour/Include" diff --git a/dGame/dComponents/CMakeLists.txt b/dGame/dComponents/CMakeLists.txt index c60e135f..c6e72f29 100644 --- a/dGame/dComponents/CMakeLists.txt +++ b/dGame/dComponents/CMakeLists.txt @@ -65,7 +65,6 @@ target_include_directories(dComponents PUBLIC "." "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" - "${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include" # dPhysics (via dpWorld.h) "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Recast/Include" "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Detour/Include" diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 9a54a64e..e01ca255 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -176,12 +176,16 @@ int main(int argc, char** argv) { } // Run migrations should any need to be run. - MigrationRunner::RunSQLiteMigrations(); + MigrationRunner::RunSQLiteMigrations(); //If the first command line argument is -a or --account then make the user //input a username and password, with the password being hidden. - if (argc > 1 && - (strcmp(argv[1], "-a") == 0 || strcmp(argv[1], "--account") == 0)) { + bool createAccount = Database::Get()->GetAccountCount() == 0 && Game::config->GetValue("skip_account_creation") != "1"; + if (createAccount) { + LOG("No accounts exist in the database. Please create an account."); + } + if ((argc > 1 && + (strcmp(argv[1], "-a") == 0 || strcmp(argv[1], "--account") == 0)) || createAccount) { std::string username; std::string password; diff --git a/dMasterServer/Start.cpp b/dMasterServer/Start.cpp index 1fb9c212..d35392f1 100644 --- a/dMasterServer/Start.cpp +++ b/dMasterServer/Start.cpp @@ -13,7 +13,7 @@ void StartChatServer() { //macOS doesn't need sudo to run on ports < 1024 auto result = system(((BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str()); #elif _WIN32 - auto result = system(("start " + (BinaryPathFinder::GetBinaryDir() / "ChatServer.exe").string()).c_str()); + auto result = system(("start /B " + (BinaryPathFinder::GetBinaryDir() / "ChatServer.exe").string()).c_str()); #else if (std::atoi(Game::config->GetValue("use_sudo_chat").c_str())) { auto result = system(("sudo " + (BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str()); @@ -31,7 +31,7 @@ void StartAuthServer() { #ifdef __APPLE__ auto result = system(((BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").c_str()); #elif _WIN32 - auto result = system(("start " + (BinaryPathFinder::GetBinaryDir() / "AuthServer.exe").string()).c_str()); + auto result = system(("start /B " + (BinaryPathFinder::GetBinaryDir() / "AuthServer.exe").string()).c_str()); #else if (std::atoi(Game::config->GetValue("use_sudo_auth").c_str())) { auto result = system(("sudo " + (BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").c_str()); @@ -43,7 +43,7 @@ void StartAuthServer() { void StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID) { #ifdef _WIN32 - std::string cmd = "start " + (BinaryPathFinder::GetBinaryDir() / "WorldServer.exe").string() + " -zone "; + std::string cmd = "start /B " + (BinaryPathFinder::GetBinaryDir() / "WorldServer.exe").string() + " -zone "; #else std::string cmd; if (std::atoi(Game::config->GetValue("use_sudo_world").c_str())) { diff --git a/dNet/AuthPackets.cpp b/dNet/AuthPackets.cpp index 380eb8f0..eb1d1c8f 100644 --- a/dNet/AuthPackets.cpp +++ b/dNet/AuthPackets.cpp @@ -82,14 +82,14 @@ void AuthPackets::SendHandshake(dServer* server, const SystemAddress& sysAddr, c if (serverType == ServerType::Auth) bitStream.Write(ServiceId::Auth); else if (serverType == ServerType::World) bitStream.Write(ServiceId::World); else bitStream.Write(ServiceId::General); - bitStream.Write(215523470896); + bitStream.Write(219818241584); server->Send(bitStream, sysAddr, false); } std::string CleanReceivedString(const std::string& str) { std::string toReturn = str; - const auto removed = std::ranges::find_if(toReturn, [](char c) { return isprint(c) == 0 && isblank(c) == 0; }); + const auto removed = std::ranges::find_if(toReturn, [](unsigned char c) { return isprint(c) == 0 && isblank(c) == 0; }); toReturn.erase(removed, toReturn.end()); return toReturn; } diff --git a/dNet/CMakeLists.txt b/dNet/CMakeLists.txt index 15cdda42..172aee20 100644 --- a/dNet/CMakeLists.txt +++ b/dNet/CMakeLists.txt @@ -19,7 +19,6 @@ target_include_directories(dNet PRIVATE "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" - "${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include" "${PROJECT_SOURCE_DIR}/dGame" # UserManager.h "${PROJECT_SOURCE_DIR}/dGame/dComponents" diff --git a/docker-compose.yml b/docker-compose.yml index 8f5a3d09..dbd16603 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,9 @@ services: - CLIENT_LOCATION=/app/luclient - DLU_CONFIG_DIR=/app/configs - DUMP_FOLDER=/app/dump + - DATABASE_TYPE=mariadb + - SQLITE_DATABASE_PATH=${SQLITE_DATABASE_PATH:-resServer/dlu.sqlite} + - SKIP_ACCOUNT_CREATION=${SKIP_ACCOUNT_CREATION:-1} - MYSQL_HOST=darkflamedb - MYSQL_DATABASE=${MARIADB_DATABASE:-darkflame} - MYSQL_USERNAME=${MARIADB_USER:-darkflame} diff --git a/migrations/cdserver/7_migration_for_migrations.sql b/migrations/cdserver/7_migration_for_migrations.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/dlu/0_initial.sql b/migrations/dlu/mysql/0_initial.sql similarity index 100% rename from migrations/dlu/0_initial.sql rename to migrations/dlu/mysql/0_initial.sql diff --git a/migrations/dlu/10_Security_updates.sql b/migrations/dlu/mysql/10_Security_updates.sql similarity index 100% rename from migrations/dlu/10_Security_updates.sql rename to migrations/dlu/mysql/10_Security_updates.sql diff --git a/migrations/dlu/11_fix_cheat_detection_table.sql b/migrations/dlu/mysql/11_fix_cheat_detection_table.sql similarity index 100% rename from migrations/dlu/11_fix_cheat_detection_table.sql rename to migrations/dlu/mysql/11_fix_cheat_detection_table.sql diff --git a/migrations/dlu/12_modular_build_ugc.sql b/migrations/dlu/mysql/12_modular_build_ugc.sql similarity index 100% rename from migrations/dlu/12_modular_build_ugc.sql rename to migrations/dlu/mysql/12_modular_build_ugc.sql diff --git a/migrations/dlu/13_ignore_list.sql b/migrations/dlu/mysql/13_ignore_list.sql similarity index 100% rename from migrations/dlu/13_ignore_list.sql rename to migrations/dlu/mysql/13_ignore_list.sql diff --git a/migrations/dlu/14_reward_codes.sql b/migrations/dlu/mysql/14_reward_codes.sql similarity index 100% rename from migrations/dlu/14_reward_codes.sql rename to migrations/dlu/mysql/14_reward_codes.sql diff --git a/migrations/dlu/15_behavior_owner.sql b/migrations/dlu/mysql/15_behavior_owner.sql similarity index 100% rename from migrations/dlu/15_behavior_owner.sql rename to migrations/dlu/mysql/15_behavior_owner.sql diff --git a/migrations/dlu/16_big_behaviors.sql b/migrations/dlu/mysql/16_big_behaviors.sql similarity index 100% rename from migrations/dlu/16_big_behaviors.sql rename to migrations/dlu/mysql/16_big_behaviors.sql diff --git a/migrations/dlu/mysql/17_migration_for_migrations.sql b/migrations/dlu/mysql/17_migration_for_migrations.sql new file mode 100644 index 00000000..59d3484c --- /dev/null +++ b/migrations/dlu/mysql/17_migration_for_migrations.sql @@ -0,0 +1 @@ +-- see MigrationRunner.cpp for what this does diff --git a/migrations/dlu/1_unique_charinfo_names.sql b/migrations/dlu/mysql/1_unique_charinfo_names.sql similarity index 100% rename from migrations/dlu/1_unique_charinfo_names.sql rename to migrations/dlu/mysql/1_unique_charinfo_names.sql diff --git a/migrations/dlu/2_reporter_id.sql b/migrations/dlu/mysql/2_reporter_id.sql similarity index 100% rename from migrations/dlu/2_reporter_id.sql rename to migrations/dlu/mysql/2_reporter_id.sql diff --git a/migrations/dlu/3_add_performance_cost.sql b/migrations/dlu/mysql/3_add_performance_cost.sql similarity index 100% rename from migrations/dlu/3_add_performance_cost.sql rename to migrations/dlu/mysql/3_add_performance_cost.sql diff --git a/migrations/dlu/4_friends_list_objectids.sql b/migrations/dlu/mysql/4_friends_list_objectids.sql similarity index 100% rename from migrations/dlu/4_friends_list_objectids.sql rename to migrations/dlu/mysql/4_friends_list_objectids.sql diff --git a/migrations/dlu/5_brick_model_sd0.sql b/migrations/dlu/mysql/5_brick_model_sd0.sql similarity index 100% rename from migrations/dlu/5_brick_model_sd0.sql rename to migrations/dlu/mysql/5_brick_model_sd0.sql diff --git a/migrations/dlu/6_property_behaviors.sql b/migrations/dlu/mysql/6_property_behaviors.sql similarity index 100% rename from migrations/dlu/6_property_behaviors.sql rename to migrations/dlu/mysql/6_property_behaviors.sql diff --git a/migrations/dlu/7_make_play_key_id_nullable.sql b/migrations/dlu/mysql/7_make_play_key_id_nullable.sql similarity index 100% rename from migrations/dlu/7_make_play_key_id_nullable.sql rename to migrations/dlu/mysql/7_make_play_key_id_nullable.sql diff --git a/migrations/dlu/8_foreign_play_key.sql b/migrations/dlu/mysql/8_foreign_play_key.sql similarity index 100% rename from migrations/dlu/8_foreign_play_key.sql rename to migrations/dlu/mysql/8_foreign_play_key.sql diff --git a/migrations/dlu/9_Update_Leaderboard_Storage.sql b/migrations/dlu/mysql/9_Update_Leaderboard_Storage.sql similarity index 100% rename from migrations/dlu/9_Update_Leaderboard_Storage.sql rename to migrations/dlu/mysql/9_Update_Leaderboard_Storage.sql diff --git a/migrations/dlu/sqlite/0_initial.sql b/migrations/dlu/sqlite/0_initial.sql new file mode 100644 index 00000000..887c61da --- /dev/null +++ b/migrations/dlu/sqlite/0_initial.sql @@ -0,0 +1,198 @@ +CREATE TABLE IF NOT EXISTS accounts ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +name TEXT NOT NULL UNIQUE, +password TEXT NOT NULL, +gm_level BIGINT NOT NULL DEFAULT 0, +locked INTEGER NOT NULL DEFAULT FALSE, +banned INTEGER NOT NULL DEFAULT FALSE, +play_key_id INTEGER DEFAULT NULL, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +mute_expire BIGINT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS charinfo ( +id BIGINT NOT NULL PRIMARY KEY, +account_id INTEGER NOT NULL REFERENCES accounts(id), +name TEXT NOT NULL UNIQUE, +pending_name TEXT NOT NULL, +needs_rename INTEGER NOT NULL DEFAULT FALSE, +prop_clone_id INTEGER UNIQUE, +last_login BIGINT NOT NULL DEFAULT 0, +permission_map BIGINT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS charxml ( +id BIGINT NOT NULL PRIMARY KEY REFERENCES charinfo(id), +xml_data TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS command_log ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +character_id BIGINT NOT NULL REFERENCES charinfo(id), +command TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS friends ( +player_id BIGINT NOT NULL REFERENCES charinfo(id), +friend_id BIGINT NOT NULL REFERENCES charinfo(id), +best_friend INTEGER NOT NULL DEFAULT FALSE, + +PRIMARY KEY (player_id, friend_id) +); + +CREATE TABLE IF NOT EXISTS leaderboard ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +game_id INTEGER NOT NULL DEFAULT 0, +last_played DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +character_id BIGINT NOT NULL REFERENCES charinfo(id), +primaryScore DOUBLE NOT NULL DEFAULT 0, +secondaryScore DOUBLE NOT NULL DEFAULT 0, +tertiaryScore DOUBLE NOT NULL DEFAULT 0, +numWins INTEGER NOT NULL DEFAULT 0, +timesPlayed INTEGER NOT NULL DEFAULT 1 +); + +CREATE TABLE IF NOT EXISTS mail ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +sender_id INTEGER NOT NULL DEFAULT 0, +sender_name TEXT NOT NULL DEFAULT '', +receiver_id BIGINT NOT NULL REFERENCES charinfo(id), +receiver_name TEXT NOT NULL, +time_sent BIGINT NOT NULL, +subject TEXT NOT NULL, +body TEXT NOT NULL, +attachment_id BIGINT NOT NULL DEFAULT 0, +attachment_lot INTEGER NOT NULL DEFAULT 0, +attachment_subkey BIGINT NOT NULL DEFAULT 0, +attachment_count INTEGER NOT NULL DEFAULT 0, +was_read INTEGER NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS object_id_tracker ( +last_object_id BIGINT NOT NULL DEFAULT 0 PRIMARY KEY +); + +CREATE TABLE IF NOT EXISTS pet_names ( +id BIGINT NOT NULL PRIMARY KEY, +pet_name TEXT NOT NULL, +approved INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS play_keys ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +key_string TEXT NOT NULL UNIQUE, +key_uses INTEGER NOT NULL DEFAULT 1, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +active INTEGER NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS properties ( +id BIGINT NOT NULL PRIMARY KEY, +owner_id BIGINT NOT NULL REFERENCES charinfo(id), +template_id INTEGER NOT NULL, +clone_id BIGINT REFERENCES charinfo(prop_clone_id), +name TEXT NOT NULL, +description TEXT NOT NULL, +rent_amount INTEGER NOT NULL, +rent_due BIGINT NOT NULL, +privacy_option INTEGER NOT NULL, +mod_approved INTEGER NOT NULL DEFAULT FALSE, +last_updated BIGINT NOT NULL, +time_claimed BIGINT NOT NULL, +rejection_reason TEXT NOT NULL, +reputation BIGINT NOT NULL, +zone_id INTEGER NOT NULL, +performance_cost DOUBLE DEFAULT 0.0 +); + +CREATE TABLE IF NOT EXISTS ugc ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +account_id INTEGER NOT NULL REFERENCES accounts(id), +character_id BIGINT NOT NULL REFERENCES charinfo(id), +is_optimized INTEGER NOT NULL DEFAULT FALSE, +lxfml BLOB NOT NULL, +bake_ao INTEGER NOT NULL DEFAULT FALSE, +filename TEXT NOT NULL DEFAULT ('') +); + +CREATE TABLE IF NOT EXISTS properties_contents ( +id BIGINT NOT NULL PRIMARY KEY, +property_id BIGINT NOT NULL REFERENCES properties(id), +ugc_id INTEGER NULL REFERENCES ugc(id), +lot INTEGER NOT NULL, +x DOUBLE NOT NULL, +y DOUBLE NOT NULL, +z DOUBLE NOT NULL, +rx DOUBLE NOT NULL, +ry DOUBLE NOT NULL, +rz DOUBLE NOT NULL, +rw DOUBLE NOT NULL, +model_name TEXT NOT NULL DEFAULT (''), +model_description TEXT NOT NULL DEFAULT (''), +behavior_1 INTEGER NOT NULL DEFAULT 0, +behavior_2 INTEGER NOT NULL DEFAULT 0, +behavior_3 INTEGER NOT NULL DEFAULT 0, +behavior_4 INTEGER NOT NULL DEFAULT 0, +behavior_5 INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS activity_log ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +character_id BIGINT NOT NULL REFERENCES charinfo(id), +activity INTEGER NOT NULL, +time BIGINT NOT NULL, +map_id INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS bug_reports ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +body TEXT NOT NULL, +client_version TEXT NOT NULL, +other_player_id TEXT NOT NULL, +selection TEXT NOT NULL, +submitted DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +reporter_id INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS servers ( +id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, +name TEXT NOT NULL, +ip TEXT NOT NULL, +port INTEGER NOT NULL, +state INTEGER NOT NULL, +version INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS player_cheat_detections ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER REFERENCES accounts(id), + name TEXT NOT NULL, + violation_msg TEXT NOT NULL, + violation_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + violation_system_address TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS ugc_modular_build ( + ugc_id BIGINT NOT NULL PRIMARY KEY, + character_id BIGINT NOT NULL REFERENCES charinfo(id) ON DELETE CASCADE, + ldf_config VARCHAR(60) NOT NULL +); + +CREATE TABLE IF NOT EXISTS ignore_list ( + player_id BIGINT NOT NULL REFERENCES charinfo(id) ON DELETE CASCADE, + ignored_player_id BIGINT NOT NULL REFERENCES charinfo(id) ON DELETE CASCADE, + + PRIMARY KEY (player_id, ignored_player_id) +); + +CREATE TABLE IF NOT EXISTS accounts_rewardcodes ( + account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + rewardcode INTEGER NOT NULL, + PRIMARY KEY (account_id, rewardcode) +); + +CREATE TABLE IF NOT EXISTS behaviors ( + behavior_info TEXT NOT NULL, + behavior_id BIGINT NOT NULL PRIMARY KEY, + character_id BIGINT NOT NULL DEFAULT 0 +); diff --git a/resources/sharedconfig.ini b/resources/sharedconfig.ini index e487058d..aa8b5eb4 100644 --- a/resources/sharedconfig.ini +++ b/resources/sharedconfig.ini @@ -65,4 +65,11 @@ version_minor=64 # The port the chat server is started and listening on # Used in chat and world servers -chat_server_port=2005 \ No newline at end of file +chat_server_port=2005 + +sqlite_database_path=resServer/dlu.sqlite + +database_type=sqlite + +# Skips the account creation check in master. Used for non-interactive setups. +skip_account_creation=0 diff --git a/thirdparty/SQLite/CMakeLists.txt b/thirdparty/SQLite/CMakeLists.txt index e745c46a..4a9e9311 100644 --- a/thirdparty/SQLite/CMakeLists.txt +++ b/thirdparty/SQLite/CMakeLists.txt @@ -3,7 +3,7 @@ set (SQLITE3_SOURCES "sqlite3.c" ) -add_library (sqlite3 ${SQLITE3_SOURCES}) +add_library(sqlite3 ${SQLITE3_SOURCES}) if(UNIX) # Add warning disable flags and link Unix libraries to sqlite3 diff --git a/thirdparty/SQLite/CppSQLite3.cpp b/thirdparty/SQLite/CppSQLite3.cpp index f816ac42..21e2811e 100644 --- a/thirdparty/SQLite/CppSQLite3.cpp +++ b/thirdparty/SQLite/CppSQLite3.cpp @@ -1016,6 +1016,20 @@ void CppSQLite3Statement::bind(int nParam, const int nValue) } +void CppSQLite3Statement::bind(int nParam, const sqlite_int64 nValue) +{ + checkVM(); + int nRes = sqlite3_bind_int64(mpVM, nParam, nValue); + + if (nRes != SQLITE_OK) + { + throw CppSQLite3Exception(nRes, + (char*)"Error binding int64 param", + DONT_DELETE_MSG); + } +} + + void CppSQLite3Statement::bind(int nParam, const double dValue) { checkVM(); @@ -1097,6 +1111,12 @@ void CppSQLite3Statement::bind(const char* szParam, const int nValue) bind(nParam, nValue); } +void CppSQLite3Statement::bind(const char* szParam, const sqlite_int64 nValue) +{ + int nParam = bindParameterIndex(szParam); + bind(nParam, nValue); +} + void CppSQLite3Statement::bind(const char* szParam, const double dwValue) { int nParam = bindParameterIndex(szParam); diff --git a/thirdparty/SQLite/CppSQLite3.h b/thirdparty/SQLite/CppSQLite3.h index 70c4b8e8..a98277b1 100644 --- a/thirdparty/SQLite/CppSQLite3.h +++ b/thirdparty/SQLite/CppSQLite3.h @@ -252,6 +252,7 @@ public: void bind(int nParam, const char* szValue); void bind(int nParam, const int nValue); void bind(int nParam, const double dwValue); + void bind(int nParam, const sqlite_int64 llValue); void bind(int nParam, const unsigned char* blobValue, int nLen); void bindNull(int nParam); @@ -259,6 +260,7 @@ public: void bind(const char* szParam, const char* szValue); void bind(const char* szParam, const int nValue); void bind(const char* szParam, const double dwValue); + void bind(const char* szParam, const sqlite_int64 llValue); void bind(const char* szParam, const unsigned char* blobValue, int nLen); void bindNull(const char* szParam); diff --git a/versions.txt b/versions.txt index fa7ea86c..682d3437 100644 --- a/versions.txt +++ b/versions.txt @@ -1,3 +1,4 @@ +3.0 - Single player with minimal setup is fully functional with SQLite database 2.3 - Dragonmaw functional, new slash command system, vanity system overhaul 2.2 - Code cleanup and QoL fixes 2.1 - Bug and crash fixes