diff --git a/.env.example b/.env.example
index 5e84184c..6ea77deb 100644
--- a/.env.example
+++ b/.env.example
@@ -3,12 +3,20 @@ CLIENT_PATH=./client
# Updates NET_VERSION in CMakeVariables.txt
NET_VERSION=171022
# make sure this is a long random string
-# grab a "SHA 256-bit Key" from here: https://keygen.io/
+# generate a "SHA 256-bit Key" from here: https://gchq.github.io/CyberChef/#recipe=Pseudo-Random_Number_Generator(256,'Hex')
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/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 9c734166..191efb53 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -43,6 +43,7 @@ jobs:
build/*/*.ini
build/*/*.so
build/*/*.dll
+ build/*/*.dylib
build/*/vanity/
build/*/navmeshes/
build/*/migrations/
diff --git a/.gitignore b/.gitignore
index ff1505d6..39f7c74f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,6 @@ valgrind-out.txt
# Third party libraries
thirdparty/mysql/
thirdparty/mysql_linux/
-CMakeVariables.txt
# Build folders
build/
@@ -96,6 +95,7 @@ ipch/
# Exceptions:
CMakeSettings.json
+CMakeUserPresets.json
*.vcxproj
*.filters
*.cmake
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ddbb6c3a..da9245b0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -78,6 +78,7 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
# Disabled no-register
# Disabled unknown pragmas because Linux doesn't understand Windows pragmas.
if(UNIX)
+ add_link_options("-Wl,-rpath,$ORIGIN/")
add_compile_options("-fPIC")
add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0)
@@ -186,16 +187,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})
@@ -225,21 +228,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)
@@ -324,7 +314,7 @@ add_subdirectory(dPhysics)
add_subdirectory(dServer)
# Create a list of common libraries shared between all binaries
-set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "MariaDB::ConnCpp" "magic_enum")
+set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
# Add platform specific common libraries
if(UNIX)
diff --git a/CMakePresets.json b/CMakePresets.json
index c4595ed5..3ed904e7 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -11,9 +11,6 @@
"displayName": "Default configure step",
"description": "Use 'build' dir and Unix makefiles",
"binaryDir": "${sourceDir}/build",
- "environment": {
- "DLU_CONFIG_DIR": "${sourceDir}/build"
- },
"generator": "Unix Makefiles"
},
{
diff --git a/README.md b/README.md
index 1caa0fb0..fc076dff 100644
--- a/README.md
+++ b/README.md
@@ -13,21 +13,33 @@ 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 windows release](https://github.com/DarkflameUniverse/DarkflameServer/releases) (or whichever release you need) and extract the files into a folder inside your client. Note that this setup is expecting that when double clicking the folder that you put in the same folder as `legouniverse.exe`, the file `MasterServer.exe` is in there.
+* You should be able to see the folder with the server files in the same folder as `legouniverse.exe`.
+* Go into the server files folder and open `sharedconfig.ini`. 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. After you have created the account, the server will shutdown and need to be restarted.
+* To connect to the server, either delete the file `boot.cfg` which is found in your LEGO Universe client, rename the file `boot.cfg` to something else or follow the steps [here](#allowing-a-user-to-connect-to-your-server) if you wish to keep the file.
+* 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 stops 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 +51,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 +285,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**
@@ -371,7 +390,7 @@ at once. For that:
- Download the [.env.example](.env.example) file and place it next to `client` with the file name `.env`
- You may get warnings that this name starts with a dot, acknowledge those, this is intentional. Depending on your operating system, you may need to activate showing hidden files (e.g. Ctrl-H in Gnome on Linux) and/or file extensions ("File name extensions" in the "View" tab on Windows).
- Update the `ACCOUNT_MANAGER_SECRET` and `MARIADB_PASSWORD` with strong random passwords.
- - Use a password generator like
+ - Use a password generator
- Avoid `:` and `@` characters
- Once the database user is created, changing the password will not update it, so the server will just fail to connect.
- Set `EXTERNAL_IP` to your LAN IP or public IP if you want to host the game for friends & family
diff --git a/dAuthServer/AuthServer.cpp b/dAuthServer/AuthServer.cpp
index d306eb70..741a6e59 100644
--- a/dAuthServer/AuthServer.cpp
+++ b/dAuthServer/AuthServer.cpp
@@ -60,7 +60,7 @@ int main(int argc, char** argv) {
try {
Database::Connect();
- } catch (sql::SQLException& ex) {
+ } catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("AuthServer");
delete Game::server;
diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp
index 022175e5..b4959992 100644
--- a/dChatServer/ChatServer.cpp
+++ b/dChatServer/ChatServer.cpp
@@ -81,7 +81,7 @@ int main(int argc, char** argv) {
//Connect to the MySQL Database
try {
Database::Connect();
- } catch (sql::SQLException& ex) {
+ } catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("ChatServer");
delete Game::server;
diff --git a/dCommon/BrickByBrickFix.cpp b/dCommon/BrickByBrickFix.cpp
index 85f4a558..7d64760f 100644
--- a/dCommon/BrickByBrickFix.cpp
+++ b/dCommon/BrickByBrickFix.cpp
@@ -123,7 +123,7 @@ uint32_t BrickByBrickFix::UpdateBrickByBrickModelsToSd0() {
Database::Get()->UpdateUgcModelData(model.id, outputStringStream);
LOG("Updated model %i to sd0", model.id);
updatedModels++;
- } catch (sql::SQLException exception) {
+ } catch (std::exception& exception) {
LOG("Failed to update model %i. This model should be inspected manually to see why."
"The database error is %s", model.id, exception.what());
}
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/dCommon/GeneralUtils.cpp b/dCommon/GeneralUtils.cpp
index cc4c4b1e..f860cdba 100644
--- a/dCommon/GeneralUtils.cpp
+++ b/dCommon/GeneralUtils.cpp
@@ -291,11 +291,12 @@ std::u16string GeneralUtils::ReadWString(RakNet::BitStream& inStream) {
std::vector GeneralUtils::GetSqlFileNamesFromFolder(const std::string_view folder) {
// Because we dont know how large the initial number before the first _ is we need to make it a map like so.
- std::map filenames{};
+ std::map filenames{};
for (const auto& t : std::filesystem::directory_iterator(folder)) {
- auto filename = t.path().filename().string();
- const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0));
- filenames.emplace(index, std::move(filename));
+ if (t.is_directory() || t.is_symlink()) continue;
+ auto filename = t.path().filename().string();
+ const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0));
+ filenames.emplace(index, std::move(filename));
}
// Now sort the map by the oldest migration.
diff --git a/dDatabase/CMakeLists.txt b/dDatabase/CMakeLists.txt
index 004bdc14..42bdb983 100644
--- a/dDatabase/CMakeLists.txt
+++ b/dDatabase/CMakeLists.txt
@@ -2,6 +2,12 @@ add_subdirectory(CDClientDatabase)
add_subdirectory(GameDatabase)
add_library(dDatabase STATIC "MigrationRunner.cpp")
+
+add_custom_target(conncpp_dylib
+ ${CMAKE_COMMAND} -E copy $ ${PROJECT_BINARY_DIR})
+
+add_dependencies(dDatabase conncpp_dylib)
+
target_include_directories(dDatabase PUBLIC ".")
target_link_libraries(dDatabase
PUBLIC dDatabaseCDClient dDatabaseGame)
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 65b04722..cb74431c 100644
--- a/dDatabase/GameDatabase/Database.h
+++ b/dDatabase/GameDatabase/Database.h
@@ -1,7 +1,6 @@
#pragma once
#include
-#include
#include "GameDatabase.h"
@@ -13,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 f52c8c4e..d0b5c866 100644
--- a/dDatabase/GameDatabase/GameDatabase.h
+++ b/dDatabase/GameDatabase/GameDatabase.h
@@ -24,14 +24,10 @@
#include "IIgnoreList.h"
#include "IAccountsRewardCodes.h"
#include "IBehaviors.h"
-
-namespace sql {
- class Statement;
- class PreparedStatement;
-};
+#include "IUgcModularBuild.h"
#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
@@ -42,14 +38,13 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
- public IBehaviors {
+ public IBehaviors, public IUgcModularBuild {
public:
virtual ~GameDatabase() = default;
// TODO: These should be made private.
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/ITables/ILeaderboard.h b/dDatabase/GameDatabase/ITables/ILeaderboard.h
index 84d44eb2..f88497b0 100644
--- a/dDatabase/GameDatabase/ITables/ILeaderboard.h
+++ b/dDatabase/GameDatabase/ITables/ILeaderboard.h
@@ -3,12 +3,45 @@
#include
#include
+#include
+#include
class ILeaderboard {
public:
+ struct Entry {
+ uint32_t charId{};
+ uint32_t lastPlayedTimestamp{};
+ float primaryScore{};
+ float secondaryScore{};
+ uint32_t tertiaryScore{};
+ uint32_t numWins{};
+ uint32_t numTimesPlayed{};
+ uint32_t ranking{};
+ std::string name{};
+ };
+
+ struct Score {
+ auto operator<=>(const Score& rhs) const = default;
+
+ float primaryScore{ 0.0f };
+ float secondaryScore{ 0.0f };
+ float tertiaryScore{ 0.0f };
+ };
+
// Get the donation total for the given activity id.
virtual std::optional GetDonationTotal(const uint32_t activityId) = 0;
+
+ virtual std::vector GetDescendingLeaderboard(const uint32_t activityId) = 0;
+ virtual std::vector GetAscendingLeaderboard(const uint32_t activityId) = 0;
+ virtual std::vector GetNsLeaderboard(const uint32_t activityId) = 0;
+ virtual std::vector GetAgsLeaderboard(const uint32_t activityId) = 0;
+ virtual std::optional GetPlayerScore(const uint32_t playerId, const uint32_t gameId) = 0;
+
+ virtual void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
+ virtual void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
+ virtual void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) = 0;
+ virtual void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) = 0;
};
#endif //!__ILEADERBOARD__H__
diff --git a/dDatabase/GameDatabase/ITables/IUgcModularBuild.h b/dDatabase/GameDatabase/ITables/IUgcModularBuild.h
new file mode 100644
index 00000000..4aa2e312
--- /dev/null
+++ b/dDatabase/GameDatabase/ITables/IUgcModularBuild.h
@@ -0,0 +1,14 @@
+#ifndef IUGCMODULARBUILD_H
+#define IUGCMODULARBUILD_H
+
+#include
+#include
+#include
+
+class IUgcModularBuild {
+public:
+ virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional characterId) = 0;
+ virtual void DeleteUgcBuild(const LWOOBJID bigId) = 0;
+};
+
+#endif //!IUGCMODULARBUILD_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 f30e33ce..08168141 100644
--- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h
+++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h
@@ -7,6 +7,7 @@
#include "GameDatabase.h"
typedef std::unique_ptr& UniquePreppedStmtRef;
+typedef std::unique_ptr UniqueResultSet;
// 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.
@@ -29,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;
@@ -113,6 +113,19 @@ public:
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;
+ 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/MySQL/Tables/CMakeLists.txt b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt
index 47cd220e..2f1fa6de 100644
--- a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt
+++ b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt
@@ -20,6 +20,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"PropertyContents.cpp"
"Servers.cpp"
"Ugc.cpp"
+ "UgcModularBuild.cpp"
PARENT_SCOPE
)
diff --git a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp
index 22403abb..14ac121a 100644
--- a/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp
+++ b/dDatabase/GameDatabase/MySQL/Tables/Leaderboard.cpp
@@ -1,5 +1,9 @@
#include "MySQLDatabase.h"
+#include "Game.h"
+#include "Logger.h"
+#include "dConfig.h"
+
std::optional MySQLDatabase::GetDonationTotal(const uint32_t activityId) {
auto donation_total = ExecuteSelect("SELECT SUM(primaryScore) as donation_total FROM leaderboard WHERE game_id = ?;", activityId);
@@ -9,3 +13,79 @@ std::optional MySQLDatabase::GetDonationTotal(const uint32_t activityI
return donation_total->getUInt("donation_total");
}
+
+std::vector ProcessQuery(UniqueResultSet& rows) {
+ std::vector entries;
+ entries.reserve(rows->rowsCount());
+
+ while (rows->next()) {
+ auto& entry = entries.emplace_back();
+
+ entry.charId = rows->getUInt("character_id");
+ entry.lastPlayedTimestamp = rows->getUInt("lp_unix");
+ entry.primaryScore = rows->getFloat("primaryScore");
+ entry.secondaryScore = rows->getFloat("secondaryScore");
+ entry.tertiaryScore = rows->getFloat("tertiaryScore");
+ entry.numWins = rows->getUInt("numWins");
+ entry.numTimesPlayed = rows->getUInt("timesPlayed");
+ entry.name = rows->getString("char_name");
+ // entry.ranking is never set because its calculated in leaderboard in code.
+ }
+
+ return entries;
+}
+
+std::vector MySQLDatabase::GetDescendingLeaderboard(const uint32_t activityId) {
+ auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) 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(leaderboard);
+}
+
+std::vector MySQLDatabase::GetAscendingLeaderboard(const uint32_t activityId) {
+ auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) 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(leaderboard);
+}
+
+std::vector MySQLDatabase::GetAgsLeaderboard(const uint32_t activityId) {
+ auto query = Game::config->GetValue("classic_survival_scoring") != "1" ?
+ "SELECT *, UNIX_TIMESTAMP(last_played) 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 *, UNIX_TIMESTAMP(last_played) 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 leaderboard = ExecuteSelect(query, activityId);
+ return ProcessQuery(leaderboard);
+}
+
+std::vector MySQLDatabase::GetNsLeaderboard(const uint32_t activityId) {
+ auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) 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(leaderboard);
+}
+
+void MySQLDatabase::SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) {
+ ExecuteInsert("INSERT leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, character_id = ?, game_id = ?;",
+ score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId);
+}
+
+void MySQLDatabase::UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) {
+ ExecuteInsert("UPDATE leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;",
+ score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId);
+}
+
+void MySQLDatabase::IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) {
+ ExecuteUpdate("UPDATE leaderboard SET timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId);
+}
+
+std::optional MySQLDatabase::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->next()) {
+ toReturn = ILeaderboard::Score{
+ .primaryScore = res->getFloat("primaryScore"),
+ .secondaryScore = res->getFloat("secondaryScore"),
+ .tertiaryScore = res->getFloat("tertiaryScore")
+ };
+ }
+
+ return toReturn;
+}
+
+void MySQLDatabase::IncrementNumWins(const uint32_t playerId, const uint32_t gameId) {
+ ExecuteUpdate("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId);
+}
diff --git a/dDatabase/GameDatabase/MySQL/Tables/UgcModularBuild.cpp b/dDatabase/GameDatabase/MySQL/Tables/UgcModularBuild.cpp
new file mode 100644
index 00000000..a9573515
--- /dev/null
+++ b/dDatabase/GameDatabase/MySQL/Tables/UgcModularBuild.cpp
@@ -0,0 +1,9 @@
+#include "MySQLDatabase.h"
+
+void MySQLDatabase::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 MySQLDatabase::DeleteUgcBuild(const LWOOBJID bigId) {
+ ExecuteDelete("DELETE FROM ugc_modular_build WHERE ugc_id = ?;", bigId);
+}
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 1fbb1845..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;
@@ -91,6 +90,18 @@ class TestSQLDatabase : public GameDatabase {
void RemoveBehavior(const int32_t behaviorId) override;
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
std::optional GetProperties(const IProperty::PropertyLookup& params) override { return {}; };
+ std::vector GetDescendingLeaderboard(const uint32_t activityId) override { return {}; };
+ std::vector GetAscendingLeaderboard(const uint32_t activityId) override { return {}; };
+ std::vector GetNsLeaderboard(const uint32_t activityId) override { return {}; };
+ std::vector GetAgsLeaderboard(const uint32_t activityId) override { return {}; };
+ 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 { return {}; };
+ 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 { return 0; };
};
#endif //!TESTSQLDATABASE_H
diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp
index 8034a3e2..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();
- sql::SQLString finalSQL = "";
+ // 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());
@@ -61,12 +70,12 @@ void MigrationRunner::RunMigrations() {
}
if (!finalSQL.empty()) {
- auto migration = GeneralUtils::SplitString(static_cast(finalSQL), ';');
+ auto migration = GeneralUtils::SplitString(finalSQL, ';');
for (auto& query : migration) {
try {
if (query.empty()) continue;
- Database::Get()->ExecuteCustomQuery(query.c_str());
- } catch (sql::SQLException& e) {
+ Database::Get()->ExecuteCustomQuery(query);
+ } catch (std::exception& e) {
LOG("Encountered error running migration: %s", e.what());
}
}
@@ -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/Entity.cpp b/dGame/Entity.cpp
index 54629888..f5887996 100644
--- a/dGame/Entity.cpp
+++ b/dGame/Entity.cpp
@@ -83,6 +83,7 @@
#include "ItemComponent.h"
#include "GhostComponent.h"
#include "AchievementVendorComponent.h"
+#include "VanityUtilities.h"
// Table includes
#include "CDComponentsRegistryTable.h"
@@ -96,6 +97,8 @@
#include "CDSkillBehaviorTable.h"
#include "CDZoneTableTable.h"
+#include
+
Observable Entity::OnPlayerPositionUpdate;
Entity::Entity(const LWOOBJID& objectID, EntityInfo info, User* parentUser, Entity* parentEntity) {
@@ -285,8 +288,9 @@ void Entity::Initialize() {
AddComponent(propertyEntranceComponentID);
}
- if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::CONTROLLABLE_PHYSICS) > 0) {
- auto* controllablePhysics = AddComponent();
+ const int32_t controllablePhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::CONTROLLABLE_PHYSICS);
+ if (controllablePhysicsComponentID > 0) {
+ auto* controllablePhysics = AddComponent(controllablePhysicsComponentID);
if (m_Character) {
controllablePhysics->LoadFromXml(m_Character->GetXMLDoc());
@@ -329,16 +333,19 @@ void Entity::Initialize() {
AddComponent(simplePhysicsComponentID);
}
- if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS) > 0) {
- AddComponent();
+ const int32_t rigidBodyPhantomPhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS);
+ if (rigidBodyPhantomPhysicsComponentID > 0) {
+ AddComponent(rigidBodyPhantomPhysicsComponentID);
}
- if (markedAsPhantom || compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::PHANTOM_PHYSICS) > 0) {
- AddComponent()->SetPhysicsEffectActive(false);
+ const int32_t phantomPhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::PHANTOM_PHYSICS);
+ if (markedAsPhantom || phantomPhysicsComponentID > 0) {
+ AddComponent(phantomPhysicsComponentID)->SetPhysicsEffectActive(false);
}
- if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::HAVOK_VEHICLE_PHYSICS) > 0) {
- auto* havokVehiclePhysicsComponent = AddComponent();
+ const int32_t havokVehiclePhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::HAVOK_VEHICLE_PHYSICS);
+ if (havokVehiclePhysicsComponentID > 0) {
+ auto* havokVehiclePhysicsComponent = AddComponent(havokVehiclePhysicsComponentID);
havokVehiclePhysicsComponent->SetPosition(m_DefaultPosition);
havokVehiclePhysicsComponent->SetRotation(m_DefaultRotation);
}
@@ -1271,6 +1278,7 @@ void Entity::Update(const float deltaTime) {
auto timerName = timer.GetName();
m_Timers.erase(m_Timers.begin() + timerPosition);
GetScript()->OnTimerDone(this, timerName);
+ VanityUtilities::OnTimerDone(this, timerName);
TriggerEvent(eTriggerEventType::TIMER_DONE, this);
} else {
@@ -1334,6 +1342,7 @@ void Entity::OnCollisionProximity(LWOOBJID otherEntity, const std::string& proxN
if (!other) return;
GetScript()->OnProximityUpdate(this, other, proxName, status);
+ VanityUtilities::OnProximityUpdate(this, other, proxName, status);
RocketLaunchpadControlComponent* rocketComp = GetComponent();
if (!rocketComp) return;
@@ -1351,6 +1360,11 @@ void Entity::OnCollisionPhantom(const LWOOBJID otherEntity) {
callback(other);
}
+ SwitchComponent* switchComp = GetComponent();
+ if (switchComp) {
+ switchComp->OnUse(other);
+ }
+
TriggerEvent(eTriggerEventType::ENTER, other);
// POI system
@@ -2153,7 +2167,19 @@ void Entity::SetRespawnPos(const NiPoint3& position) {
auto* characterComponent = GetComponent();
if (characterComponent) characterComponent->SetRespawnPos(position);
}
+
void Entity::SetRespawnRot(const NiQuaternion& rotation) {
auto* characterComponent = GetComponent();
if (characterComponent) characterComponent->SetRespawnRot(rotation);
}
+
+int32_t Entity::GetCollisionGroup() const {
+ for (const auto* component : m_Components | std::views::values) {
+ auto* compToCheck = dynamic_cast(component);
+ if (compToCheck) {
+ return compToCheck->GetCollisionGroup();
+ }
+ }
+
+ return 0;
+}
diff --git a/dGame/Entity.h b/dGame/Entity.h
index 5d2b9527..2ed7aa53 100644
--- a/dGame/Entity.h
+++ b/dGame/Entity.h
@@ -107,6 +107,11 @@ public:
const SystemAddress& GetSystemAddress() const;
+ // Returns the collision group for this entity.
+ // Because the collision group is stored on a base component, this will look for a physics component
+ // then return the collision group from that.
+ int32_t GetCollisionGroup() const;
+
/**
* Setters
*/
diff --git a/dGame/LeaderboardManager.cpp b/dGame/LeaderboardManager.cpp
index 347bd68e..da27e88b 100644
--- a/dGame/LeaderboardManager.cpp
+++ b/dGame/LeaderboardManager.cpp
@@ -1,5 +1,6 @@
#include "LeaderboardManager.h"
+#include
#include
#include
@@ -72,197 +73,191 @@ void Leaderboard::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write0();
}
-void Leaderboard::QueryToLdf(std::unique_ptr& rows) {
- Clear();
- if (rows->rowsCount() == 0) return;
+// Takes the resulting query from a leaderboard lookup and converts it to the LDF we need
+// to send it to a client.
+void QueryToLdf(Leaderboard& leaderboard, const std::vector& leaderboardEntries) {
+ using enum Leaderboard::Type;
+ leaderboard.Clear();
+ if (leaderboardEntries.empty()) return;
- this->entries.reserve(rows->rowsCount());
- while (rows->next()) {
+ for (const auto& leaderboardEntry : leaderboardEntries) {
constexpr int32_t MAX_NUM_DATA_PER_ROW = 9;
- this->entries.push_back(std::vector());
- auto& entry = this->entries.back();
+ auto& entry = leaderboard.PushBackEntry();
entry.reserve(MAX_NUM_DATA_PER_ROW);
- entry.push_back(new LDFData(u"CharacterID", rows->getInt("character_id")));
- entry.push_back(new LDFData(u"LastPlayed", rows->getUInt64("lastPlayed")));
- entry.push_back(new LDFData(u"NumPlayed", rows->getInt("timesPlayed")));
- entry.push_back(new LDFData(u"name", GeneralUtils::ASCIIToUTF16(rows->getString("name").c_str())));
- entry.push_back(new LDFData(u"RowNumber", rows->getInt("ranking")));
- switch (leaderboardType) {
- case Type::ShootingGallery:
- entry.push_back(new LDFData(u"Score", rows->getInt("primaryScore")));
+ entry.push_back(new LDFData(u"CharacterID", leaderboardEntry.charId));
+ entry.push_back(new LDFData(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp));
+ entry.push_back(new LDFData(u"NumPlayed", leaderboardEntry.numTimesPlayed));
+ entry.push_back(new LDFData(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name)));
+ entry.push_back(new LDFData(u"RowNumber", leaderboardEntry.ranking));
+ switch (leaderboard.GetLeaderboardType()) {
+ case ShootingGallery:
+ entry.push_back(new LDFData(u"Score", leaderboardEntry.primaryScore));
// Score:1
- entry.push_back(new LDFData(u"Streak", rows->getInt("secondaryScore")));
+ entry.push_back(new LDFData(u"Streak", leaderboardEntry.secondaryScore));
// Streak:1
- entry.push_back(new LDFData(u"HitPercentage", (rows->getInt("tertiaryScore") / 100.0f)));
+ entry.push_back(new LDFData(u"HitPercentage", (leaderboardEntry.tertiaryScore / 100.0f)));
// HitPercentage:3 between 0 and 1
break;
- case Type::Racing:
- entry.push_back(new LDFData(u"BestTime", rows->getDouble("primaryScore")));
+ case Racing:
+ entry.push_back(new LDFData(u"BestTime", leaderboardEntry.primaryScore));
// BestLapTime:3
- entry.push_back(new LDFData(u"BestLapTime", rows->getDouble("secondaryScore")));
+ entry.push_back(new LDFData(u"BestLapTime", leaderboardEntry.secondaryScore));
// BestTime:3
entry.push_back(new LDFData(u"License", 1));
// License:1 - 1 if player has completed mission 637 and 0 otherwise
- entry.push_back(new LDFData(u"NumWins", rows->getInt("numWins")));
+ entry.push_back(new LDFData(u"NumWins", leaderboardEntry.numWins));
// NumWins:1
break;
- case Type::UnusedLeaderboard4:
- entry.push_back(new LDFData(u"Points", rows->getInt("primaryScore")));
+ case UnusedLeaderboard4:
+ entry.push_back(new LDFData(u"Points", leaderboardEntry.primaryScore));
// Points:1
break;
- case Type::MonumentRace:
- entry.push_back(new LDFData(u"Time", rows->getInt("primaryScore")));
+ case MonumentRace:
+ entry.push_back(new LDFData(u"Time", leaderboardEntry.primaryScore));
// Time:1(?)
break;
- case Type::FootRace:
- entry.push_back(new LDFData(u"Time", rows->getInt("primaryScore")));
+ case FootRace:
+ entry.push_back(new LDFData(u"Time", leaderboardEntry.primaryScore));
// Time:1
break;
- case Type::Survival:
- entry.push_back(new LDFData(u"Points", rows->getInt("primaryScore")));
+ case Survival:
+ entry.push_back(new LDFData(u"Points", leaderboardEntry.primaryScore));
// Points:1
- entry.push_back(new LDFData(u"Time", rows->getInt("secondaryScore")));
+ entry.push_back(new LDFData(u"Time", leaderboardEntry.secondaryScore));
// Time:1
break;
- case Type::SurvivalNS:
- entry.push_back(new LDFData(u"Wave", rows->getInt("primaryScore")));
+ case SurvivalNS:
+ entry.push_back(new LDFData(u"Wave", leaderboardEntry.primaryScore));
// Wave:1
- entry.push_back(new LDFData(u"Time", rows->getInt("secondaryScore")));
+ entry.push_back(new LDFData(u"Time", leaderboardEntry.secondaryScore));
// Time:1
break;
- case Type::Donations:
- entry.push_back(new LDFData(u"Score", rows->getInt("primaryScore")));
+ case Donations:
+ entry.push_back(new LDFData(u"Score", leaderboardEntry.primaryScore));
// Score:1
break;
- case Type::None:
- // This type is included here simply to resolve a compiler warning on mac about unused enum types
- break;
+ case None:
+ [[fallthrough]];
default:
break;
}
}
}
-const std::string_view Leaderboard::GetOrdering(Leaderboard::Type leaderboardType) {
- // Use a switch case and return desc for all 3 columns if higher is better and asc if lower is better
- switch (leaderboardType) {
- case Type::Racing:
- case Type::MonumentRace:
- return "primaryScore ASC, secondaryScore ASC, tertiaryScore ASC";
- case Type::Survival:
- return Game::config->GetValue("classic_survival_scoring") == "1" ?
- "secondaryScore DESC, primaryScore DESC, tertiaryScore DESC" :
- "primaryScore DESC, secondaryScore DESC, tertiaryScore DESC";
- case Type::SurvivalNS:
- return "primaryScore DESC, secondaryScore ASC, tertiaryScore DESC";
- case Type::ShootingGallery:
- case Type::FootRace:
- case Type::UnusedLeaderboard4:
- case Type::Donations:
- case Type::None:
- default:
- return "primaryScore DESC, secondaryScore DESC, tertiaryScore DESC";
+std::vector FilterTo10(const std::vector& leaderboard, const uint32_t relatedPlayer, const Leaderboard::InfoType infoType) {
+ std::vector toReturn;
+
+ int32_t index = 0;
+ // for friends and top, we dont need to find this players index.
+ if (infoType == Leaderboard::InfoType::MyStanding || infoType == Leaderboard::InfoType::Friends) {
+ for (; index < leaderboard.size(); index++) {
+ if (leaderboard[index].charId == relatedPlayer) break;
+ }
}
+
+ if (leaderboard.size() < 10) {
+ toReturn.assign(leaderboard.begin(), leaderboard.end());
+ index = 0;
+ } else if (index < 10) {
+ toReturn.assign(leaderboard.begin(), leaderboard.begin() + 10); // get the top 10 since we are in the top 10
+ index = 0;
+ } else if (index > leaderboard.size() - 10) {
+ toReturn.assign(leaderboard.end() - 10, leaderboard.end()); // get the bottom 10 since we are in the bottom 10
+ index = leaderboard.size() - 10;
+ } else {
+ toReturn.assign(leaderboard.begin() + index - 5, leaderboard.begin() + index + 5); // get the 5 above and below
+ index -= 5;
+ }
+
+ int32_t i = index;
+ for (auto& entry : toReturn) {
+ entry.ranking = ++i;
+ }
+
+ return toReturn;
}
-void Leaderboard::SetupLeaderboard(bool weekly, uint32_t resultStart, uint32_t resultEnd) {
- resultStart++;
- resultEnd++;
- // We need everything except 1 column so i'm selecting * from leaderboard
- const std::string queryBase =
- R"QUERY(
- WITH leaderboardsRanked AS (
- SELECT leaderboard.*, charinfo.name,
- RANK() OVER
- (
- ORDER BY %s, UNIX_TIMESTAMP(last_played) ASC, id DESC
- ) AS ranking
- FROM leaderboard JOIN charinfo on charinfo.id = leaderboard.character_id
- WHERE game_id = ? %s
- ),
- myStanding AS (
- SELECT
- ranking as myRank
- FROM leaderboardsRanked
- WHERE id = ?
- ),
- lowestRanking AS (
- SELECT MAX(ranking) AS lowestRank
- FROM leaderboardsRanked
- )
- SELECT leaderboardsRanked.*, character_id, UNIX_TIMESTAMP(last_played) as lastPlayed, leaderboardsRanked.name, leaderboardsRanked.ranking FROM leaderboardsRanked, myStanding, lowestRanking
- WHERE leaderboardsRanked.ranking
- BETWEEN
- LEAST(GREATEST(CAST(myRank AS SIGNED) - 5, %i), CAST(lowestRanking.lowestRank AS SIGNED) - 9)
- AND
- LEAST(GREATEST(myRank + 5, %i), lowestRanking.lowestRank)
- ORDER BY ranking ASC;
- )QUERY";
+std::vector FilterWeeklies(const std::vector& leaderboard) {
+ // Filter the leaderboard to only include entries from the last week
+ const auto currentTime = std::chrono::system_clock::now();
+ auto epochTime = currentTime.time_since_epoch().count();
+ constexpr auto SECONDS_IN_A_WEEK = 60 * 60 * 24 * 7; // if you think im taking leap seconds into account thats cute.
- std::string friendsFilter =
- R"QUERY(
- AND (
- character_id IN (
- SELECT fr.requested_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
- )
- OR character_id = ?
- )
- )QUERY";
-
- std::string weeklyFilter = " AND UNIX_TIMESTAMP(last_played) BETWEEN UNIX_TIMESTAMP(date_sub(now(),INTERVAL 1 WEEK)) AND UNIX_TIMESTAMP(now()) ";
-
- std::string filter;
- // Setup our filter based on the query type
- if (this->infoType == InfoType::Friends) filter += friendsFilter;
- if (this->weekly) filter += weeklyFilter;
- const auto orderBase = GetOrdering(this->leaderboardType);
-
- // For top query, we want to just rank all scores, but for all others we need the scores around a specific player
- std::string baseLookup;
- if (this->infoType == InfoType::Top) {
- baseLookup = "SELECT id, last_played FROM leaderboard WHERE game_id = ? " + (this->weekly ? weeklyFilter : std::string("")) + " ORDER BY ";
- baseLookup += orderBase.data();
- } else {
- baseLookup = "SELECT id, last_played FROM leaderboard WHERE game_id = ? " + (this->weekly ? weeklyFilter : std::string("")) + " AND character_id = ";
- baseLookup += std::to_string(static_cast(this->relatedPlayer));
+ std::vector weeklyLeaderboard;
+ for (const auto& entry : leaderboard) {
+ if (epochTime - entry.lastPlayedTimestamp < SECONDS_IN_A_WEEK) {
+ weeklyLeaderboard.push_back(entry);
+ }
}
- baseLookup += " LIMIT 1";
- LOG_DEBUG("query is %s", baseLookup.c_str());
- std::unique_ptr baseQuery(Database::Get()->CreatePreppedStmt(baseLookup));
- baseQuery->setInt(1, this->gameID);
- std::unique_ptr baseResult(baseQuery->executeQuery());
- if (!baseResult->next()) return; // In this case, there are no entries in the leaderboard for this game.
+ return weeklyLeaderboard;
+}
- uint32_t relatedPlayerLeaderboardId = baseResult->getInt("id");
-
- // Create and execute the actual save here. Using a heap allocated buffer to avoid stack overflow
- constexpr uint16_t STRING_LENGTH = 4096;
- std::unique_ptr lookupBuffer = std::make_unique(STRING_LENGTH);
- int32_t res = snprintf(lookupBuffer.get(), STRING_LENGTH, queryBase.c_str(), orderBase.data(), filter.c_str(), resultStart, resultEnd);
- DluAssert(res != -1);
- std::unique_ptr query(Database::Get()->CreatePreppedStmt(lookupBuffer.get()));
- LOG_DEBUG("Query is %s vars are %i %i %i", lookupBuffer.get(), this->gameID, this->relatedPlayer, relatedPlayerLeaderboardId);
- query->setInt(1, this->gameID);
- if (this->infoType == InfoType::Friends) {
- query->setInt(2, this->relatedPlayer);
- query->setInt(3, this->relatedPlayer);
- query->setInt(4, this->relatedPlayer);
- query->setInt(5, relatedPlayerLeaderboardId);
- } else {
- query->setInt(2, relatedPlayerLeaderboardId);
+std::vector FilterFriends(const std::vector& leaderboard, const uint32_t relatedPlayer) {
+ // Filter the leaderboard to only include friends of the player
+ auto friendOfPlayer = Database::Get()->GetFriendsList(relatedPlayer);
+ std::vector friendsLeaderboard;
+ for (const auto& entry : leaderboard) {
+ const auto res = std::ranges::find_if(friendOfPlayer, [&entry, relatedPlayer](const FriendData& data) {
+ return entry.charId == data.friendID || entry.charId == relatedPlayer;
+ });
+ if (res != friendOfPlayer.cend()) {
+ friendsLeaderboard.push_back(entry);
+ }
}
- std::unique_ptr result(query->executeQuery());
- QueryToLdf(result);
+
+ return friendsLeaderboard;
+}
+
+std::vector ProcessLeaderboard(
+ const std::vector& leaderboard,
+ const bool weekly,
+ const Leaderboard::InfoType infoType,
+ const uint32_t relatedPlayer) {
+ std::vector toReturn;
+
+ if (infoType == Leaderboard::InfoType::Friends) {
+ const auto friendsLeaderboard = FilterFriends(leaderboard, relatedPlayer);
+ toReturn = FilterTo10(weekly ? FilterWeeklies(friendsLeaderboard) : friendsLeaderboard, relatedPlayer, infoType);
+ } else {
+ toReturn = FilterTo10(weekly ? FilterWeeklies(leaderboard) : leaderboard, relatedPlayer, infoType);
+ }
+
+ return toReturn;
+}
+
+void Leaderboard::SetupLeaderboard(bool weekly) {
+ const auto leaderboardType = LeaderboardManager::GetLeaderboardType(gameID);
+ std::vector leaderboardRes;
+
+ switch (leaderboardType) {
+ case Type::SurvivalNS:
+ leaderboardRes = Database::Get()->GetNsLeaderboard(gameID);
+ break;
+ case Type::Survival:
+ leaderboardRes = Database::Get()->GetAgsLeaderboard(gameID);
+ break;
+ case Type::Racing:
+ [[fallthrough]];
+ case Type::MonumentRace:
+ leaderboardRes = Database::Get()->GetAscendingLeaderboard(gameID);
+ break;
+ case Type::ShootingGallery:
+ [[fallthrough]];
+ case Type::FootRace:
+ [[fallthrough]];
+ case Type::Donations:
+ [[fallthrough]];
+ case Type::None:
+ [[fallthrough]];
+ default:
+ leaderboardRes = Database::Get()->GetDescendingLeaderboard(gameID);
+ break;
+ }
+
+ const auto processedLeaderboard = ProcessLeaderboard(leaderboardRes, weekly, infoType, relatedPlayer);
+
+ QueryToLdf(*this, processedLeaderboard);
}
void Leaderboard::Send(const LWOOBJID targetID) const {
@@ -272,129 +267,43 @@ void Leaderboard::Send(const LWOOBJID targetID) const {
}
}
-std::string FormatInsert(const Leaderboard::Type& type, const Score& score, const bool useUpdate) {
- std::string insertStatement;
- if (useUpdate) {
- insertStatement =
- R"QUERY(
- UPDATE leaderboard
- SET primaryScore = %f, secondaryScore = %f, tertiaryScore = %f,
- timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;
- )QUERY";
- } else {
- insertStatement =
- R"QUERY(
- INSERT leaderboard SET
- primaryScore = %f, secondaryScore = %f, tertiaryScore = %f,
- character_id = ?, game_id = ?;
- )QUERY";
- }
-
- constexpr uint16_t STRING_LENGTH = 400;
- // Then fill in our score
- char finishedQuery[STRING_LENGTH];
- int32_t res = snprintf(finishedQuery, STRING_LENGTH, insertStatement.c_str(), score.GetPrimaryScore(), score.GetSecondaryScore(), score.GetTertiaryScore());
- DluAssert(res != -1);
- return finishedQuery;
-}
-
void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore, const float tertiaryScore) {
const Leaderboard::Type leaderboardType = GetLeaderboardType(activityId);
- std::unique_ptr query(Database::Get()->CreatePreppedStmt("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;"));
- query->setInt(1, playerID);
- query->setInt(2, activityId);
- std::unique_ptr myScoreResult(query->executeQuery());
+ const auto oldScore = Database::Get()->GetPlayerScore(playerID, activityId);
- std::string saveQuery("UPDATE leaderboard SET timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;");
- Score newScore(primaryScore, secondaryScore, tertiaryScore);
- if (myScoreResult->next()) {
- Score oldScore;
- bool lowerScoreBetter = false;
- switch (leaderboardType) {
- // Higher score better
- case Leaderboard::Type::ShootingGallery: {
- oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
- oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
- oldScore.SetTertiaryScore(myScoreResult->getInt("tertiaryScore"));
- break;
- }
- case Leaderboard::Type::FootRace: {
- oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
- break;
- }
- case Leaderboard::Type::Survival: {
- oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
- oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
- break;
- }
- case Leaderboard::Type::SurvivalNS: {
- oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
- oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
- break;
- }
- case Leaderboard::Type::UnusedLeaderboard4:
- case Leaderboard::Type::Donations: {
- oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
- newScore.SetPrimaryScore(oldScore.GetPrimaryScore() + newScore.GetPrimaryScore());
- break;
- }
- case Leaderboard::Type::Racing: {
- oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
- oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
-
- // For wins we dont care about the score, just the time, so zero out the tertiary.
- // Wins are updated later.
- oldScore.SetTertiaryScore(0);
- newScore.SetTertiaryScore(0);
- lowerScoreBetter = true;
- break;
- }
- case Leaderboard::Type::MonumentRace: {
- oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
- lowerScoreBetter = true;
- // Do score checking here
- break;
- }
- case Leaderboard::Type::None:
- default:
- LOG("Unknown leaderboard type %i for game %i. Cannot save score!", leaderboardType, activityId);
- return;
- }
+ ILeaderboard::Score newScore{ .primaryScore = primaryScore, .secondaryScore = secondaryScore, .tertiaryScore = tertiaryScore };
+ if (oldScore.has_value()) {
+ bool lowerScoreBetter = leaderboardType == Leaderboard::Type::Racing || leaderboardType == Leaderboard::Type::MonumentRace;
bool newHighScore = lowerScoreBetter ? newScore < oldScore : newScore > oldScore;
// Nimbus station has a weird leaderboard where we need a custom scoring system
if (leaderboardType == Leaderboard::Type::SurvivalNS) {
- newHighScore = newScore.GetPrimaryScore() > oldScore.GetPrimaryScore() ||
- (newScore.GetPrimaryScore() == oldScore.GetPrimaryScore() && newScore.GetSecondaryScore() < oldScore.GetSecondaryScore());
+ newHighScore = newScore.primaryScore > oldScore->primaryScore ||
+ (newScore.primaryScore == oldScore->primaryScore && newScore.secondaryScore < oldScore->secondaryScore);
} else if (leaderboardType == Leaderboard::Type::Survival && Game::config->GetValue("classic_survival_scoring") == "1") {
- Score oldScoreFlipped(oldScore.GetSecondaryScore(), oldScore.GetPrimaryScore());
- Score newScoreFlipped(newScore.GetSecondaryScore(), newScore.GetPrimaryScore());
+ ILeaderboard::Score oldScoreFlipped{oldScore->secondaryScore, oldScore->primaryScore, oldScore->tertiaryScore};
+ ILeaderboard::Score newScoreFlipped{newScore.secondaryScore, newScore.primaryScore, newScore.tertiaryScore};
newHighScore = newScoreFlipped > oldScoreFlipped;
}
+
if (newHighScore) {
- saveQuery = FormatInsert(leaderboardType, newScore, true);
+ Database::Get()->UpdateScore(playerID, activityId, newScore);
+ } else {
+ Database::Get()->IncrementTimesPlayed(playerID, activityId);
}
} else {
- saveQuery = FormatInsert(leaderboardType, newScore, false);
+ Database::Get()->SaveScore(playerID, activityId, newScore);
}
- LOG("save query %s %i %i", saveQuery.c_str(), playerID, activityId);
- std::unique_ptr saveStatement(Database::Get()->CreatePreppedStmt(saveQuery));
- saveStatement->setInt(1, playerID);
- saveStatement->setInt(2, activityId);
- saveStatement->execute();
// track wins separately
if (leaderboardType == Leaderboard::Type::Racing && tertiaryScore != 0.0f) {
- std::unique_ptr winUpdate(Database::Get()->CreatePreppedStmt("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;"));
- winUpdate->setInt(1, playerID);
- winUpdate->setInt(2, activityId);
- winUpdate->execute();
+ Database::Get()->IncrementNumWins(playerID, activityId);
}
}
-void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t resultStart, const uint32_t resultEnd) {
+void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID) {
Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID));
- leaderboard.SetupLeaderboard(weekly, resultStart, resultEnd);
+ leaderboard.SetupLeaderboard(weekly);
leaderboard.Send(targetID);
}
diff --git a/dGame/LeaderboardManager.h b/dGame/LeaderboardManager.h
index 527ae02d..af879573 100644
--- a/dGame/LeaderboardManager.h
+++ b/dGame/LeaderboardManager.h
@@ -9,46 +9,10 @@
#include "dCommonVars.h"
#include "LDFFormat.h"
-namespace sql {
- class ResultSet;
-};
-
namespace RakNet {
class BitStream;
};
-class Score {
-public:
- Score() {
- primaryScore = 0;
- secondaryScore = 0;
- tertiaryScore = 0;
- }
- Score(const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0) {
- this->primaryScore = primaryScore;
- this->secondaryScore = secondaryScore;
- this->tertiaryScore = tertiaryScore;
- }
- bool operator<(const Score& rhs) const {
- return primaryScore < rhs.primaryScore || (primaryScore == rhs.primaryScore && secondaryScore < rhs.secondaryScore) || (primaryScore == rhs.primaryScore && secondaryScore == rhs.secondaryScore && tertiaryScore < rhs.tertiaryScore);
- }
- bool operator>(const Score& rhs) const {
- return primaryScore > rhs.primaryScore || (primaryScore == rhs.primaryScore && secondaryScore > rhs.secondaryScore) || (primaryScore == rhs.primaryScore && secondaryScore == rhs.secondaryScore && tertiaryScore > rhs.tertiaryScore);
- }
- void SetPrimaryScore(const float score) { primaryScore = score; }
- float GetPrimaryScore() const { return primaryScore; }
-
- void SetSecondaryScore(const float score) { secondaryScore = score; }
- float GetSecondaryScore() const { return secondaryScore; }
-
- void SetTertiaryScore(const float score) { tertiaryScore = score; }
- float GetTertiaryScore() const { return tertiaryScore; }
-private:
- float primaryScore;
- float secondaryScore;
- float tertiaryScore;
-};
-
using GameID = uint32_t;
class Leaderboard {
@@ -79,7 +43,7 @@ public:
/**
* @brief Resets the leaderboard state and frees its allocated memory
- *
+ *
*/
void Clear();
@@ -96,20 +60,16 @@ public:
* @param resultStart The index to start the leaderboard at. Zero indexed.
* @param resultEnd The index to end the leaderboard at. Zero indexed.
*/
- void SetupLeaderboard(bool weekly, uint32_t resultStart = 0, uint32_t resultEnd = 10);
+ void SetupLeaderboard(bool weekly);
/**
* Sends the leaderboard to the client specified by targetID.
*/
void Send(const LWOOBJID targetID) const;
- // Helper function to get the columns, ordering and insert format for a leaderboard
- static const std::string_view GetOrdering(Type leaderboardType);
-private:
- // Takes the resulting query from a leaderboard lookup and converts it to the LDF we need
- // to send it to a client.
- void QueryToLdf(std::unique_ptr& rows);
+
+private:
using LeaderboardEntry = std::vector;
using LeaderboardEntries = std::vector;
@@ -119,10 +79,18 @@ private:
InfoType infoType;
Leaderboard::Type leaderboardType;
bool weekly;
+public:
+ LeaderboardEntry& PushBackEntry() {
+ return entries.emplace_back();
+ }
+
+ Type GetLeaderboardType() const {
+ return leaderboardType;
+ }
};
namespace LeaderboardManager {
- void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t resultStart = 0, const uint32_t resultEnd = 10);
+ void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID);
void SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0);
diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp
index bfb0bbfa..fbe5a382 100644
--- a/dGame/dComponents/BaseCombatAIComponent.cpp
+++ b/dGame/dComponents/BaseCombatAIComponent.cpp
@@ -16,6 +16,7 @@
#include "DestroyableComponent.h"
#include
+#include
#include
#include