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