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..fc076dff 100644
--- a/README.md
+++ b/README.md
@@ -13,21 +13,33 @@ Darkflame Universe is licensed under AGPLv3, please read [LICENSE](LICENSE). Som
* You must disclose any changes you make to the code when you distribute it
* Hosting a server for others counts as distribution
-## Disclaimers
-### Setup difficulty
-Throughout the entire build and setup process a level of familiarity with the command line and preferably a Unix-like development environment is greatly advantageous.
-
### Hosting a server
We do not recommend hosting public servers. Darkflame Universe is intended for small scale deployment, for example within a group of friends. It has not been tested for large scale deployment which comes with additional security risks.
### Supply of resource files
Darkflame Universe is a server emulator and does not distribute any LEGO® Universe files. A separate game client is required to setup this server emulator and play the game, which we cannot supply. Users are strongly suggested to refer to the safe checksums listed [here](#verifying-your-client-files) to see if a client will work.
-## Step by step walkthrough for a single-player server
-If you would like a setup for a single player server only on a Windows machine, use the [Native Windows Setup Guide by HailStorm](https://gist.github.com/HailStorm32/169df65a47a104199b5cc57d10fa57de) and skip this README.
+## Setting up a single player server
+* If you don't know what WSL is, skip this warning.
+ Warning: WSL version 1 does NOT support using sqlite as a database due to how it handles filesystem synchronization.
+ You must use Version 2 if you must run the server under WSL. Not doing so will result in save data loss.
+* Single player installs now no longer require building the server from source or installing development tools.
+* Download the [latest windows release](https://github.com/DarkflameUniverse/DarkflameServer/releases) (or whichever release you need) and extract the files into a folder inside your client. Note that this setup is expecting that when double clicking the folder that you put in the same folder as `legouniverse.exe`, the file `MasterServer.exe` is in there.
+* You should be able to see the folder with the server files in the same folder as `legouniverse.exe`.
+* Go into the server files folder and open `sharedconfig.ini`. Find the line that says `client_location` and put `..` after it so the line reads `client_location=..`.
+* To run the server, double-click `MasterServer.exe`.
+* You will be asked to create an account the first time you run the server. After you have created the account, the server will shutdown and need to be restarted.
+* To connect to the server, either delete the file `boot.cfg` which is found in your LEGO Universe client, rename the file `boot.cfg` to something else or follow the steps [here](#allowing-a-user-to-connect-to-your-server) if you wish to keep the file.
+* When shutting down the server, it is highly recommended to click the `MasterServer.exe` window and hold `ctrl` while pressing `c` to stop the server.
+* We are working on a way to make it so when you close the game, the server stops automatically alongside when you open the game, the server starts automatically.
-## Steps to setup server
+**If you are not planning on hosting a server for others, working in the codebase or wanting to use MariaDB for a database, you can stop reading here.**
+
+If you would like to use a MariaDB as a database instead of the default of sqlite, follow the steps [here](#database-setup).
+
+# Steps to setup a development environment
* [Clone this repository](#clone-the-repository)
+* [Setting up a development environment](#setting-up-a-development-environment)
* [Install dependencies](#install-dependencies)
* [Database setup](#database-setup)
* [Build the server](#build-the-server)
@@ -39,6 +51,13 @@ If you would like a setup for a single player server only on a Windows machine,
* [User Guide](#user-guide)
* [Docker](#docker)
+## Disclaimers
+### Setup difficulty
+Throughout the entire build and setup process a level of familiarity with the command line and preferably a Unix-like development environment is greatly advantageous.
+
+## Step by step walkthrough for building a single-player Windows server from source
+If you would like a setup for a single player server only on a Windows machine built from source, use the [Native Windows Setup Guide by HailStorm](https://gist.github.com/HailStorm32/169df65a47a104199b5cc57d10fa57de) and skip this README.
+
## Clone the repository
If you are on Windows, you will need to download and install git from [here](https://git-scm.com/download/win)
@@ -266,8 +285,8 @@ systemctl stop darkflame.service
journalctl -xeu darkflame.service
```
-### First admin user
-Run `MasterServer -a` to get prompted to create an admin account. This method is only intended for the system administrator as a means to get started, do NOT use this method to create accounts for other users!
+### First user or adding more users.
+The first time you run `MasterServer`, you will be prompted to create an account. To create more accounts from the command line, `MasterServer -a` to get prompted to create an admin account. This method is only intended for the system administrator as a means to get started, do NOT use this method to create accounts for other users!
### Account management tool (Nexus Dashboard)
**If you are just using this server for yourself, you can skip setting up Nexus Dashboard**
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/Entity.cpp b/dGame/Entity.cpp
index 59f6e0e0..f5887996 100644
--- a/dGame/Entity.cpp
+++ b/dGame/Entity.cpp
@@ -97,6 +97,8 @@
#include "CDSkillBehaviorTable.h"
#include "CDZoneTableTable.h"
+#include
+
Observable Entity::OnPlayerPositionUpdate;
Entity::Entity(const LWOOBJID& objectID, EntityInfo info, User* parentUser, Entity* parentEntity) {
@@ -286,8 +288,9 @@ void Entity::Initialize() {
AddComponent(propertyEntranceComponentID);
}
- if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::CONTROLLABLE_PHYSICS) > 0) {
- auto* controllablePhysics = AddComponent();
+ const int32_t controllablePhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::CONTROLLABLE_PHYSICS);
+ if (controllablePhysicsComponentID > 0) {
+ auto* controllablePhysics = AddComponent(controllablePhysicsComponentID);
if (m_Character) {
controllablePhysics->LoadFromXml(m_Character->GetXMLDoc());
@@ -330,16 +333,19 @@ void Entity::Initialize() {
AddComponent(simplePhysicsComponentID);
}
- if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS) > 0) {
- AddComponent();
+ const int32_t rigidBodyPhantomPhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS);
+ if (rigidBodyPhantomPhysicsComponentID > 0) {
+ AddComponent(rigidBodyPhantomPhysicsComponentID);
}
- if (markedAsPhantom || compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::PHANTOM_PHYSICS) > 0) {
- AddComponent()->SetPhysicsEffectActive(false);
+ const int32_t phantomPhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::PHANTOM_PHYSICS);
+ if (markedAsPhantom || phantomPhysicsComponentID > 0) {
+ AddComponent(phantomPhysicsComponentID)->SetPhysicsEffectActive(false);
}
- if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::HAVOK_VEHICLE_PHYSICS) > 0) {
- auto* havokVehiclePhysicsComponent = AddComponent();
+ const int32_t havokVehiclePhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::HAVOK_VEHICLE_PHYSICS);
+ if (havokVehiclePhysicsComponentID > 0) {
+ auto* havokVehiclePhysicsComponent = AddComponent(havokVehiclePhysicsComponentID);
havokVehiclePhysicsComponent->SetPosition(m_DefaultPosition);
havokVehiclePhysicsComponent->SetRotation(m_DefaultRotation);
}
@@ -2161,7 +2167,19 @@ void Entity::SetRespawnPos(const NiPoint3& position) {
auto* characterComponent = GetComponent();
if (characterComponent) characterComponent->SetRespawnPos(position);
}
+
void Entity::SetRespawnRot(const NiQuaternion& rotation) {
auto* characterComponent = GetComponent();
if (characterComponent) characterComponent->SetRespawnRot(rotation);
}
+
+int32_t Entity::GetCollisionGroup() const {
+ for (const auto* component : m_Components | std::views::values) {
+ auto* compToCheck = dynamic_cast(component);
+ if (compToCheck) {
+ return compToCheck->GetCollisionGroup();
+ }
+ }
+
+ return 0;
+}
diff --git a/dGame/Entity.h b/dGame/Entity.h
index 5d2b9527..2ed7aa53 100644
--- a/dGame/Entity.h
+++ b/dGame/Entity.h
@@ -107,6 +107,11 @@ public:
const SystemAddress& GetSystemAddress() const;
+ // Returns the collision group for this entity.
+ // Because the collision group is stored on a base component, this will look for a physics component
+ // then return the collision group from that.
+ int32_t GetCollisionGroup() const;
+
/**
* Setters
*/
diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp
index bfb0bbfa..fbe5a382 100644
--- a/dGame/dComponents/BaseCombatAIComponent.cpp
+++ b/dGame/dComponents/BaseCombatAIComponent.cpp
@@ -16,6 +16,7 @@
#include "DestroyableComponent.h"
#include
+#include
#include
#include
@@ -27,7 +28,7 @@
#include "CDPhysicsComponentTable.h"
#include "dNavMesh.h"
-BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id): Component(parent) {
+BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id) : Component(parent) {
m_Target = LWOOBJID_EMPTY;
m_DirtyStateOrTarget = true;
m_State = AiState::spawn;
@@ -37,6 +38,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id):
m_Disabled = false;
m_SkillEntries = {};
m_SoftTimer = 5.0f;
+ m_ForcedTetherTime = 0.0f;
//Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
@@ -170,6 +172,17 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
GameMessages::SendStopFXEffect(m_Parent, true, "tether");
m_TetherEffectActive = false;
}
+ m_ForcedTetherTime -= deltaTime;
+ if (m_ForcedTetherTime >= 0) return;
+ }
+
+ for (auto entry = m_RemovedThreatList.begin(); entry != m_RemovedThreatList.end();) {
+ entry->second -= deltaTime;
+ if (entry->second <= 0.0f) {
+ entry = m_RemovedThreatList.erase(entry);
+ } else {
+ ++entry;
+ }
}
if (m_SoftTimer <= 0.0f) {
@@ -287,40 +300,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
}
if (!m_TetherEffectActive && m_OutOfCombat && (m_OutOfCombatTime -= deltaTime) <= 0) {
- auto* destroyableComponent = m_Parent->GetComponent();
-
- if (destroyableComponent != nullptr && destroyableComponent->HasFaction(4)) {
- auto serilizationRequired = false;
-
- if (destroyableComponent->GetHealth() != destroyableComponent->GetMaxHealth()) {
- destroyableComponent->SetHealth(destroyableComponent->GetMaxHealth());
-
- serilizationRequired = true;
- }
-
- if (destroyableComponent->GetArmor() != destroyableComponent->GetMaxArmor()) {
- destroyableComponent->SetArmor(destroyableComponent->GetMaxArmor());
-
- serilizationRequired = true;
- }
-
- if (serilizationRequired) {
- Game::entityManager->SerializeEntity(m_Parent);
- }
-
- GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), 6270, u"tether", "tether");
-
- m_TetherEffectActive = true;
-
- m_TetherTime = 3.0f;
- }
-
- // Speed towards start position
- if (m_MovementAI != nullptr) {
- m_MovementAI->SetHaltDistance(0);
- m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
- m_MovementAI->SetDestination(m_StartPosition);
- }
+ TetherLogic();
m_OutOfCombat = false;
m_OutOfCombatTime = 0.0f;
@@ -499,7 +479,7 @@ std::vector BaseCombatAIComponent::GetTargetWithinAggroRange() const {
const auto distance = Vector3::DistanceSquared(m_Parent->GetPosition(), other->GetPosition());
- if (distance > m_AggroRadius * m_AggroRadius) continue;
+ if (distance > m_AggroRadius * m_AggroRadius || m_RemovedThreatList.contains(id)) continue;
targets.push_back(id);
}
@@ -626,6 +606,7 @@ const NiPoint3& BaseCombatAIComponent::GetStartPosition() const {
void BaseCombatAIComponent::ClearThreat() {
m_ThreatEntries.clear();
+ m_Target = LWOOBJID_EMPTY;
m_DirtyThreat = true;
}
@@ -806,3 +787,55 @@ void BaseCombatAIComponent::Wake() {
m_dpEntity->SetSleeping(false);
m_dpEntityEnemy->SetSleeping(false);
}
+
+void BaseCombatAIComponent::TetherLogic() {
+ auto* destroyableComponent = m_Parent->GetComponent();
+
+ if (destroyableComponent != nullptr && destroyableComponent->HasFaction(4)) {
+ auto serilizationRequired = false;
+
+ if (destroyableComponent->GetHealth() != destroyableComponent->GetMaxHealth()) {
+ destroyableComponent->SetHealth(destroyableComponent->GetMaxHealth());
+
+ serilizationRequired = true;
+ }
+
+ if (destroyableComponent->GetArmor() != destroyableComponent->GetMaxArmor()) {
+ destroyableComponent->SetArmor(destroyableComponent->GetMaxArmor());
+
+ serilizationRequired = true;
+ }
+
+ if (serilizationRequired) {
+ Game::entityManager->SerializeEntity(m_Parent);
+ }
+
+ GameMessages::SendPlayFXEffect(m_Parent->GetObjectID(), 6270, u"tether", "tether");
+
+ m_TetherEffectActive = true;
+
+ m_TetherTime = 3.0f;
+ }
+
+ // Speed towards start position
+ if (m_MovementAI != nullptr) {
+ m_MovementAI->SetHaltDistance(0);
+ m_MovementAI->SetMaxSpeed(m_PursuitSpeed);
+ m_MovementAI->SetDestination(m_StartPosition);
+ }
+}
+
+void BaseCombatAIComponent::ForceTether() {
+ SetTarget(LWOOBJID_EMPTY);
+ m_ThreatEntries.clear();
+ TetherLogic();
+ m_ForcedTetherTime = m_TetherTime;
+
+ SetAiState(AiState::aggro);
+}
+
+void BaseCombatAIComponent::IgnoreThreat(const LWOOBJID threat, const float value) {
+ m_RemovedThreatList[threat] = value;
+ SetThreat(threat, 0.0f);
+ m_Target = LWOOBJID_EMPTY;
+}
diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h
index 89985d64..52adb429 100644
--- a/dGame/dComponents/BaseCombatAIComponent.h
+++ b/dGame/dComponents/BaseCombatAIComponent.h
@@ -224,6 +224,16 @@ public:
*/
void Wake();
+ // Force this entity to tether and ignore all other actions
+ void ForceTether();
+
+ // heals the entity to full health and armor
+ // and tethers them to their spawn point
+ void TetherLogic();
+
+ // Ignore a threat for a certain amount of time
+ void IgnoreThreat(const LWOOBJID target, const float time);
+
private:
/**
* Returns the current target or the target that currently is the largest threat to this entity
@@ -382,6 +392,12 @@ private:
*/
bool m_DirtyStateOrTarget = false;
+ // The amount of time the entity will be forced to tether for
+ float m_ForcedTetherTime = 0.0f;
+
+ // The amount of time a removed threat will be ignored for.
+ std::map m_RemovedThreatList;
+
/**
* Whether the current entity is a mech enemy, needed as mechs tether radius works differently
* @return whether this entity is a mech
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/dGame/dComponents/ControllablePhysicsComponent.cpp b/dGame/dComponents/ControllablePhysicsComponent.cpp
index 18e4b19d..1a05020b 100644
--- a/dGame/dComponents/ControllablePhysicsComponent.cpp
+++ b/dGame/dComponents/ControllablePhysicsComponent.cpp
@@ -15,7 +15,7 @@
#include "LevelProgressionComponent.h"
#include "eStateChangeType.h"
-ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity) : PhysicsComponent(entity) {
+ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity, int32_t componentId) : PhysicsComponent(entity, componentId) {
m_Velocity = {};
m_AngularVelocity = {};
m_InJetpackMode = false;
diff --git a/dGame/dComponents/ControllablePhysicsComponent.h b/dGame/dComponents/ControllablePhysicsComponent.h
index 6309b8fc..31a6bb30 100644
--- a/dGame/dComponents/ControllablePhysicsComponent.h
+++ b/dGame/dComponents/ControllablePhysicsComponent.h
@@ -23,7 +23,7 @@ class ControllablePhysicsComponent : public PhysicsComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::CONTROLLABLE_PHYSICS;
- ControllablePhysicsComponent(Entity* entity);
+ ControllablePhysicsComponent(Entity* entity, int32_t componentId);
~ControllablePhysicsComponent() override;
void Update(float deltaTime) override;
diff --git a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp
index 635830cc..e977f952 100644
--- a/dGame/dComponents/HavokVehiclePhysicsComponent.cpp
+++ b/dGame/dComponents/HavokVehiclePhysicsComponent.cpp
@@ -1,7 +1,7 @@
#include "HavokVehiclePhysicsComponent.h"
#include "EntityManager.h"
-HavokVehiclePhysicsComponent::HavokVehiclePhysicsComponent(Entity* parent) : PhysicsComponent(parent) {
+HavokVehiclePhysicsComponent::HavokVehiclePhysicsComponent(Entity* parent, int32_t componentId) : PhysicsComponent(parent, componentId) {
m_Velocity = NiPoint3Constant::ZERO;
m_AngularVelocity = NiPoint3Constant::ZERO;
m_IsOnGround = true;
diff --git a/dGame/dComponents/HavokVehiclePhysicsComponent.h b/dGame/dComponents/HavokVehiclePhysicsComponent.h
index 83eb82fe..ad6087a7 100644
--- a/dGame/dComponents/HavokVehiclePhysicsComponent.h
+++ b/dGame/dComponents/HavokVehiclePhysicsComponent.h
@@ -13,7 +13,7 @@ class HavokVehiclePhysicsComponent : public PhysicsComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::HAVOK_VEHICLE_PHYSICS;
- HavokVehiclePhysicsComponent(Entity* parentEntity);
+ HavokVehiclePhysicsComponent(Entity* parentEntity, int32_t componentId);
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp
index f108e13c..cf185c43 100644
--- a/dGame/dComponents/InventoryComponent.cpp
+++ b/dGame/dComponents/InventoryComponent.cpp
@@ -31,6 +31,7 @@
#include "eStateChangeType.h"
#include "eUseItemResponse.h"
#include "Mail.h"
+#include "ProximityMonitorComponent.h"
#include "CDComponentsRegistryTable.h"
#include "CDInventoryComponentTable.h"
@@ -829,6 +830,30 @@ void InventoryComponent::EquipItem(Item* item, const bool skipChecks) {
break;
}
+ return;
+ } else if (item->GetLot() == 8092) {
+ // Trying to equip a car
+ const auto proximityObjects = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::PROXIMITY_MONITOR);
+
+ // look for car instancers and check if we are in its setup range
+ for (auto* const entity : proximityObjects) {
+ if (!entity) continue;
+
+ auto* proximityMonitorComponent = entity->GetComponent();
+ if (!proximityMonitorComponent) continue;
+
+ if (proximityMonitorComponent->IsInProximity("Interaction_Distance", m_Parent->GetObjectID())) {
+ // in the range of a car instancer
+ entity->OnUse(m_Parent);
+ GameMessages::UseItemOnClient itemMsg;
+ itemMsg.target = entity->GetObjectID();
+ itemMsg.itemLOT = item->GetLot();
+ itemMsg.itemToUse = item->GetId();
+ itemMsg.playerId = m_Parent->GetObjectID();
+ itemMsg.Send(m_Parent->GetSystemAddress());
+ break;
+ }
+ }
return;
}
diff --git a/dGame/dComponents/PhantomPhysicsComponent.cpp b/dGame/dComponents/PhantomPhysicsComponent.cpp
index 95fed36e..be7fe774 100644
--- a/dGame/dComponents/PhantomPhysicsComponent.cpp
+++ b/dGame/dComponents/PhantomPhysicsComponent.cpp
@@ -27,7 +27,7 @@
#include "dpShapeBox.h"
#include "dpShapeSphere.h"
-PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent) : PhysicsComponent(parent) {
+PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, int32_t componentId) : PhysicsComponent(parent, componentId) {
m_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation();
m_Scale = m_Parent->GetDefaultScale();
diff --git a/dGame/dComponents/PhantomPhysicsComponent.h b/dGame/dComponents/PhantomPhysicsComponent.h
index 89cfb857..cd54587b 100644
--- a/dGame/dComponents/PhantomPhysicsComponent.h
+++ b/dGame/dComponents/PhantomPhysicsComponent.h
@@ -30,7 +30,7 @@ class PhantomPhysicsComponent final : public PhysicsComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::PHANTOM_PHYSICS;
- PhantomPhysicsComponent(Entity* parent);
+ PhantomPhysicsComponent(Entity* parent, int32_t componentId);
~PhantomPhysicsComponent() override;
void Update(float deltaTime) override;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
diff --git a/dGame/dComponents/PhysicsComponent.cpp b/dGame/dComponents/PhysicsComponent.cpp
index 4a250a6a..67fca8d9 100644
--- a/dGame/dComponents/PhysicsComponent.cpp
+++ b/dGame/dComponents/PhysicsComponent.cpp
@@ -14,10 +14,21 @@
#include "EntityInfo.h"
-PhysicsComponent::PhysicsComponent(Entity* parent) : Component(parent) {
+PhysicsComponent::PhysicsComponent(Entity* parent, int32_t componentId) : Component(parent) {
m_Position = NiPoint3Constant::ZERO;
m_Rotation = NiQuaternionConstant::IDENTITY;
m_DirtyPosition = false;
+
+ CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable();
+
+ if (physicsComponentTable) {
+ auto* info = physicsComponentTable->GetByID(componentId);
+ if (info) {
+ m_CollisionGroup = info->collisionGroup;
+ }
+ }
+
+ if (m_Parent->HasVar(u"CollisionGroupID")) m_CollisionGroup = m_Parent->GetVar(u"CollisionGroupID");
}
void PhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
diff --git a/dGame/dComponents/PhysicsComponent.h b/dGame/dComponents/PhysicsComponent.h
index 4bf0828a..41a4b9d1 100644
--- a/dGame/dComponents/PhysicsComponent.h
+++ b/dGame/dComponents/PhysicsComponent.h
@@ -15,7 +15,7 @@ class dpEntity;
class PhysicsComponent : public Component {
public:
- PhysicsComponent(Entity* parent);
+ PhysicsComponent(Entity* parent, int32_t componentId);
virtual ~PhysicsComponent() = default;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
@@ -25,6 +25,9 @@ public:
const NiQuaternion& GetRotation() const { return m_Rotation; }
virtual void SetRotation(const NiQuaternion& rot) { if (m_Rotation == rot) return; m_Rotation = rot; m_DirtyPosition = true; }
+
+ int32_t GetCollisionGroup() const noexcept { return m_CollisionGroup; }
+ void SetCollisionGroup(int32_t group) noexcept { m_CollisionGroup = group; }
protected:
dpEntity* CreatePhysicsEntity(eReplicaComponentType type);
@@ -37,6 +40,8 @@ protected:
NiQuaternion m_Rotation;
bool m_DirtyPosition;
+
+ int32_t m_CollisionGroup{};
};
#endif //!__PHYSICSCOMPONENT__H__
diff --git a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp
index df81aab3..d0f8caeb 100644
--- a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp
+++ b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.cpp
@@ -12,7 +12,7 @@
#include "dpShapeSphere.h"
#include"EntityInfo.h"
-RigidbodyPhantomPhysicsComponent::RigidbodyPhantomPhysicsComponent(Entity* parent) : PhysicsComponent(parent) {
+RigidbodyPhantomPhysicsComponent::RigidbodyPhantomPhysicsComponent(Entity* parent, int32_t componentId) : PhysicsComponent(parent, componentId) {
m_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation();
m_Scale = m_Parent->GetDefaultScale();
diff --git a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h
index 11595ec0..e7ce93d6 100644
--- a/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h
+++ b/dGame/dComponents/RigidbodyPhantomPhysicsComponent.h
@@ -21,7 +21,7 @@ class RigidbodyPhantomPhysicsComponent : public PhysicsComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS;
- RigidbodyPhantomPhysicsComponent(Entity* parent);
+ RigidbodyPhantomPhysicsComponent(Entity* parent, int32_t componentId);
void Update(const float deltaTime) override;
diff --git a/dGame/dComponents/SimplePhysicsComponent.cpp b/dGame/dComponents/SimplePhysicsComponent.cpp
index 6bc2e2bc..825570f2 100644
--- a/dGame/dComponents/SimplePhysicsComponent.cpp
+++ b/dGame/dComponents/SimplePhysicsComponent.cpp
@@ -13,7 +13,7 @@
#include "Entity.h"
-SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, uint32_t componentID) : PhysicsComponent(parent) {
+SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, int32_t componentID) : PhysicsComponent(parent, componentID) {
m_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation();
diff --git a/dGame/dComponents/SimplePhysicsComponent.h b/dGame/dComponents/SimplePhysicsComponent.h
index b4491e12..61362712 100644
--- a/dGame/dComponents/SimplePhysicsComponent.h
+++ b/dGame/dComponents/SimplePhysicsComponent.h
@@ -30,7 +30,7 @@ class SimplePhysicsComponent : public PhysicsComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::SIMPLE_PHYSICS;
- SimplePhysicsComponent(Entity* parent, uint32_t componentID);
+ SimplePhysicsComponent(Entity* parent, int32_t componentID);
~SimplePhysicsComponent() override;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp
index 9e1a004d..daa9bf19 100644
--- a/dGame/dGameMessages/GameMessages.cpp
+++ b/dGame/dGameMessages/GameMessages.cpp
@@ -6375,3 +6375,58 @@ void GameMessages::SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress&
SEND_PACKET;
}
+
+namespace GameMessages {
+ void GameMsg::Send(const SystemAddress& sysAddr) const {
+ CBITSTREAM;
+ CMSGHEADER;
+
+ bitStream.Write(target); // Who this message will be sent to on the (a) client
+ bitStream.Write(msgId); // the ID of this message
+
+ Serialize(bitStream); // write the message data
+
+ // Send to everyone if someone sent unassigned system address, or to one specific client.
+ if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
+ SEND_PACKET_BROADCAST;
+ } else {
+ SEND_PACKET;
+ }
+ }
+
+ void DisplayTooltip::Serialize(RakNet::BitStream& bitStream) const {
+ bitStream.Write(doOrDie);
+ bitStream.Write(noRepeat);
+ bitStream.Write(noRevive);
+ bitStream.Write(isPropertyTooltip);
+ bitStream.Write(show);
+ bitStream.Write(translate);
+ bitStream.Write(time);
+ bitStream.Write(id.size());
+ bitStream.Write(id);
+
+ std::string toWrite;
+ for (const auto* item : localizeParams) {
+ toWrite += item->GetString() + "\n";
+ }
+ if (!toWrite.empty()) toWrite.pop_back();
+ bitStream.Write(toWrite.size());
+ bitStream.Write(GeneralUtils::ASCIIToUTF16(toWrite));
+ if (!toWrite.empty()) bitStream.Write(0x00); // Null Terminator
+
+ bitStream.Write(imageName.size());
+ bitStream.Write(imageName);
+ bitStream.Write(text.size());
+ bitStream.Write(text);
+ }
+
+ void UseItemOnClient::Serialize(RakNet::BitStream& bitStream) const {
+ bitStream.Write(itemLOT);
+ bitStream.Write(itemToUse);
+ bitStream.Write(itemType);
+ bitStream.Write(playerId);
+ bitStream.Write(targetPosition.x);
+ bitStream.Write(targetPosition.y);
+ bitStream.Write(targetPosition.z);
+ }
+}
diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h
index be1fe145..599ce70f 100644
--- a/dGame/dGameMessages/GameMessages.h
+++ b/dGame/dGameMessages/GameMessages.h
@@ -11,6 +11,7 @@
#include "eCyclingMode.h"
#include "eLootSourceType.h"
#include "Brick.h"
+#include "MessageType/Game.h"
class AMFBaseValue;
class Entity;
@@ -20,6 +21,7 @@ class User;
class Leaderboard;
class PropertySelectQueryProperty;
class TradeItem;
+class LDFBaseData;
enum class eAnimationFlags : uint32_t;
@@ -48,6 +50,15 @@ enum class eCameraTargetCyclingMode : int32_t {
};
namespace GameMessages {
+ struct GameMsg {
+ GameMsg(MessageType::Game gmId) : msgId{ gmId } {}
+ virtual ~GameMsg() = default;
+ void Send(const SystemAddress& sysAddr) const;
+ virtual void Serialize(RakNet::BitStream& bitStream) const {}
+ MessageType::Game msgId;
+ LWOOBJID target{ LWOOBJID_EMPTY };
+ };
+
class PropertyDataMessage;
void SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender);
void SendTeleport(const LWOOBJID& objectID, const NiPoint3& pos, const NiQuaternion& rot, const SystemAddress& sysAddr, bool bSetRotation = false);
@@ -700,6 +711,32 @@ namespace GameMessages {
// This is a client gm however its default values are exactly what we need to get around the invisible inventory item issues.
void SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr);
+
+ struct DisplayTooltip : public GameMsg {
+ DisplayTooltip() : GameMsg(MessageType::Game::DISPLAY_TOOLTIP) {}
+ bool doOrDie{};
+ bool noRepeat{};
+ bool noRevive{};
+ bool isPropertyTooltip{};
+ bool show{};
+ bool translate{};
+ int32_t time{};
+ std::u16string id{};
+ std::vector localizeParams{};
+ std::u16string imageName{};
+ std::u16string text{};
+ void Serialize(RakNet::BitStream& bitStream) const override;
+ };
+
+ struct UseItemOnClient : public GameMsg {
+ UseItemOnClient() : GameMsg(MessageType::Game::USE_ITEM_ON_CLIENT) {}
+ LWOOBJID playerId{};
+ LWOOBJID itemToUse{};
+ uint32_t itemType{};
+ LOT itemLOT{};
+ NiPoint3 targetPosition{};
+ void Serialize(RakNet::BitStream& bitStream) const override;
+ };
};
#endif // GAMEMESSAGES_H
diff --git a/dGame/dPropertyBehaviors/ControlBehaviorMessages/MoveToInventoryMessage.h b/dGame/dPropertyBehaviors/ControlBehaviorMessages/MoveToInventoryMessage.h
index e1f88713..afd7a14c 100644
--- a/dGame/dPropertyBehaviors/ControlBehaviorMessages/MoveToInventoryMessage.h
+++ b/dGame/dPropertyBehaviors/ControlBehaviorMessages/MoveToInventoryMessage.h
@@ -8,7 +8,6 @@ class AMFArrayValue;
/**
* @brief Sent when a player moves a Behavior A at position B to their inventory.
*/
-#pragma warning("This Control Behavior Message does not have a test yet. Non-developers can ignore this warning.")
class MoveToInventoryMessage : public BehaviorMessageBase {
public:
MoveToInventoryMessage(const AMFArrayValue& arguments);
diff --git a/dGame/dUtilities/Preconditions.cpp b/dGame/dUtilities/Preconditions.cpp
index a0aac27e..118d9037 100644
--- a/dGame/dUtilities/Preconditions.cpp
+++ b/dGame/dUtilities/Preconditions.cpp
@@ -137,7 +137,7 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat
return inventoryComponent->GetLotCount(value) >= count;
case PreconditionType::DoesNotHaveItem:
- return inventoryComponent->IsEquipped(value) < count;
+ return inventoryComponent->IsEquipped(value) && count > 0;
case PreconditionType::HasAchievement:
if (missionComponent == nullptr) return false;
return missionComponent->GetMissionState(value) >= eMissionState::COMPLETE;
diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp
index c0966897..becdcdd4 100644
--- a/dGame/dUtilities/SlashCommandHandler.cpp
+++ b/dGame/dUtilities/SlashCommandHandler.cpp
@@ -287,8 +287,8 @@ void SlashCommandHandler::Startup() {
RegisterCommand(SpawnPhysicsVertsCommand);
Command TeleportCommand{
- .help = "Teleports you",
- .info = "Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z)",
+ .help = "Teleports you to a position or a player to another player.",
+ .info = "Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player.",
.aliases = { "teleport", "tele", "tp" },
.handle = DEVGMCommands::Teleport,
.requiredLevel = eGameMasterLevel::JUNIOR_DEVELOPER
@@ -1056,6 +1056,15 @@ void SlashCommandHandler::Startup() {
};
RegisterCommand(InstanceInfoCommand);
+ Command ServerUptimeCommand{
+ .help = "Display the time the current world server has been active",
+ .info = "Display the time the current world server has been active",
+ .aliases = { "uptime" },
+ .handle = GMZeroCommands::ServerUptime,
+ .requiredLevel = eGameMasterLevel::DEVELOPER
+ };
+ RegisterCommand(ServerUptimeCommand);
+
//Commands that are handled by the client
Command faqCommand{
diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp
index 37fba911..43f46746 100644
--- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp
+++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp
@@ -555,25 +555,45 @@ namespace DEVGMCommands {
}
}
+ std::optional ParseRelativeAxis(const float sourcePos, const std::string& toParse) {
+ if (toParse.empty()) return std::nullopt;
+
+ // relative offset from current position
+ if (toParse[0] == '~') {
+ if (toParse.size() == 1) return sourcePos;
+
+ if (toParse.size() < 3 || !(toParse[1] != '+' || toParse[1] != '-')) return std::nullopt;
+
+ const auto offset = GeneralUtils::TryParse(toParse.substr(2));
+ if (!offset.has_value()) return std::nullopt;
+
+ bool isNegative = toParse[1] == '-';
+ return isNegative ? sourcePos - offset.value() : sourcePos + offset.value();
+ }
+
+ return GeneralUtils::TryParse(toParse);
+ }
+
void Teleport(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
+ const auto& sourcePos = entity->GetPosition();
NiPoint3 pos{};
+ auto* sourceEntity = entity;
if (splitArgs.size() == 3) {
-
- const auto x = GeneralUtils::TryParse(splitArgs.at(0));
+ const auto x = ParseRelativeAxis(sourcePos.x, splitArgs[0]);
if (!x) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid x.");
return;
}
- const auto y = GeneralUtils::TryParse(splitArgs.at(1));
+ const auto y = ParseRelativeAxis(sourcePos.y, splitArgs[1]);
if (!y) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid y.");
return;
}
- const auto z = GeneralUtils::TryParse(splitArgs.at(2));
+ const auto z = ParseRelativeAxis(sourcePos.z, splitArgs[2]);
if (!z) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid z.");
return;
@@ -584,32 +604,39 @@ namespace DEVGMCommands {
pos.SetZ(z.value());
LOG("Teleporting objectID: %llu to %f, %f, %f", entity->GetObjectID(), pos.x, pos.y, pos.z);
- GameMessages::SendTeleport(entity->GetObjectID(), pos, NiQuaternion(), sysAddr);
} else if (splitArgs.size() == 2) {
+ const auto x = ParseRelativeAxis(sourcePos.x, splitArgs[0]);
+ auto* sourcePlayer = PlayerManager::GetPlayer(splitArgs[0]);
+ if (!x && !sourcePlayer) {
+ ChatPackets::SendSystemMessage(sysAddr, u"Invalid x or source player not found.");
+ return;
+ }
+ if (sourcePlayer) sourceEntity = sourcePlayer;
- const auto x = GeneralUtils::TryParse(splitArgs.at(0));
- if (!x) {
- ChatPackets::SendSystemMessage(sysAddr, u"Invalid x.");
+ const auto z = ParseRelativeAxis(sourcePos.z, splitArgs[1]);
+ const auto* const targetPlayer = PlayerManager::GetPlayer(splitArgs[1]);
+ if (!z && !targetPlayer) {
+ ChatPackets::SendSystemMessage(sysAddr, u"Invalid z or target player not found.");
return;
}
- const auto z = GeneralUtils::TryParse(splitArgs.at(1));
- if (!z) {
- ChatPackets::SendSystemMessage(sysAddr, u"Invalid z.");
+ if (x && z) {
+ pos.SetX(x.value());
+ pos.SetY(0.0f);
+ pos.SetZ(z.value());
+ } else if (sourcePlayer && targetPlayer) {
+ pos = targetPlayer->GetPosition();
+ } else {
+ ChatPackets::SendSystemMessage(sysAddr, u"Unable to teleport.");
return;
}
-
- pos.SetX(x.value());
- pos.SetY(0.0f);
- pos.SetZ(z.value());
-
LOG("Teleporting objectID: %llu to X: %f, Z: %f", entity->GetObjectID(), pos.x, pos.z);
- GameMessages::SendTeleport(entity->GetObjectID(), pos, NiQuaternion(), sysAddr);
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /teleport () - if no Y given, will teleport to the height of the terrain (or any physics object).");
}
+ GameMessages::SendTeleport(sourceEntity->GetObjectID(), pos, sourceEntity->GetRotation(), sourceEntity->GetSystemAddress());
- auto* possessorComponent = entity->GetComponent();
+ auto* possessorComponent = sourceEntity->GetComponent();
if (possessorComponent) {
auto* possassableEntity = Game::entityManager->GetEntity(possessorComponent->GetPossessable());
diff --git a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp
index 6c9811c2..51fa6e15 100644
--- a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp
+++ b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp
@@ -225,8 +225,13 @@ namespace GMZeroCommands {
ChatPackets::SendSystemMessage(sysAddr, u"Map: " + (GeneralUtils::to_u16string(zoneId.GetMapID())) + u"\nClone: " + (GeneralUtils::to_u16string(zoneId.GetCloneID())) + u"\nInstance: " + (GeneralUtils::to_u16string(zoneId.GetInstanceID())));
}
+ // Display the server uptime
+ void ServerUptime(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
+ const auto time = Game::server->GetUptime();
+ const auto seconds = std::chrono::duration_cast(time).count();
+ ChatPackets::SendSystemMessage(sysAddr, u"Server has been up for " + GeneralUtils::to_u16string(seconds) + u" s");
+ }
+
//For client side commands
void ClientHandled(Entity* entity, const SystemAddress& sysAddr, const std::string args) {}
-
};
-
diff --git a/dGame/dUtilities/SlashCommands/GMZeroCommands.h b/dGame/dUtilities/SlashCommands/GMZeroCommands.h
index d3f6753d..00824bf8 100644
--- a/dGame/dUtilities/SlashCommands/GMZeroCommands.h
+++ b/dGame/dUtilities/SlashCommands/GMZeroCommands.h
@@ -15,6 +15,7 @@ namespace GMZeroCommands {
void LeaveZone(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void Resurrect(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void InstanceInfo(Entity* entity, const SystemAddress& sysAddr, const std::string args);
+ void ServerUptime(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void ClientHandled(Entity* entity, const SystemAddress& sysAddr, const std::string args);
}
diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp
index 9a54a64e..b764169a 100644
--- a/dMasterServer/MasterServer.cpp
+++ b/dMasterServer/MasterServer.cpp
@@ -126,6 +126,7 @@ int main(int argc, char** argv) {
MigrationRunner::RunMigrations();
const auto resServerPath = BinaryPathFinder::GetBinaryDir() / "resServer";
+ std::filesystem::create_directories(resServerPath);
const bool cdServerExists = std::filesystem::exists(resServerPath / "CDServer.sqlite");
const bool oldCDServerExists = std::filesystem::exists(Game::assetManager->GetResPath() / "CDServer.sqlite");
const bool fdbExists = std::filesystem::exists(Game::assetManager->GetResPath() / "cdclient.fdb");
@@ -176,12 +177,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/dNet/dServer.h b/dNet/dServer.h
index f958fc40..a577d191 100644
--- a/dNet/dServer.h
+++ b/dNet/dServer.h
@@ -1,5 +1,6 @@
#pragma once
#include
+#include
#include
#include "RakPeerInterface.h"
#include "ReplicaManager.h"
@@ -80,6 +81,11 @@ public:
const ServerType GetServerType() const { return mServerType; }
+ [[nodiscard]]
+ std::chrono::steady_clock::duration GetUptime() const {
+ return std::chrono::steady_clock::now() - mStartTime;
+ }
+
private:
bool Startup();
void Shutdown();
@@ -114,4 +120,5 @@ protected:
SystemAddress mMasterSystemAddress;
std::string mMasterIP;
int mMasterPort;
+ std::chrono::steady_clock::time_point mStartTime = std::chrono::steady_clock::now();
};
diff --git a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp
index 515603a0..fc724fb9 100644
--- a/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp
+++ b/dScripts/02_server/Map/AG/NpcAgCourseStarter.cpp
@@ -3,104 +3,106 @@
#include "ScriptedActivityComponent.h"
#include "GameMessages.h"
#include "LeaderboardManager.h"
+#include "dServer.h"
#include "eMissionTaskType.h"
#include "eMissionState.h"
#include "MissionComponent.h"
-#include
+#include
-void NpcAgCourseStarter::OnStartup(Entity* self) {
-
-}
+void NpcAgCourseStarter::OnStartup(Entity* self) {}
void NpcAgCourseStarter::OnUse(Entity* self, Entity* user) {
- auto* scriptedActivityComponent = self->GetComponent();
+ auto* const scriptedActivityComponent = self->GetComponent();
+ if (!scriptedActivityComponent) return;
- if (scriptedActivityComponent == nullptr) {
- return;
- }
+ const auto selfId = self->GetObjectID();
+ const auto userId = user->GetObjectID();
+ const auto& userSysAddr = user->GetSystemAddress();
- if (scriptedActivityComponent->GetActivityPlayerData(user->GetObjectID()) != nullptr) {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"exit", 0, 0, LWOOBJID_EMPTY, "", user->GetSystemAddress());
+ if (scriptedActivityComponent->GetActivityPlayerData(userId) != nullptr) {
+ GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", userSysAddr);
} else {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"start", 0, 0, LWOOBJID_EMPTY, "", user->GetSystemAddress());
+ GameMessages::SendNotifyClientObject(selfId, u"start", 0, 0, LWOOBJID_EMPTY, "", userSysAddr);
}
}
void NpcAgCourseStarter::OnMessageBoxResponse(Entity* self, Entity* sender, int32_t button, const std::u16string& identifier, const std::u16string& userData) {
- auto* scriptedActivityComponent = self->GetComponent();
+ auto* const scriptedActivityComponent = self->GetComponent();
+ if (!scriptedActivityComponent) return;
- if (scriptedActivityComponent == nullptr) {
- return;
- }
+ const auto selfId = self->GetObjectID();
+ const auto senderId = sender->GetObjectID();
+ const auto& senderSysAddr = sender->GetSystemAddress();
if (identifier == u"player_dialog_cancel_course" && button == 1) {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"stop_timer", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress());
+ GameMessages::SendNotifyClientObject(selfId, u"stop_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
+ GameMessages::SendNotifyClientObject(selfId, u"cancel_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"cancel_timer", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress());
-
- scriptedActivityComponent->RemoveActivityPlayerData(sender->GetObjectID());
+ scriptedActivityComponent->RemoveActivityPlayerData(senderId);
Game::entityManager->SerializeEntity(self);
} else if (identifier == u"player_dialog_start_course" && button == 1) {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"start_timer", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress());
-
- GameMessages::SendActivityStart(self->GetObjectID(), sender->GetSystemAddress());
-
- auto* data = scriptedActivityComponent->AddActivityPlayerData(sender->GetObjectID());
+ GameMessages::SendNotifyClientObject(selfId, u"start_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
+ GameMessages::SendActivityStart(selfId, senderSysAddr);
+ auto* const data = scriptedActivityComponent->AddActivityPlayerData(senderId);
if (data->values[1] != 0) return;
- time_t startTime = std::time(0) + 4; // Offset for starting timer
-
- data->values[1] = *reinterpret_cast(&startTime);
+ const auto raceStartTime = Game::server->GetUptime() + std::chrono::seconds(4); // Offset for starting timer
+ const auto fRaceStartTime = std::chrono::duration>(raceStartTime).count();
+ data->values[1] = fRaceStartTime;
Game::entityManager->SerializeEntity(self);
} else if (identifier == u"FootRaceCancel") {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"stop_timer", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress());
+ GameMessages::SendNotifyClientObject(selfId, u"stop_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
- if (scriptedActivityComponent->GetActivityPlayerData(sender->GetObjectID()) != nullptr) {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"exit", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress());
+ if (scriptedActivityComponent->GetActivityPlayerData(senderId) != nullptr) {
+ GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
} else {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"start", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress());
+ GameMessages::SendNotifyClientObject(selfId, u"start", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
}
- scriptedActivityComponent->RemoveActivityPlayerData(sender->GetObjectID());
+ scriptedActivityComponent->RemoveActivityPlayerData(senderId);
}
}
void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) {
- auto* scriptedActivityComponent = self->GetComponent();
- if (scriptedActivityComponent == nullptr) return;
+ auto* const scriptedActivityComponent = self->GetComponent();
+ if (!scriptedActivityComponent) return;
- auto* data = scriptedActivityComponent->GetActivityPlayerData(sender->GetObjectID());
- if (data == nullptr) return;
+ const auto selfId = self->GetObjectID();
+ const auto senderId = sender->GetObjectID();
+ const auto& senderSysAddr = sender->GetSystemAddress();
+
+ auto* const data = scriptedActivityComponent->GetActivityPlayerData(senderId);
+ if (!data) return;
if (args == "course_cancel") {
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"cancel_timer", 0, 0,
- LWOOBJID_EMPTY, "", sender->GetSystemAddress());
- scriptedActivityComponent->RemoveActivityPlayerData(sender->GetObjectID());
+ GameMessages::SendNotifyClientObject(selfId, u"cancel_timer", 0, 0,
+ LWOOBJID_EMPTY, "", senderSysAddr);
+ scriptedActivityComponent->RemoveActivityPlayerData(senderId);
} else if (args == "course_finish") {
- time_t endTime = std::time(0);
- time_t finish = (endTime - *reinterpret_cast(&data->values[1]));
+ const auto raceEndTime = Game::server->GetUptime();
+ const auto fRaceEndTime = std::chrono::duration>(raceEndTime).count();
+ const auto raceTimeElapsed = fRaceEndTime - data->values[1];
+ data->values[2] = raceTimeElapsed;
- data->values[2] = *reinterpret_cast(&finish);
-
- auto* missionComponent = sender->GetComponent();
+ auto* const missionComponent = sender->GetComponent();
if (missionComponent != nullptr) {
missionComponent->ForceProgressTaskType(1884, 1, 1, false);
- missionComponent->Progress(eMissionTaskType::PERFORM_ACTIVITY, -finish, self->GetObjectID(),
+ missionComponent->Progress(eMissionTaskType::PERFORM_ACTIVITY, -raceTimeElapsed, selfId,
"performact_time");
}
Game::entityManager->SerializeEntity(self);
- LeaderboardManager::SaveScore(sender->GetObjectID(), scriptedActivityComponent->GetActivityID(), static_cast(finish));
+ LeaderboardManager::SaveScore(senderId, scriptedActivityComponent->GetActivityID(), raceTimeElapsed);
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard",
- scriptedActivityComponent->GetActivityID(), 0, sender->GetObjectID(),
- "", sender->GetSystemAddress());
- GameMessages::SendNotifyClientObject(self->GetObjectID(), u"stop_timer", 1, finish, LWOOBJID_EMPTY, "",
- sender->GetSystemAddress());
+ GameMessages::SendNotifyClientObject(selfId, u"ToggleLeaderBoard",
+ scriptedActivityComponent->GetActivityID(), 0, senderId,
+ "", senderSysAddr);
+ GameMessages::SendNotifyClientObject(selfId, u"stop_timer", 1, raceTimeElapsed, LWOOBJID_EMPTY, "",
+ senderSysAddr);
- scriptedActivityComponent->RemoveActivityPlayerData(sender->GetObjectID());
+ scriptedActivityComponent->RemoveActivityPlayerData(senderId);
}
}
diff --git a/dScripts/02_server/Map/General/CMakeLists.txt b/dScripts/02_server/Map/General/CMakeLists.txt
index 3379e5b0..dc32d53c 100644
--- a/dScripts/02_server/Map/General/CMakeLists.txt
+++ b/dScripts/02_server/Map/General/CMakeLists.txt
@@ -2,6 +2,7 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_GENERAL
"BankInteractServer.cpp"
"BaseInteractDropLootServer.cpp"
"Binoculars.cpp"
+ "EnemyClearThreat.cpp"
"ExplodingAsset.cpp"
"FrictionVolumeServer.cpp"
"ForceVolumeServer.cpp"
diff --git a/dScripts/02_server/Map/General/EnemyClearThreat.cpp b/dScripts/02_server/Map/General/EnemyClearThreat.cpp
new file mode 100644
index 00000000..b026899c
--- /dev/null
+++ b/dScripts/02_server/Map/General/EnemyClearThreat.cpp
@@ -0,0 +1,25 @@
+#include "EnemyClearThreat.h"
+
+#include "BaseCombatAIComponent.h"
+#include "PhysicsComponent.h"
+
+void EnemyClearThreat::OnCollisionPhantom(Entity* self, Entity* target) {
+ if (!target) return;
+
+ const auto colGroup = target->GetCollisionGroup();
+ if (colGroup == 12) { // enemy
+ auto* const baseCombatAiComponent = target->GetComponent();
+ if (!baseCombatAiComponent) return;
+
+ baseCombatAiComponent->ClearThreat();
+ baseCombatAiComponent->ForceTether();
+ } else if (colGroup == 10) { // player
+ const auto enemies = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::BASE_COMBAT_AI);
+ for (const auto& enemy : enemies) {
+ auto* const baseCombatAiComponent = enemy->GetComponent();
+ if (!baseCombatAiComponent) continue;
+
+ baseCombatAiComponent->IgnoreThreat(target->GetObjectID(), 3.0f);
+ }
+ }
+}
diff --git a/dScripts/02_server/Map/General/EnemyClearThreat.h b/dScripts/02_server/Map/General/EnemyClearThreat.h
new file mode 100644
index 00000000..97cce426
--- /dev/null
+++ b/dScripts/02_server/Map/General/EnemyClearThreat.h
@@ -0,0 +1,11 @@
+#ifndef ENEMYCLEARTHREAT_H
+#define ENEMYCLEARTHREAT_H
+
+#include "CppScripts.h"
+
+class EnemyClearThreat : public CppScripts::Script {
+public:
+ void OnCollisionPhantom(Entity* self, Entity* target) override;
+};
+
+#endif //!ENEMYCLEARTHREAT_H
diff --git a/dScripts/CppScripts.cpp b/dScripts/CppScripts.cpp
index ed0de2ba..8b38e5ce 100644
--- a/dScripts/CppScripts.cpp
+++ b/dScripts/CppScripts.cpp
@@ -327,6 +327,9 @@
#include "VisToggleNotifierServer.h"
#include "LupGenericInteract.h"
#include "WblRobotCitizen.h"
+#include "EnemyClearThreat.h"
+#include "AgSpiderBossMessage.h"
+#include "GfRaceInstancer.h"
#include