Merge branch 'main' into ub-fixes

This commit is contained in:
jadebenn 2024-12-20 01:58:51 -06:00 committed by GitHub
commit 6699081080
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
145 changed files with 118707 additions and 42054 deletions

View File

@ -3,12 +3,20 @@ CLIENT_PATH=./client
# Updates NET_VERSION in CMakeVariables.txt # Updates NET_VERSION in CMakeVariables.txt
NET_VERSION=171022 NET_VERSION=171022
# make sure this is a long random string # make sure this is a long random string
# grab a "SHA 256-bit Key" from here: https://keygen.io/ # generate a "SHA 256-bit Key" from here: https://gchq.github.io/CyberChef/#recipe=Pseudo-Random_Number_Generator(256,'Hex')
ACCOUNT_MANAGER_SECRET= ACCOUNT_MANAGER_SECRET=
# Should be the externally facing IP of your server host # Should be the externally facing IP of your server host
EXTERNAL_IP=localhost 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 # Database values
# Be careful with special characters here. It is more safe to use normal characters and/or numbers. # Be careful with special characters here. It is more safe to use normal characters and/or numbers.
MARIADB_USER=darkflame MARIADB_USER=darkflame
MARIADB_PASSWORD= MARIADB_PASSWORD=
MARIADB_DATABASE=darkflame MARIADB_DATABASE=darkflame
SKIP_ACCOUNT_CREATION=1

View File

@ -43,6 +43,7 @@ jobs:
build/*/*.ini build/*/*.ini
build/*/*.so build/*/*.so
build/*/*.dll build/*/*.dll
build/*/*.dylib
build/*/vanity/ build/*/vanity/
build/*/navmeshes/ build/*/navmeshes/
build/*/migrations/ build/*/migrations/

2
.gitignore vendored
View File

@ -7,7 +7,6 @@ valgrind-out.txt
# Third party libraries # Third party libraries
thirdparty/mysql/ thirdparty/mysql/
thirdparty/mysql_linux/ thirdparty/mysql_linux/
CMakeVariables.txt
# Build folders # Build folders
build/ build/
@ -96,6 +95,7 @@ ipch/
# Exceptions: # Exceptions:
CMakeSettings.json CMakeSettings.json
CMakeUserPresets.json
*.vcxproj *.vcxproj
*.filters *.filters
*.cmake *.cmake

View File

@ -78,6 +78,7 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
# Disabled no-register # Disabled no-register
# Disabled unknown pragmas because Linux doesn't understand Windows pragmas. # Disabled unknown pragmas because Linux doesn't understand Windows pragmas.
if(UNIX) if(UNIX)
add_link_options("-Wl,-rpath,$ORIGIN/")
add_compile_options("-fPIC") add_compile_options("-fPIC")
add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0) add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0)
@ -186,6 +187,8 @@ foreach(resource_file ${RESOURCE_FILES})
list(GET line_split 0 variable_name) list(GET line_split 0 variable_name)
if(NOT ${parsed_current_file_contents} MATCHES ${variable_name}) if(NOT ${parsed_current_file_contents} MATCHES ${variable_name})
# 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}) message(STATUS "Adding missing config option " ${variable_name} " to " ${resource_file})
set(line_to_add ${line_to_add} ${line}) set(line_to_add ${line_to_add} ${line})
@ -195,7 +198,7 @@ foreach(resource_file ${RESOURCE_FILES})
file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n") file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n")
endif() endif()
endif()
set(line_to_add "") set(line_to_add "")
else() else()
set(line_to_add ${line_to_add} ${line}) set(line_to_add ${line_to_add} ${line})
@ -225,21 +228,8 @@ foreach(file ${VANITY_FILES})
endforeach() endforeach()
# Move our migrations for MasterServer to run # Move our migrations for MasterServer to run
file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/migrations/dlu/) file(REMOVE_RECURSE ${PROJECT_BINARY_DIR}/migrations)
file(GLOB SQL_FILES ${CMAKE_SOURCE_DIR}/migrations/dlu/*.sql) file(COPY ${CMAKE_SOURCE_DIR}/migrations DESTINATION ${CMAKE_BINARY_DIR})
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()
# Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux) # Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux)
if (APPLE) if (APPLE)
@ -324,7 +314,7 @@ add_subdirectory(dPhysics)
add_subdirectory(dServer) add_subdirectory(dServer)
# Create a list of common libraries shared between all binaries # Create a list of common libraries shared between all binaries
set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "MariaDB::ConnCpp" "magic_enum") set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
# Add platform specific common libraries # Add platform specific common libraries
if(UNIX) if(UNIX)

View File

@ -11,9 +11,6 @@
"displayName": "Default configure step", "displayName": "Default configure step",
"description": "Use 'build' dir and Unix makefiles", "description": "Use 'build' dir and Unix makefiles",
"binaryDir": "${sourceDir}/build", "binaryDir": "${sourceDir}/build",
"environment": {
"DLU_CONFIG_DIR": "${sourceDir}/build"
},
"generator": "Unix Makefiles" "generator": "Unix Makefiles"
}, },
{ {

View File

@ -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 * You must disclose any changes you make to the code when you distribute it
* Hosting a server for others counts as distribution * 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 ### 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. 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 ### 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. 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 ## Setting up 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. * 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 <font size="32">**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.**</font>
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) * [Clone this repository](#clone-the-repository)
* [Setting up a development environment](#setting-up-a-development-environment)
* [Install dependencies](#install-dependencies) * [Install dependencies](#install-dependencies)
* [Database setup](#database-setup) * [Database setup](#database-setup)
* [Build the server](#build-the-server) * [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) * [User Guide](#user-guide)
* [Docker](#docker) * [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 ## Clone the repository
If you are on Windows, you will need to download and install git from [here](https://git-scm.com/download/win) 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 journalctl -xeu darkflame.service
``` ```
### First admin user ### First user or adding more users.
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! 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) ### Account management tool (Nexus Dashboard)
**If you are just using this server for yourself, you can skip setting up Nexus Dashboard** **If you are just using this server for yourself, you can skip setting up Nexus Dashboard**
@ -371,7 +390,7 @@ at once. For that:
- Download the [.env.example](.env.example) file and place it next to `client` with the file name `.env` - Download the [.env.example](.env.example) file and place it next to `client` with the file name `.env`
- You may get warnings that this name starts with a dot, acknowledge those, this is intentional. Depending on your operating system, you may need to activate showing hidden files (e.g. Ctrl-H in Gnome on Linux) and/or file extensions ("File name extensions" in the "View" tab on Windows). - You may get warnings that this name starts with a dot, acknowledge those, this is intentional. Depending on your operating system, you may need to activate showing hidden files (e.g. Ctrl-H in Gnome on Linux) and/or file extensions ("File name extensions" in the "View" tab on Windows).
- Update the `ACCOUNT_MANAGER_SECRET` and `MARIADB_PASSWORD` with strong random passwords. - Update the `ACCOUNT_MANAGER_SECRET` and `MARIADB_PASSWORD` with strong random passwords.
- Use a password generator like <https://keygen.io> - Use a password generator <https://gchq.github.io/CyberChef/#recipe=Pseudo-Random_Number_Generator(256,'Hex')>
- Avoid `:` and `@` characters - Avoid `:` and `@` characters
- Once the database user is created, changing the password will not update it, so the server will just fail to connect. - Once the database user is created, changing the password will not update it, so the server will just fail to connect.
- Set `EXTERNAL_IP` to your LAN IP or public IP if you want to host the game for friends & family - Set `EXTERNAL_IP` to your LAN IP or public IP if you want to host the game for friends & family

View File

@ -60,7 +60,7 @@ int main(int argc, char** argv) {
try { try {
Database::Connect(); Database::Connect();
} catch (sql::SQLException& ex) { } catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what()); LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("AuthServer"); Database::Destroy("AuthServer");
delete Game::server; delete Game::server;

View File

@ -81,7 +81,7 @@ int main(int argc, char** argv) {
//Connect to the MySQL Database //Connect to the MySQL Database
try { try {
Database::Connect(); Database::Connect();
} catch (sql::SQLException& ex) { } catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what()); LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("ChatServer"); Database::Destroy("ChatServer");
delete Game::server; delete Game::server;

View File

@ -123,7 +123,7 @@ uint32_t BrickByBrickFix::UpdateBrickByBrickModelsToSd0() {
Database::Get()->UpdateUgcModelData(model.id, outputStringStream); Database::Get()->UpdateUgcModelData(model.id, outputStringStream);
LOG("Updated model %i to sd0", model.id); LOG("Updated model %i to sd0", model.id);
updatedModels++; updatedModels++;
} catch (sql::SQLException exception) { } catch (std::exception& exception) {
LOG("Failed to update model %i. This model should be inspected manually to see why." LOG("Failed to update model %i. This model should be inspected manually to see why."
"The database error is %s", model.id, exception.what()); "The database error is %s", model.id, exception.what());
} }

View File

@ -37,7 +37,6 @@ target_include_directories(dCommon
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
"${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include"
) )
if (UNIX) if (UNIX)

View File

@ -293,6 +293,7 @@ std::vector<std::string> GeneralUtils::GetSqlFileNamesFromFolder(const std::stri
// Because we dont know how large the initial number before the first _ is we need to make it a map like so. // Because we dont know how large the initial number before the first _ is we need to make it a map like so.
std::map<uint32_t, std::string> filenames{}; std::map<uint32_t, std::string> filenames{};
for (const auto& t : std::filesystem::directory_iterator(folder)) { for (const auto& t : std::filesystem::directory_iterator(folder)) {
if (t.is_directory() || t.is_symlink()) continue;
auto filename = t.path().filename().string(); auto filename = t.path().filename().string();
const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0)); const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0));
filenames.emplace(index, std::move(filename)); filenames.emplace(index, std::move(filename));

View File

@ -2,6 +2,12 @@ add_subdirectory(CDClientDatabase)
add_subdirectory(GameDatabase) add_subdirectory(GameDatabase)
add_library(dDatabase STATIC "MigrationRunner.cpp") add_library(dDatabase STATIC "MigrationRunner.cpp")
add_custom_target(conncpp_dylib
${CMAKE_COMMAND} -E copy $<TARGET_FILE:MariaDB::ConnCpp> ${PROJECT_BINARY_DIR})
add_dependencies(dDatabase conncpp_dylib)
target_include_directories(dDatabase PUBLIC ".") target_include_directories(dDatabase PUBLIC ".")
target_link_libraries(dDatabase target_link_libraries(dDatabase
PUBLIC dDatabaseCDClient dDatabaseGame) PUBLIC dDatabaseCDClient dDatabaseGame)

View File

@ -8,6 +8,12 @@ foreach(file ${DDATABSE_DATABSES_MYSQL_SOURCES})
set(DDATABASE_GAMEDATABASE_SOURCES ${DDATABASE_GAMEDATABASE_SOURCES} "MySQL/${file}") set(DDATABASE_GAMEDATABASE_SOURCES ${DDATABASE_GAMEDATABASE_SOURCES} "MySQL/${file}")
endforeach() endforeach()
add_subdirectory(SQLite)
foreach(file ${DDATABSE_DATABSES_SQLITE_SOURCES})
set(DDATABASE_GAMEDATABASE_SOURCES ${DDATABASE_GAMEDATABASE_SOURCES} "SQLite/${file}")
endforeach()
add_subdirectory(TestSQL) add_subdirectory(TestSQL)
foreach(file ${DDATABSE_DATABSES_TEST_SQL_SOURCES}) foreach(file ${DDATABSE_DATABSES_TEST_SQL_SOURCES})
@ -16,13 +22,14 @@ endforeach()
add_library(dDatabaseGame STATIC ${DDATABASE_GAMEDATABASE_SOURCES}) add_library(dDatabaseGame STATIC ${DDATABASE_GAMEDATABASE_SOURCES})
target_include_directories(dDatabaseGame PUBLIC "." target_include_directories(dDatabaseGame PUBLIC "."
"ITables" PRIVATE "MySQL" "TestSQL" "ITables" PRIVATE "MySQL" "SQLite" "TestSQL"
"${PROJECT_SOURCE_DIR}/dCommon" "${PROJECT_SOURCE_DIR}/dCommon"
"${PROJECT_SOURCE_DIR}/dCommon/dEnums" "${PROJECT_SOURCE_DIR}/dCommon/dEnums"
) )
target_link_libraries(dDatabaseGame target_link_libraries(dDatabaseGame
PUBLIC MariaDB::ConnCpp INTERFACE dCommon
INTERFACE dCommon) PRIVATE sqlite3 MariaDB::ConnCpp)
# Glob together all headers that need to be precompiled # Glob together all headers that need to be precompiled
file( file(

View File

@ -2,22 +2,46 @@
#include "Game.h" #include "Game.h"
#include "dConfig.h" #include "dConfig.h"
#include "Logger.h" #include "Logger.h"
#include "MySQLDatabase.h"
#include "DluAssert.h" #include "DluAssert.h"
#include "SQLiteDatabase.h"
#include "MySQLDatabase.h"
#include <ranges>
#pragma warning (disable:4251) //Disables SQL warnings #pragma warning (disable:4251) //Disables SQL warnings
namespace { namespace {
GameDatabase* database = nullptr; GameDatabase* database = nullptr;
} }
std::string Database::GetMigrationFolder() {
const std::set<std::string> 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() { void Database::Connect() {
if (database) { if (database) {
LOG("Tried to connect to database when it's already connected!"); LOG("Tried to connect to database when it's already connected!");
return; return;
} }
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 = new MySQLDatabase();
}
database->Connect(); database->Connect();
} }

View File

@ -1,7 +1,6 @@
#pragma once #pragma once
#include <string> #include <string>
#include <conncpp.hpp>
#include "GameDatabase.h" #include "GameDatabase.h"
@ -13,4 +12,6 @@ namespace Database {
// Used for assigning a test database as the handler for database logic. // Used for assigning a test database as the handler for database logic.
// Do not use in production code. // Do not use in production code.
void _setDatabase(GameDatabase* const db); void _setDatabase(GameDatabase* const db);
std::string GetMigrationFolder();
}; };

View File

@ -24,14 +24,10 @@
#include "IIgnoreList.h" #include "IIgnoreList.h"
#include "IAccountsRewardCodes.h" #include "IAccountsRewardCodes.h"
#include "IBehaviors.h" #include "IBehaviors.h"
#include "IUgcModularBuild.h"
namespace sql {
class Statement;
class PreparedStatement;
};
#ifdef _DEBUG #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 #else
# define DLU_SQL_TRY_CATCH_RETHROW(x) x # define DLU_SQL_TRY_CATCH_RETHROW(x) x
#endif // _DEBUG #endif // _DEBUG
@ -42,14 +38,13 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml, public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo, public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList, public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors { public IBehaviors, public IUgcModularBuild {
public: public:
virtual ~GameDatabase() = default; virtual ~GameDatabase() = default;
// TODO: These should be made private. // TODO: These should be made private.
virtual void Connect() = 0; virtual void Connect() = 0;
virtual void Destroy(std::string source = "") = 0; virtual void Destroy(std::string source = "") = 0;
virtual void ExecuteCustomQuery(const std::string_view query) = 0; virtual void ExecuteCustomQuery(const std::string_view query) = 0;
virtual sql::PreparedStatement* CreatePreppedStmt(const std::string& query) = 0;
virtual void Commit() = 0; virtual void Commit() = 0;
virtual bool GetAutoCommit() = 0; virtual bool GetAutoCommit() = 0;
virtual void SetAutoCommit(bool value) = 0; virtual void SetAutoCommit(bool value) = 0;

View File

@ -36,6 +36,8 @@ public:
// Update the GameMaster level of an account. // Update the GameMaster level of an account.
virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0; virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0;
virtual uint32_t GetAccountCount() = 0;
}; };
#endif //!__IACCOUNTS__H__ #endif //!__IACCOUNTS__H__

View File

@ -3,12 +3,45 @@
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
#include <string>
#include <vector>
class ILeaderboard { class ILeaderboard {
public: public:
struct Entry {
uint32_t charId{};
uint32_t lastPlayedTimestamp{};
float primaryScore{};
float secondaryScore{};
uint32_t tertiaryScore{};
uint32_t numWins{};
uint32_t numTimesPlayed{};
uint32_t ranking{};
std::string name{};
};
struct Score {
auto operator<=>(const Score& rhs) const = default;
float primaryScore{ 0.0f };
float secondaryScore{ 0.0f };
float tertiaryScore{ 0.0f };
};
// Get the donation total for the given activity id. // Get the donation total for the given activity id.
virtual std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) = 0; virtual std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetAgsLeaderboard(const uint32_t activityId) = 0;
virtual std::optional<Score> GetPlayerScore(const uint32_t playerId, const uint32_t gameId) = 0;
virtual void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
virtual void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
virtual void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) = 0;
virtual void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) = 0;
}; };
#endif //!__ILEADERBOARD__H__ #endif //!__ILEADERBOARD__H__

View File

@ -0,0 +1,14 @@
#ifndef IUGCMODULARBUILD_H
#define IUGCMODULARBUILD_H
#include <cstdint>
#include <optional>
#include <string>
class IUgcModularBuild {
public:
virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) = 0;
virtual void DeleteUgcBuild(const LWOOBJID bigId) = 0;
};
#endif //!IUGCMODULARBUILD_H

View File

@ -14,6 +14,7 @@ namespace {
}; };
void MySQLDatabase::Connect() { void MySQLDatabase::Connect() {
LOG("Using MySQL database");
driver = sql::mariadb::get_driver_instance(); driver = sql::mariadb::get_driver_instance();
// The mariadb connector is *supposed* to handle unix:// and pipe:// prefixes to hostName, but there are bugs where // 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) { sql::PreparedStatement* MySQLDatabase::CreatePreppedStmt(const std::string& query) {
if (!con) { if (!con) {
Connect(); Database::Get()->Connect();
LOG("Trying to reconnect to MySQL"); LOG("Trying to reconnect to MySQL");
} }
@ -76,7 +77,7 @@ sql::PreparedStatement* MySQLDatabase::CreatePreppedStmt(const std::string& quer
con = nullptr; con = nullptr;
Connect(); Database::Get()->Connect();
LOG("Trying to reconnect to MySQL from invalid or closed connection"); LOG("Trying to reconnect to MySQL from invalid or closed connection");
} }

View File

@ -7,6 +7,7 @@
#include "GameDatabase.h" #include "GameDatabase.h"
typedef std::unique_ptr<sql::PreparedStatement>& UniquePreppedStmtRef; typedef std::unique_ptr<sql::PreparedStatement>& UniquePreppedStmtRef;
typedef std::unique_ptr<sql::ResultSet> UniqueResultSet;
// Purposefully no definition for this to provide linker errors in the case someone tries to // 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. // bind a parameter to a type that isn't defined.
@ -29,7 +30,6 @@ public:
void Connect() override; void Connect() override;
void Destroy(std::string source = "") override; void Destroy(std::string source = "") override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query) override;
void Commit() override; void Commit() override;
bool GetAutoCommit() override; bool GetAutoCommit() override;
void SetAutoCommit(bool value) override; void SetAutoCommit(bool value) override;
@ -113,6 +113,19 @@ public:
void RemoveBehavior(const int32_t characterId) override; void RemoveBehavior(const int32_t characterId) override;
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override; std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override;
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> 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<ILeaderboard::Score> 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<uint32_t> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
uint32_t GetAccountCount() override;
private: private:
// Generic query functions that can be used for any query. // Generic query functions that can be used for any query.

View File

@ -39,3 +39,8 @@ void MySQLDatabase::InsertNewAccount(const std::string_view username, const std:
void MySQLDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) { void MySQLDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) {
ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast<int32_t>(gmLevel), accountId); ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast<int32_t>(gmLevel), accountId);
} }
uint32_t MySQLDatabase::GetAccountCount() {
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
return res->next() ? res->getUInt("count") : 0;
}

View File

@ -20,6 +20,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"PropertyContents.cpp" "PropertyContents.cpp"
"Servers.cpp" "Servers.cpp"
"Ugc.cpp" "Ugc.cpp"
"UgcModularBuild.cpp"
PARENT_SCOPE PARENT_SCOPE
) )

View File

@ -1,5 +1,9 @@
#include "MySQLDatabase.h" #include "MySQLDatabase.h"
#include "Game.h"
#include "Logger.h"
#include "dConfig.h"
std::optional<uint32_t> MySQLDatabase::GetDonationTotal(const uint32_t activityId) { std::optional<uint32_t> MySQLDatabase::GetDonationTotal(const uint32_t activityId) {
auto donation_total = ExecuteSelect("SELECT SUM(primaryScore) as donation_total FROM leaderboard WHERE game_id = ?;", activityId); auto donation_total = ExecuteSelect("SELECT SUM(primaryScore) as donation_total FROM leaderboard WHERE game_id = ?;", activityId);
@ -9,3 +13,79 @@ std::optional<uint32_t> MySQLDatabase::GetDonationTotal(const uint32_t activityI
return donation_total->getUInt("donation_total"); return donation_total->getUInt("donation_total");
} }
std::vector<ILeaderboard::Entry> ProcessQuery(UniqueResultSet& rows) {
std::vector<ILeaderboard::Entry> entries;
entries.reserve(rows->rowsCount());
while (rows->next()) {
auto& entry = entries.emplace_back();
entry.charId = rows->getUInt("character_id");
entry.lastPlayedTimestamp = rows->getUInt("lp_unix");
entry.primaryScore = rows->getFloat("primaryScore");
entry.secondaryScore = rows->getFloat("secondaryScore");
entry.tertiaryScore = rows->getFloat("tertiaryScore");
entry.numWins = rows->getUInt("numWins");
entry.numTimesPlayed = rows->getUInt("timesPlayed");
entry.name = rows->getString("char_name");
// entry.ranking is never set because its calculated in leaderboard in code.
}
return entries;
}
std::vector<ILeaderboard::Entry> MySQLDatabase::GetDescendingLeaderboard(const uint32_t activityId) {
auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore DESC, tertiaryScore DESC, last_played ASC;", activityId);
return ProcessQuery(leaderboard);
}
std::vector<ILeaderboard::Entry> MySQLDatabase::GetAscendingLeaderboard(const uint32_t activityId) {
auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore ASC, secondaryscore ASC, tertiaryScore ASC, last_played ASC;", activityId);
return ProcessQuery(leaderboard);
}
std::vector<ILeaderboard::Entry> MySQLDatabase::GetAgsLeaderboard(const uint32_t activityId) {
auto query = Game::config->GetValue("classic_survival_scoring") != "1" ?
"SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore DESC, tertiaryScore DESC, last_played ASC;" :
"SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY secondaryscore DESC, primaryscore DESC, tertiaryScore DESC, last_played ASC;";
auto leaderboard = ExecuteSelect(query, activityId);
return ProcessQuery(leaderboard);
}
std::vector<ILeaderboard::Entry> MySQLDatabase::GetNsLeaderboard(const uint32_t activityId) {
auto leaderboard = ExecuteSelect("SELECT *, UNIX_TIMESTAMP(last_played) as lp_unix, ci.name as char_name FROM leaderboard lb JOIN charinfo ci on ci.id = lb.character_id where game_id = ? ORDER BY primaryscore DESC, secondaryscore ASC, tertiaryScore DESC, last_played ASC;", activityId);
return ProcessQuery(leaderboard);
}
void MySQLDatabase::SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) {
ExecuteInsert("INSERT leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, character_id = ?, game_id = ?;",
score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId);
}
void MySQLDatabase::UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) {
ExecuteInsert("UPDATE leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;",
score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId);
}
void MySQLDatabase::IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) {
ExecuteUpdate("UPDATE leaderboard SET timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId);
}
std::optional<ILeaderboard::Score> MySQLDatabase::GetPlayerScore(const uint32_t playerId, const uint32_t gameId) {
std::optional<ILeaderboard::Score> toReturn = std::nullopt;
auto res = ExecuteSelect("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;", playerId, gameId);
if (res->next()) {
toReturn = ILeaderboard::Score{
.primaryScore = res->getFloat("primaryScore"),
.secondaryScore = res->getFloat("secondaryScore"),
.tertiaryScore = res->getFloat("tertiaryScore")
};
}
return toReturn;
}
void MySQLDatabase::IncrementNumWins(const uint32_t playerId, const uint32_t gameId) {
ExecuteUpdate("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId);
}

View File

@ -0,0 +1,9 @@
#include "MySQLDatabase.h"
void MySQLDatabase::InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) {
ExecuteInsert("INSERT INTO ugc_modular_build (ugc_id, ldf_config, character_id) VALUES (?,?,?)", bigId, modules, characterId);
}
void MySQLDatabase::DeleteUgcBuild(const LWOOBJID bigId) {
ExecuteDelete("DELETE FROM ugc_modular_build WHERE ugc_id = ?;", bigId);
}

View File

@ -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)

View File

@ -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);
}

View File

@ -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<typename ParamType>
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<typename... Args>
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<IServers::MasterInfo> GetMasterInfo() override;
std::vector<std::string> GetApprovedCharacterNames() override;
std::vector<FriendData> GetFriendsList(uint32_t charID) override;
std::optional<IFriends::BestFriendStatus> 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<IUgc::Model> GetAllUgcModels() override;
void CreateMigrationHistoryTable() override;
bool IsMigrationRun(const std::string_view str) override;
void InsertMigration(const std::string_view str) override;
std::optional<ICharInfo::Info> GetCharacterInfo(const uint32_t charId) override;
std::optional<ICharInfo::Info> 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<IAccounts::Info> 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<uint32_t> 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<IPetNames::Info> GetPetNameInfo(const LWOOBJID& petId) override;
std::optional<IProperty::Info> 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<IPropertyContents::Model> 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<std::pair<int32_t, std::string>, 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<IMail::MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
std::optional<IMail::MailInfo> 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<uint32_t> GetCurrentPersistentId() override;
void InsertDefaultPersistentId() override;
void UpdatePersistentId(const uint32_t id) override;
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
std::vector<IUgc::Model> 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<IIgnoreList::Info> GetIgnoreList(const uint32_t playerId) override;
void InsertRewardCode(const uint32_t account_id, const uint32_t reward_code) override;
std::vector<uint32_t> 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<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override;
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> 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<ILeaderboard::Score> 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<uint32_t> 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<typename... Args>
inline std::pair<CppSQLite3Statement, CppSQLite3Query> ExecuteSelect(const std::string& query, Args&&... args) {
std::pair<CppSQLite3Statement, CppSQLite3Query> toReturn;
toReturn.first = CreatePreppedStmt(query);
SetParams(toReturn.first, std::forward<Args>(args)...);
DLU_SQL_TRY_CATCH_RETHROW(toReturn.second = toReturn.first.execQuery());
return toReturn;
}
template<typename... Args>
inline void ExecuteDelete(const std::string& query, Args&&... args) {
auto preppedStmt = CreatePreppedStmt(query);
SetParams(preppedStmt, std::forward<Args>(args)...);
DLU_SQL_TRY_CATCH_RETHROW(preppedStmt.execDML());
}
template<typename... Args>
inline int32_t ExecuteUpdate(const std::string& query, Args&&... args) {
auto preppedStmt = CreatePreppedStmt(query);
SetParams(preppedStmt, std::forward<Args>(args)...);
DLU_SQL_TRY_CATCH_RETHROW(return preppedStmt.execDML());
}
template<typename... Args>
inline int ExecuteInsert(const std::string& query, Args&&... args) {
auto preppedStmt = CreatePreppedStmt(query);
SetParams(preppedStmt, std::forward<Args>(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<int32_t>(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<sqlite_int64>(param));
}
template<>
inline void SetParam(PreppedStmtRef stmt, const int index, const uint64_t param) {
LOG("%llu", param);
stmt.bind(index, static_cast<sqlite_int64>(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<const unsigned char*>(stream.str().c_str()), stream.str().size());
}
template<>
inline void SetParam(PreppedStmtRef stmt, const int index, const std::optional<uint32_t> param) {
if (param) {
LOG("%d", param.value());
stmt.bind(index, static_cast<int>(param.value()));
} else {
LOG("Null");
stmt.bindNull(index);
}
}
#endif //!SQLITEDATABASE_H

View File

@ -0,0 +1,49 @@
#include "SQLiteDatabase.h"
#include "eGameMasterLevel.h"
#include "Database.h"
std::optional<IAccounts::Info> 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<eGameMasterLevel>(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<int32_t>(eGameMasterLevel::OPERATOR));
}
void SQLiteDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) {
ExecuteUpdate("UPDATE accounts SET gm_level = ? WHERE id = ?;", static_cast<int32_t>(gmLevel), accountId);
}
uint32_t SQLiteDatabase::GetAccountCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
if (res.eof()) return 0;
return res.getIntField("count");
}

View File

@ -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<uint32_t> SQLiteDatabase::GetRewardCodesByAccountID(const uint32_t account_id) {
auto [_, result] = ExecuteSelect("SELECT rewardcode FROM accounts_rewardcodes WHERE account_id = ?;", account_id);
std::vector<uint32_t> toReturn;
while (!result.eof()) {
toReturn.push_back(result.getIntField("rewardcode"));
result.nextRow();
}
return toReturn;
}

View File

@ -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<uint32_t>(activityType), static_cast<uint32_t>(time(NULL)), mapId);
}

View File

@ -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") : "";
}

View File

@ -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);
}

View File

@ -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
)

View File

@ -0,0 +1,79 @@
#include "SQLiteDatabase.h"
std::vector<std::string> SQLiteDatabase::GetApprovedCharacterNames() {
auto [_, result] = ExecuteSelect("SELECT name FROM charinfo;");
std::vector<std::string> toReturn;
while (!result.eof()) {
toReturn.push_back(result.getStringField("name"));
result.nextRow();
}
return toReturn;
}
std::optional<ICharInfo::Info> 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<ePermissionMap>(stmt.getIntField("permission_map"));
return toReturn;
}
std::optional<ICharInfo::Info> 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<ICharInfo::Info> 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<uint32_t> 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<uint32_t> 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<uint32_t>(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<uint32_t>(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<uint32_t>(time(NULL)), characterId);
}
void SQLiteDatabase::UpdateLastLoggedInCharacter(const uint32_t characterId) {
ExecuteUpdate("UPDATE charinfo SET last_login = ? WHERE id = ?;", static_cast<uint32_t>(time(NULL)), characterId);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,73 @@
#include "SQLiteDatabase.h"
std::vector<FriendData> 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<FriendData> 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<IFriends::BestFriendStatus> 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
);
}

View File

@ -0,0 +1,22 @@
#include "SQLiteDatabase.h"
std::vector<IIgnoreList::Info> 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<IIgnoreList::Info> ignoreList;
while (!result.eof()) {
ignoreList.push_back(IIgnoreList::Info{ result.getStringField("name"), static_cast<uint32_t>(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);
}

View File

@ -0,0 +1,91 @@
#include "SQLiteDatabase.h"
#include "Game.h"
#include "Logger.h"
#include "dConfig.h"
std::optional<uint32_t> 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<ILeaderboard::Entry> ProcessQuery(CppSQLite3Query& rows) {
std::vector<ILeaderboard::Entry> 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<ILeaderboard::Entry> 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<ILeaderboard::Entry> 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<ILeaderboard::Entry> 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<ILeaderboard::Entry> 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<ILeaderboard::Score> SQLiteDatabase::GetPlayerScore(const uint32_t playerId, const uint32_t gameId) {
std::optional<ILeaderboard::Score> 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<float>(res.getFloatField("primaryScore")),
.secondaryScore = static_cast<float>(res.getFloatField("secondaryScore")),
.tertiaryScore = static_cast<float>(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);
}

View File

@ -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<uint32_t>(time(NULL)),
mail.subject,
mail.body,
mail.itemID,
mail.itemLOT,
0,
mail.itemCount);
}
std::vector<IMail::MailInfo> 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<IMail::MailInfo> 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<IMail::MailInfo> 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);
}

View File

@ -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);
}

View File

@ -0,0 +1,17 @@
#include "SQLiteDatabase.h"
std::optional<uint32_t> 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);
}

View File

@ -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<IPetNames::Info> 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;
}

View File

@ -0,0 +1,11 @@
#include "SQLiteDatabase.h"
std::optional<bool> 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");
}

View File

@ -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);
}

View File

@ -0,0 +1,195 @@
#include "SQLiteDatabase.h"
#include "ePropertySortType.h"
std::optional<IProperty::PropertyEntranceResult> SQLiteDatabase::GetProperties(const IProperty::PropertyLookup& params) {
std::optional<IProperty::PropertyEntranceResult> result;
std::string query;
std::pair<CppSQLite3Statement, CppSQLite3Query> 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<IProperty::Info> 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()
);
}

View File

@ -0,0 +1,65 @@
#include "SQLiteDatabase.h"
std::vector<IPropertyContents::Model> 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<IPropertyContents::Model> toReturn;
while (!result.eof()) {
IPropertyContents::Model model;
model.id = result.getInt64Field("id");
model.lot = static_cast<LOT>(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<uint32_t>(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<std::pair<int32_t, std::string>, 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);
}

View File

@ -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<IServers::MasterInfo> 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;
}

View File

@ -0,0 +1,72 @@
#include "SQLiteDatabase.h"
std::vector<IUgc::Model> 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<IUgc::Model> toReturn;
while (!result.eof()) {
IUgc::Model model;
int blobSize{};
const auto* blob = result.getBlobField("lxfml", blobSize);
model.lxfmlData << std::string(reinterpret_cast<const char*>(blob), blobSize);
model.id = result.getInt64Field("id");
toReturn.push_back(std::move(model));
result.nextRow();
}
return toReturn;
}
std::vector<IUgc::Model> SQLiteDatabase::GetAllUgcModels() {
auto [_, result] = ExecuteSelect("SELECT id, lxfml FROM ugc;");
std::vector<IUgc::Model> 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<const char*>(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);
}

View File

@ -0,0 +1,9 @@
#include "SQLiteDatabase.h"
void SQLiteDatabase::InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> 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);
}

View File

@ -8,10 +8,6 @@ void TestSQLDatabase::Destroy(std::string source) {
} }
sql::PreparedStatement* TestSQLDatabase::CreatePreppedStmt(const std::string& query) {
return nullptr;
}
void TestSQLDatabase::Commit() { void TestSQLDatabase::Commit() {
} }

View File

@ -7,7 +7,6 @@ class TestSQLDatabase : public GameDatabase {
void Connect() override; void Connect() override;
void Destroy(std::string source = "") override; void Destroy(std::string source = "") override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query) override;
void Commit() override; void Commit() override;
bool GetAutoCommit() override; bool GetAutoCommit() override;
void SetAutoCommit(bool value) override; void SetAutoCommit(bool value) override;
@ -91,6 +90,18 @@ class TestSQLDatabase : public GameDatabase {
void RemoveBehavior(const int32_t behaviorId) override; void RemoveBehavior(const int32_t behaviorId) override;
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override; void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override { return {}; }; std::optional<IProperty::PropertyEntranceResult> GetProperties(const IProperty::PropertyLookup& params) override { return {}; };
std::vector<ILeaderboard::Entry> GetDescendingLeaderboard(const uint32_t activityId) override { return {}; };
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override { return {}; };
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override { return {}; };
std::vector<ILeaderboard::Entry> GetAgsLeaderboard(const uint32_t activityId) override { return {}; };
void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override {};
void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override {};
std::optional<ILeaderboard::Score> GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override { return {}; };
void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override {};
void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override {};
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) override {};
void DeleteUgcBuild(const LWOOBJID bigId) override {};
uint32_t GetAccountCount() override { return 0; };
}; };
#endif //!TESTSQLDATABASE_H #endif //!TESTSQLDATABASE_H

View File

@ -10,9 +10,9 @@
#include <fstream> #include <fstream>
Migration LoadMigration(std::string path) { Migration LoadMigration(std::string folder, std::string path) {
Migration migration{}; Migration migration{};
std::ifstream file(BinaryPathFinder::GetBinaryDir() / "migrations/" / path); std::ifstream file(BinaryPathFinder::GetBinaryDir() / "migrations/" / folder / path);
if (file.is_open()) { if (file.is_open()) {
std::string line; std::string line;
@ -34,10 +34,19 @@ Migration LoadMigration(std::string path) {
void MigrationRunner::RunMigrations() { void MigrationRunner::RunMigrations() {
Database::Get()->CreateMigrationHistoryTable(); Database::Get()->CreateMigrationHistoryTable();
sql::SQLString finalSQL = ""; // has to be here because when moving the files to the new folder, the migration_history table is not updated so it will run them all again.
const auto migrationFolder = Database::GetMigrationFolder();
if (!Database::Get()->IsMigrationRun("17_migration_for_migrations.sql") && migrationFolder == "mysql") {
LOG("Running migration: 17_migration_for_migrations.sql");
Database::Get()->ExecuteCustomQuery("UPDATE `migration_history` SET `name` = SUBSTR(`name`, 5) WHERE `name` LIKE \"dlu%\";");
Database::Get()->InsertMigration("17_migration_for_migrations.sql");
}
std::string finalSQL = "";
bool runSd0Migrations = false; bool runSd0Migrations = false;
for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/").string())) { for (const auto& entry : GeneralUtils::GetSqlFileNamesFromFolder((BinaryPathFinder::GetBinaryDir() / "./migrations/dlu/" / migrationFolder).string())) {
auto migration = LoadMigration("dlu/" + entry); auto migration = LoadMigration("dlu/" + migrationFolder + "/", entry);
if (migration.data.empty()) { if (migration.data.empty()) {
continue; continue;
@ -46,7 +55,7 @@ void MigrationRunner::RunMigrations() {
if (Database::Get()->IsMigrationRun(migration.name)) continue; if (Database::Get()->IsMigrationRun(migration.name)) continue;
LOG("Running migration: %s", migration.name.c_str()); 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; runSd0Migrations = true;
} else { } else {
finalSQL.append(migration.data.c_str()); finalSQL.append(migration.data.c_str());
@ -61,12 +70,12 @@ void MigrationRunner::RunMigrations() {
} }
if (!finalSQL.empty()) { if (!finalSQL.empty()) {
auto migration = GeneralUtils::SplitString(static_cast<std::string>(finalSQL), ';'); auto migration = GeneralUtils::SplitString(finalSQL, ';');
for (auto& query : migration) { for (auto& query : migration) {
try { try {
if (query.empty()) continue; if (query.empty()) continue;
Database::Get()->ExecuteCustomQuery(query.c_str()); Database::Get()->ExecuteCustomQuery(query);
} catch (sql::SQLException& e) { } catch (std::exception& e) {
LOG("Encountered error running migration: %s", e.what()); LOG("Encountered error running migration: %s", e.what());
} }
} }
@ -86,10 +95,14 @@ void MigrationRunner::RunSQLiteMigrations() {
cdstmt.execQuery().finalize(); cdstmt.execQuery().finalize();
cdstmt.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())) { 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; if (migration.data.empty()) continue;

View File

@ -26,7 +26,6 @@ target_include_directories(dGameBase PUBLIC "." "dEntity"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
"${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include"
# dPhysics # dPhysics
"${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Recast/Include" "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Recast/Include"
"${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Detour/Include" "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Detour/Include"

View File

@ -83,6 +83,7 @@
#include "ItemComponent.h" #include "ItemComponent.h"
#include "GhostComponent.h" #include "GhostComponent.h"
#include "AchievementVendorComponent.h" #include "AchievementVendorComponent.h"
#include "VanityUtilities.h"
// Table includes // Table includes
#include "CDComponentsRegistryTable.h" #include "CDComponentsRegistryTable.h"
@ -96,6 +97,8 @@
#include "CDSkillBehaviorTable.h" #include "CDSkillBehaviorTable.h"
#include "CDZoneTableTable.h" #include "CDZoneTableTable.h"
#include <ranges>
Observable<Entity*, const PositionUpdate&> Entity::OnPlayerPositionUpdate; Observable<Entity*, const PositionUpdate&> Entity::OnPlayerPositionUpdate;
Entity::Entity(const LWOOBJID& objectID, EntityInfo info, User* parentUser, Entity* parentEntity) { Entity::Entity(const LWOOBJID& objectID, EntityInfo info, User* parentUser, Entity* parentEntity) {
@ -285,8 +288,9 @@ void Entity::Initialize() {
AddComponent<PropertyEntranceComponent>(propertyEntranceComponentID); AddComponent<PropertyEntranceComponent>(propertyEntranceComponentID);
} }
if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::CONTROLLABLE_PHYSICS) > 0) { const int32_t controllablePhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::CONTROLLABLE_PHYSICS);
auto* controllablePhysics = AddComponent<ControllablePhysicsComponent>(); if (controllablePhysicsComponentID > 0) {
auto* controllablePhysics = AddComponent<ControllablePhysicsComponent>(controllablePhysicsComponentID);
if (m_Character) { if (m_Character) {
controllablePhysics->LoadFromXml(m_Character->GetXMLDoc()); controllablePhysics->LoadFromXml(m_Character->GetXMLDoc());
@ -329,16 +333,19 @@ void Entity::Initialize() {
AddComponent<SimplePhysicsComponent>(simplePhysicsComponentID); AddComponent<SimplePhysicsComponent>(simplePhysicsComponentID);
} }
if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS) > 0) { const int32_t rigidBodyPhantomPhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS);
AddComponent<RigidbodyPhantomPhysicsComponent>(); if (rigidBodyPhantomPhysicsComponentID > 0) {
AddComponent<RigidbodyPhantomPhysicsComponent>(rigidBodyPhantomPhysicsComponentID);
} }
if (markedAsPhantom || compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::PHANTOM_PHYSICS) > 0) { const int32_t phantomPhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::PHANTOM_PHYSICS);
AddComponent<PhantomPhysicsComponent>()->SetPhysicsEffectActive(false); if (markedAsPhantom || phantomPhysicsComponentID > 0) {
AddComponent<PhantomPhysicsComponent>(phantomPhysicsComponentID)->SetPhysicsEffectActive(false);
} }
if (compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::HAVOK_VEHICLE_PHYSICS) > 0) { const int32_t havokVehiclePhysicsComponentID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::HAVOK_VEHICLE_PHYSICS);
auto* havokVehiclePhysicsComponent = AddComponent<HavokVehiclePhysicsComponent>(); if (havokVehiclePhysicsComponentID > 0) {
auto* havokVehiclePhysicsComponent = AddComponent<HavokVehiclePhysicsComponent>(havokVehiclePhysicsComponentID);
havokVehiclePhysicsComponent->SetPosition(m_DefaultPosition); havokVehiclePhysicsComponent->SetPosition(m_DefaultPosition);
havokVehiclePhysicsComponent->SetRotation(m_DefaultRotation); havokVehiclePhysicsComponent->SetRotation(m_DefaultRotation);
} }
@ -1271,6 +1278,7 @@ void Entity::Update(const float deltaTime) {
auto timerName = timer.GetName(); auto timerName = timer.GetName();
m_Timers.erase(m_Timers.begin() + timerPosition); m_Timers.erase(m_Timers.begin() + timerPosition);
GetScript()->OnTimerDone(this, timerName); GetScript()->OnTimerDone(this, timerName);
VanityUtilities::OnTimerDone(this, timerName);
TriggerEvent(eTriggerEventType::TIMER_DONE, this); TriggerEvent(eTriggerEventType::TIMER_DONE, this);
} else { } else {
@ -1334,6 +1342,7 @@ void Entity::OnCollisionProximity(LWOOBJID otherEntity, const std::string& proxN
if (!other) return; if (!other) return;
GetScript()->OnProximityUpdate(this, other, proxName, status); GetScript()->OnProximityUpdate(this, other, proxName, status);
VanityUtilities::OnProximityUpdate(this, other, proxName, status);
RocketLaunchpadControlComponent* rocketComp = GetComponent<RocketLaunchpadControlComponent>(); RocketLaunchpadControlComponent* rocketComp = GetComponent<RocketLaunchpadControlComponent>();
if (!rocketComp) return; if (!rocketComp) return;
@ -1351,6 +1360,11 @@ void Entity::OnCollisionPhantom(const LWOOBJID otherEntity) {
callback(other); callback(other);
} }
SwitchComponent* switchComp = GetComponent<SwitchComponent>();
if (switchComp) {
switchComp->OnUse(other);
}
TriggerEvent(eTriggerEventType::ENTER, other); TriggerEvent(eTriggerEventType::ENTER, other);
// POI system // POI system
@ -2153,7 +2167,19 @@ void Entity::SetRespawnPos(const NiPoint3& position) {
auto* characterComponent = GetComponent<CharacterComponent>(); auto* characterComponent = GetComponent<CharacterComponent>();
if (characterComponent) characterComponent->SetRespawnPos(position); if (characterComponent) characterComponent->SetRespawnPos(position);
} }
void Entity::SetRespawnRot(const NiQuaternion& rotation) { void Entity::SetRespawnRot(const NiQuaternion& rotation) {
auto* characterComponent = GetComponent<CharacterComponent>(); auto* characterComponent = GetComponent<CharacterComponent>();
if (characterComponent) characterComponent->SetRespawnRot(rotation); if (characterComponent) characterComponent->SetRespawnRot(rotation);
} }
int32_t Entity::GetCollisionGroup() const {
for (const auto* component : m_Components | std::views::values) {
auto* compToCheck = dynamic_cast<const PhysicsComponent*>(component);
if (compToCheck) {
return compToCheck->GetCollisionGroup();
}
}
return 0;
}

View File

@ -107,6 +107,11 @@ public:
const SystemAddress& GetSystemAddress() const; 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 * Setters
*/ */

View File

@ -1,5 +1,6 @@
#include "LeaderboardManager.h" #include "LeaderboardManager.h"
#include <ranges>
#include <sstream> #include <sstream>
#include <utility> #include <utility>
@ -72,197 +73,191 @@ void Leaderboard::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write0(); bitStream.Write0();
} }
void Leaderboard::QueryToLdf(std::unique_ptr<sql::ResultSet>& rows) { // Takes the resulting query from a leaderboard lookup and converts it to the LDF we need
Clear(); // to send it to a client.
if (rows->rowsCount() == 0) return; void QueryToLdf(Leaderboard& leaderboard, const std::vector<ILeaderboard::Entry>& leaderboardEntries) {
using enum Leaderboard::Type;
leaderboard.Clear();
if (leaderboardEntries.empty()) return;
this->entries.reserve(rows->rowsCount()); for (const auto& leaderboardEntry : leaderboardEntries) {
while (rows->next()) {
constexpr int32_t MAX_NUM_DATA_PER_ROW = 9; constexpr int32_t MAX_NUM_DATA_PER_ROW = 9;
this->entries.push_back(std::vector<LDFBaseData*>()); auto& entry = leaderboard.PushBackEntry();
auto& entry = this->entries.back();
entry.reserve(MAX_NUM_DATA_PER_ROW); entry.reserve(MAX_NUM_DATA_PER_ROW);
entry.push_back(new LDFData<uint64_t>(u"CharacterID", rows->getInt("character_id"))); entry.push_back(new LDFData<uint64_t>(u"CharacterID", leaderboardEntry.charId));
entry.push_back(new LDFData<uint64_t>(u"LastPlayed", rows->getUInt64("lastPlayed"))); entry.push_back(new LDFData<uint64_t>(u"LastPlayed", leaderboardEntry.lastPlayedTimestamp));
entry.push_back(new LDFData<int32_t>(u"NumPlayed", rows->getInt("timesPlayed"))); entry.push_back(new LDFData<int32_t>(u"NumPlayed", leaderboardEntry.numTimesPlayed));
entry.push_back(new LDFData<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(rows->getString("name").c_str()))); entry.push_back(new LDFData<std::u16string>(u"name", GeneralUtils::ASCIIToUTF16(leaderboardEntry.name)));
entry.push_back(new LDFData<uint64_t>(u"RowNumber", rows->getInt("ranking"))); entry.push_back(new LDFData<uint64_t>(u"RowNumber", leaderboardEntry.ranking));
switch (leaderboardType) { switch (leaderboard.GetLeaderboardType()) {
case Type::ShootingGallery: case ShootingGallery:
entry.push_back(new LDFData<int32_t>(u"Score", rows->getInt("primaryScore"))); entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore));
// Score:1 // Score:1
entry.push_back(new LDFData<int32_t>(u"Streak", rows->getInt("secondaryScore"))); entry.push_back(new LDFData<int32_t>(u"Streak", leaderboardEntry.secondaryScore));
// Streak:1 // Streak:1
entry.push_back(new LDFData<float>(u"HitPercentage", (rows->getInt("tertiaryScore") / 100.0f))); entry.push_back(new LDFData<float>(u"HitPercentage", (leaderboardEntry.tertiaryScore / 100.0f)));
// HitPercentage:3 between 0 and 1 // HitPercentage:3 between 0 and 1
break; break;
case Type::Racing: case Racing:
entry.push_back(new LDFData<float>(u"BestTime", rows->getDouble("primaryScore"))); entry.push_back(new LDFData<float>(u"BestTime", leaderboardEntry.primaryScore));
// BestLapTime:3 // BestLapTime:3
entry.push_back(new LDFData<float>(u"BestLapTime", rows->getDouble("secondaryScore"))); entry.push_back(new LDFData<float>(u"BestLapTime", leaderboardEntry.secondaryScore));
// BestTime:3 // BestTime:3
entry.push_back(new LDFData<int32_t>(u"License", 1)); entry.push_back(new LDFData<int32_t>(u"License", 1));
// License:1 - 1 if player has completed mission 637 and 0 otherwise // License:1 - 1 if player has completed mission 637 and 0 otherwise
entry.push_back(new LDFData<int32_t>(u"NumWins", rows->getInt("numWins"))); entry.push_back(new LDFData<int32_t>(u"NumWins", leaderboardEntry.numWins));
// NumWins:1 // NumWins:1
break; break;
case Type::UnusedLeaderboard4: case UnusedLeaderboard4:
entry.push_back(new LDFData<int32_t>(u"Points", rows->getInt("primaryScore"))); entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore));
// Points:1 // Points:1
break; break;
case Type::MonumentRace: case MonumentRace:
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("primaryScore"))); entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore));
// Time:1(?) // Time:1(?)
break; break;
case Type::FootRace: case FootRace:
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("primaryScore"))); entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.primaryScore));
// Time:1 // Time:1
break; break;
case Type::Survival: case Survival:
entry.push_back(new LDFData<int32_t>(u"Points", rows->getInt("primaryScore"))); entry.push_back(new LDFData<int32_t>(u"Points", leaderboardEntry.primaryScore));
// Points:1 // Points:1
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("secondaryScore"))); entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore));
// Time:1 // Time:1
break; break;
case Type::SurvivalNS: case SurvivalNS:
entry.push_back(new LDFData<int32_t>(u"Wave", rows->getInt("primaryScore"))); entry.push_back(new LDFData<int32_t>(u"Wave", leaderboardEntry.primaryScore));
// Wave:1 // Wave:1
entry.push_back(new LDFData<int32_t>(u"Time", rows->getInt("secondaryScore"))); entry.push_back(new LDFData<int32_t>(u"Time", leaderboardEntry.secondaryScore));
// Time:1 // Time:1
break; break;
case Type::Donations: case Donations:
entry.push_back(new LDFData<int32_t>(u"Score", rows->getInt("primaryScore"))); entry.push_back(new LDFData<int32_t>(u"Score", leaderboardEntry.primaryScore));
// Score:1 // Score:1
break; break;
case Type::None: case None:
// This type is included here simply to resolve a compiler warning on mac about unused enum types [[fallthrough]];
break;
default: default:
break; break;
} }
} }
} }
const std::string_view Leaderboard::GetOrdering(Leaderboard::Type leaderboardType) { std::vector<ILeaderboard::Entry> FilterTo10(const std::vector<ILeaderboard::Entry>& leaderboard, const uint32_t relatedPlayer, const Leaderboard::InfoType infoType) {
// Use a switch case and return desc for all 3 columns if higher is better and asc if lower is better std::vector<ILeaderboard::Entry> toReturn;
int32_t index = 0;
// for friends and top, we dont need to find this players index.
if (infoType == Leaderboard::InfoType::MyStanding || infoType == Leaderboard::InfoType::Friends) {
for (; index < leaderboard.size(); index++) {
if (leaderboard[index].charId == relatedPlayer) break;
}
}
if (leaderboard.size() < 10) {
toReturn.assign(leaderboard.begin(), leaderboard.end());
index = 0;
} else if (index < 10) {
toReturn.assign(leaderboard.begin(), leaderboard.begin() + 10); // get the top 10 since we are in the top 10
index = 0;
} else if (index > leaderboard.size() - 10) {
toReturn.assign(leaderboard.end() - 10, leaderboard.end()); // get the bottom 10 since we are in the bottom 10
index = leaderboard.size() - 10;
} else {
toReturn.assign(leaderboard.begin() + index - 5, leaderboard.begin() + index + 5); // get the 5 above and below
index -= 5;
}
int32_t i = index;
for (auto& entry : toReturn) {
entry.ranking = ++i;
}
return toReturn;
}
std::vector<ILeaderboard::Entry> FilterWeeklies(const std::vector<ILeaderboard::Entry>& leaderboard) {
// Filter the leaderboard to only include entries from the last week
const auto currentTime = std::chrono::system_clock::now();
auto epochTime = currentTime.time_since_epoch().count();
constexpr auto SECONDS_IN_A_WEEK = 60 * 60 * 24 * 7; // if you think im taking leap seconds into account thats cute.
std::vector<ILeaderboard::Entry> weeklyLeaderboard;
for (const auto& entry : leaderboard) {
if (epochTime - entry.lastPlayedTimestamp < SECONDS_IN_A_WEEK) {
weeklyLeaderboard.push_back(entry);
}
}
return weeklyLeaderboard;
}
std::vector<ILeaderboard::Entry> FilterFriends(const std::vector<ILeaderboard::Entry>& leaderboard, const uint32_t relatedPlayer) {
// Filter the leaderboard to only include friends of the player
auto friendOfPlayer = Database::Get()->GetFriendsList(relatedPlayer);
std::vector<ILeaderboard::Entry> friendsLeaderboard;
for (const auto& entry : leaderboard) {
const auto res = std::ranges::find_if(friendOfPlayer, [&entry, relatedPlayer](const FriendData& data) {
return entry.charId == data.friendID || entry.charId == relatedPlayer;
});
if (res != friendOfPlayer.cend()) {
friendsLeaderboard.push_back(entry);
}
}
return friendsLeaderboard;
}
std::vector<ILeaderboard::Entry> ProcessLeaderboard(
const std::vector<ILeaderboard::Entry>& leaderboard,
const bool weekly,
const Leaderboard::InfoType infoType,
const uint32_t relatedPlayer) {
std::vector<ILeaderboard::Entry> toReturn;
if (infoType == Leaderboard::InfoType::Friends) {
const auto friendsLeaderboard = FilterFriends(leaderboard, relatedPlayer);
toReturn = FilterTo10(weekly ? FilterWeeklies(friendsLeaderboard) : friendsLeaderboard, relatedPlayer, infoType);
} else {
toReturn = FilterTo10(weekly ? FilterWeeklies(leaderboard) : leaderboard, relatedPlayer, infoType);
}
return toReturn;
}
void Leaderboard::SetupLeaderboard(bool weekly) {
const auto leaderboardType = LeaderboardManager::GetLeaderboardType(gameID);
std::vector<ILeaderboard::Entry> leaderboardRes;
switch (leaderboardType) { switch (leaderboardType) {
case Type::Racing:
case Type::MonumentRace:
return "primaryScore ASC, secondaryScore ASC, tertiaryScore ASC";
case Type::Survival:
return Game::config->GetValue("classic_survival_scoring") == "1" ?
"secondaryScore DESC, primaryScore DESC, tertiaryScore DESC" :
"primaryScore DESC, secondaryScore DESC, tertiaryScore DESC";
case Type::SurvivalNS: case Type::SurvivalNS:
return "primaryScore DESC, secondaryScore ASC, tertiaryScore DESC"; leaderboardRes = Database::Get()->GetNsLeaderboard(gameID);
break;
case Type::Survival:
leaderboardRes = Database::Get()->GetAgsLeaderboard(gameID);
break;
case Type::Racing:
[[fallthrough]];
case Type::MonumentRace:
leaderboardRes = Database::Get()->GetAscendingLeaderboard(gameID);
break;
case Type::ShootingGallery: case Type::ShootingGallery:
[[fallthrough]];
case Type::FootRace: case Type::FootRace:
case Type::UnusedLeaderboard4: [[fallthrough]];
case Type::Donations: case Type::Donations:
[[fallthrough]];
case Type::None: case Type::None:
[[fallthrough]];
default: default:
return "primaryScore DESC, secondaryScore DESC, tertiaryScore DESC"; leaderboardRes = Database::Get()->GetDescendingLeaderboard(gameID);
} break;
} }
void Leaderboard::SetupLeaderboard(bool weekly, uint32_t resultStart, uint32_t resultEnd) { const auto processedLeaderboard = ProcessLeaderboard(leaderboardRes, weekly, infoType, relatedPlayer);
resultStart++;
resultEnd++;
// We need everything except 1 column so i'm selecting * from leaderboard
const std::string queryBase =
R"QUERY(
WITH leaderboardsRanked AS (
SELECT leaderboard.*, charinfo.name,
RANK() OVER
(
ORDER BY %s, UNIX_TIMESTAMP(last_played) ASC, id DESC
) AS ranking
FROM leaderboard JOIN charinfo on charinfo.id = leaderboard.character_id
WHERE game_id = ? %s
),
myStanding AS (
SELECT
ranking as myRank
FROM leaderboardsRanked
WHERE id = ?
),
lowestRanking AS (
SELECT MAX(ranking) AS lowestRank
FROM leaderboardsRanked
)
SELECT leaderboardsRanked.*, character_id, UNIX_TIMESTAMP(last_played) as lastPlayed, leaderboardsRanked.name, leaderboardsRanked.ranking FROM leaderboardsRanked, myStanding, lowestRanking
WHERE leaderboardsRanked.ranking
BETWEEN
LEAST(GREATEST(CAST(myRank AS SIGNED) - 5, %i), CAST(lowestRanking.lowestRank AS SIGNED) - 9)
AND
LEAST(GREATEST(myRank + 5, %i), lowestRanking.lowestRank)
ORDER BY ranking ASC;
)QUERY";
std::string friendsFilter = QueryToLdf(*this, processedLeaderboard);
R"QUERY(
AND (
character_id IN (
SELECT fr.requested_player FROM (
SELECT CASE
WHEN player_id = ? THEN friend_id
WHEN friend_id = ? THEN player_id
END AS requested_player
FROM friends
) AS fr
JOIN charinfo AS ci
ON ci.id = fr.requested_player
WHERE fr.requested_player IS NOT NULL
)
OR character_id = ?
)
)QUERY";
std::string weeklyFilter = " AND UNIX_TIMESTAMP(last_played) BETWEEN UNIX_TIMESTAMP(date_sub(now(),INTERVAL 1 WEEK)) AND UNIX_TIMESTAMP(now()) ";
std::string filter;
// Setup our filter based on the query type
if (this->infoType == InfoType::Friends) filter += friendsFilter;
if (this->weekly) filter += weeklyFilter;
const auto orderBase = GetOrdering(this->leaderboardType);
// For top query, we want to just rank all scores, but for all others we need the scores around a specific player
std::string baseLookup;
if (this->infoType == InfoType::Top) {
baseLookup = "SELECT id, last_played FROM leaderboard WHERE game_id = ? " + (this->weekly ? weeklyFilter : std::string("")) + " ORDER BY ";
baseLookup += orderBase.data();
} else {
baseLookup = "SELECT id, last_played FROM leaderboard WHERE game_id = ? " + (this->weekly ? weeklyFilter : std::string("")) + " AND character_id = ";
baseLookup += std::to_string(static_cast<uint32_t>(this->relatedPlayer));
}
baseLookup += " LIMIT 1";
LOG_DEBUG("query is %s", baseLookup.c_str());
std::unique_ptr<sql::PreparedStatement> baseQuery(Database::Get()->CreatePreppedStmt(baseLookup));
baseQuery->setInt(1, this->gameID);
std::unique_ptr<sql::ResultSet> baseResult(baseQuery->executeQuery());
if (!baseResult->next()) return; // In this case, there are no entries in the leaderboard for this game.
uint32_t relatedPlayerLeaderboardId = baseResult->getInt("id");
// Create and execute the actual save here. Using a heap allocated buffer to avoid stack overflow
constexpr uint16_t STRING_LENGTH = 4096;
std::unique_ptr<char[]> lookupBuffer = std::make_unique<char[]>(STRING_LENGTH);
int32_t res = snprintf(lookupBuffer.get(), STRING_LENGTH, queryBase.c_str(), orderBase.data(), filter.c_str(), resultStart, resultEnd);
DluAssert(res != -1);
std::unique_ptr<sql::PreparedStatement> query(Database::Get()->CreatePreppedStmt(lookupBuffer.get()));
LOG_DEBUG("Query is %s vars are %i %i %i", lookupBuffer.get(), this->gameID, this->relatedPlayer, relatedPlayerLeaderboardId);
query->setInt(1, this->gameID);
if (this->infoType == InfoType::Friends) {
query->setInt(2, this->relatedPlayer);
query->setInt(3, this->relatedPlayer);
query->setInt(4, this->relatedPlayer);
query->setInt(5, relatedPlayerLeaderboardId);
} else {
query->setInt(2, relatedPlayerLeaderboardId);
}
std::unique_ptr<sql::ResultSet> result(query->executeQuery());
QueryToLdf(result);
} }
void Leaderboard::Send(const LWOOBJID targetID) const { void Leaderboard::Send(const LWOOBJID targetID) const {
@ -272,129 +267,43 @@ void Leaderboard::Send(const LWOOBJID targetID) const {
} }
} }
std::string FormatInsert(const Leaderboard::Type& type, const Score& score, const bool useUpdate) {
std::string insertStatement;
if (useUpdate) {
insertStatement =
R"QUERY(
UPDATE leaderboard
SET primaryScore = %f, secondaryScore = %f, tertiaryScore = %f,
timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;
)QUERY";
} else {
insertStatement =
R"QUERY(
INSERT leaderboard SET
primaryScore = %f, secondaryScore = %f, tertiaryScore = %f,
character_id = ?, game_id = ?;
)QUERY";
}
constexpr uint16_t STRING_LENGTH = 400;
// Then fill in our score
char finishedQuery[STRING_LENGTH];
int32_t res = snprintf(finishedQuery, STRING_LENGTH, insertStatement.c_str(), score.GetPrimaryScore(), score.GetSecondaryScore(), score.GetTertiaryScore());
DluAssert(res != -1);
return finishedQuery;
}
void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore, const float tertiaryScore) { void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore, const float tertiaryScore) {
const Leaderboard::Type leaderboardType = GetLeaderboardType(activityId); const Leaderboard::Type leaderboardType = GetLeaderboardType(activityId);
std::unique_ptr<sql::PreparedStatement> query(Database::Get()->CreatePreppedStmt("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;")); const auto oldScore = Database::Get()->GetPlayerScore(playerID, activityId);
query->setInt(1, playerID);
query->setInt(2, activityId);
std::unique_ptr<sql::ResultSet> myScoreResult(query->executeQuery());
std::string saveQuery("UPDATE leaderboard SET timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;"); ILeaderboard::Score newScore{ .primaryScore = primaryScore, .secondaryScore = secondaryScore, .tertiaryScore = tertiaryScore };
Score newScore(primaryScore, secondaryScore, tertiaryScore); if (oldScore.has_value()) {
if (myScoreResult->next()) { bool lowerScoreBetter = leaderboardType == Leaderboard::Type::Racing || leaderboardType == Leaderboard::Type::MonumentRace;
Score oldScore;
bool lowerScoreBetter = false;
switch (leaderboardType) {
// Higher score better
case Leaderboard::Type::ShootingGallery: {
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
oldScore.SetTertiaryScore(myScoreResult->getInt("tertiaryScore"));
break;
}
case Leaderboard::Type::FootRace: {
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
break;
}
case Leaderboard::Type::Survival: {
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
break;
}
case Leaderboard::Type::SurvivalNS: {
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
break;
}
case Leaderboard::Type::UnusedLeaderboard4:
case Leaderboard::Type::Donations: {
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
newScore.SetPrimaryScore(oldScore.GetPrimaryScore() + newScore.GetPrimaryScore());
break;
}
case Leaderboard::Type::Racing: {
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
oldScore.SetSecondaryScore(myScoreResult->getInt("secondaryScore"));
// For wins we dont care about the score, just the time, so zero out the tertiary.
// Wins are updated later.
oldScore.SetTertiaryScore(0);
newScore.SetTertiaryScore(0);
lowerScoreBetter = true;
break;
}
case Leaderboard::Type::MonumentRace: {
oldScore.SetPrimaryScore(myScoreResult->getInt("primaryScore"));
lowerScoreBetter = true;
// Do score checking here
break;
}
case Leaderboard::Type::None:
default:
LOG("Unknown leaderboard type %i for game %i. Cannot save score!", leaderboardType, activityId);
return;
}
bool newHighScore = lowerScoreBetter ? newScore < oldScore : newScore > oldScore; bool newHighScore = lowerScoreBetter ? newScore < oldScore : newScore > oldScore;
// Nimbus station has a weird leaderboard where we need a custom scoring system // Nimbus station has a weird leaderboard where we need a custom scoring system
if (leaderboardType == Leaderboard::Type::SurvivalNS) { if (leaderboardType == Leaderboard::Type::SurvivalNS) {
newHighScore = newScore.GetPrimaryScore() > oldScore.GetPrimaryScore() || newHighScore = newScore.primaryScore > oldScore->primaryScore ||
(newScore.GetPrimaryScore() == oldScore.GetPrimaryScore() && newScore.GetSecondaryScore() < oldScore.GetSecondaryScore()); (newScore.primaryScore == oldScore->primaryScore && newScore.secondaryScore < oldScore->secondaryScore);
} else if (leaderboardType == Leaderboard::Type::Survival && Game::config->GetValue("classic_survival_scoring") == "1") { } else if (leaderboardType == Leaderboard::Type::Survival && Game::config->GetValue("classic_survival_scoring") == "1") {
Score oldScoreFlipped(oldScore.GetSecondaryScore(), oldScore.GetPrimaryScore()); ILeaderboard::Score oldScoreFlipped{oldScore->secondaryScore, oldScore->primaryScore, oldScore->tertiaryScore};
Score newScoreFlipped(newScore.GetSecondaryScore(), newScore.GetPrimaryScore()); ILeaderboard::Score newScoreFlipped{newScore.secondaryScore, newScore.primaryScore, newScore.tertiaryScore};
newHighScore = newScoreFlipped > oldScoreFlipped; newHighScore = newScoreFlipped > oldScoreFlipped;
} }
if (newHighScore) { if (newHighScore) {
saveQuery = FormatInsert(leaderboardType, newScore, true); Database::Get()->UpdateScore(playerID, activityId, newScore);
} else {
Database::Get()->IncrementTimesPlayed(playerID, activityId);
} }
} else { } else {
saveQuery = FormatInsert(leaderboardType, newScore, false); Database::Get()->SaveScore(playerID, activityId, newScore);
} }
LOG("save query %s %i %i", saveQuery.c_str(), playerID, activityId);
std::unique_ptr<sql::PreparedStatement> saveStatement(Database::Get()->CreatePreppedStmt(saveQuery));
saveStatement->setInt(1, playerID);
saveStatement->setInt(2, activityId);
saveStatement->execute();
// track wins separately // track wins separately
if (leaderboardType == Leaderboard::Type::Racing && tertiaryScore != 0.0f) { if (leaderboardType == Leaderboard::Type::Racing && tertiaryScore != 0.0f) {
std::unique_ptr<sql::PreparedStatement> winUpdate(Database::Get()->CreatePreppedStmt("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;")); Database::Get()->IncrementNumWins(playerID, activityId);
winUpdate->setInt(1, playerID);
winUpdate->setInt(2, activityId);
winUpdate->execute();
} }
} }
void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t resultStart, const uint32_t resultEnd) { void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID) {
Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID)); Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID));
leaderboard.SetupLeaderboard(weekly, resultStart, resultEnd); leaderboard.SetupLeaderboard(weekly);
leaderboard.Send(targetID); leaderboard.Send(targetID);
} }

View File

@ -9,46 +9,10 @@
#include "dCommonVars.h" #include "dCommonVars.h"
#include "LDFFormat.h" #include "LDFFormat.h"
namespace sql {
class ResultSet;
};
namespace RakNet { namespace RakNet {
class BitStream; class BitStream;
}; };
class Score {
public:
Score() {
primaryScore = 0;
secondaryScore = 0;
tertiaryScore = 0;
}
Score(const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0) {
this->primaryScore = primaryScore;
this->secondaryScore = secondaryScore;
this->tertiaryScore = tertiaryScore;
}
bool operator<(const Score& rhs) const {
return primaryScore < rhs.primaryScore || (primaryScore == rhs.primaryScore && secondaryScore < rhs.secondaryScore) || (primaryScore == rhs.primaryScore && secondaryScore == rhs.secondaryScore && tertiaryScore < rhs.tertiaryScore);
}
bool operator>(const Score& rhs) const {
return primaryScore > rhs.primaryScore || (primaryScore == rhs.primaryScore && secondaryScore > rhs.secondaryScore) || (primaryScore == rhs.primaryScore && secondaryScore == rhs.secondaryScore && tertiaryScore > rhs.tertiaryScore);
}
void SetPrimaryScore(const float score) { primaryScore = score; }
float GetPrimaryScore() const { return primaryScore; }
void SetSecondaryScore(const float score) { secondaryScore = score; }
float GetSecondaryScore() const { return secondaryScore; }
void SetTertiaryScore(const float score) { tertiaryScore = score; }
float GetTertiaryScore() const { return tertiaryScore; }
private:
float primaryScore;
float secondaryScore;
float tertiaryScore;
};
using GameID = uint32_t; using GameID = uint32_t;
class Leaderboard { class Leaderboard {
@ -96,20 +60,16 @@ public:
* @param resultStart The index to start the leaderboard at. Zero indexed. * @param resultStart The index to start the leaderboard at. Zero indexed.
* @param resultEnd The index to end the leaderboard at. Zero indexed. * @param resultEnd The index to end the leaderboard at. Zero indexed.
*/ */
void SetupLeaderboard(bool weekly, uint32_t resultStart = 0, uint32_t resultEnd = 10); void SetupLeaderboard(bool weekly);
/** /**
* Sends the leaderboard to the client specified by targetID. * Sends the leaderboard to the client specified by targetID.
*/ */
void Send(const LWOOBJID targetID) const; void Send(const LWOOBJID targetID) const;
// Helper function to get the columns, ordering and insert format for a leaderboard
static const std::string_view GetOrdering(Type leaderboardType);
private:
// Takes the resulting query from a leaderboard lookup and converts it to the LDF we need
// to send it to a client.
void QueryToLdf(std::unique_ptr<sql::ResultSet>& rows);
private:
using LeaderboardEntry = std::vector<LDFBaseData*>; using LeaderboardEntry = std::vector<LDFBaseData*>;
using LeaderboardEntries = std::vector<LeaderboardEntry>; using LeaderboardEntries = std::vector<LeaderboardEntry>;
@ -119,10 +79,18 @@ private:
InfoType infoType; InfoType infoType;
Leaderboard::Type leaderboardType; Leaderboard::Type leaderboardType;
bool weekly; bool weekly;
public:
LeaderboardEntry& PushBackEntry() {
return entries.emplace_back();
}
Type GetLeaderboardType() const {
return leaderboardType;
}
}; };
namespace LeaderboardManager { namespace LeaderboardManager {
void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t resultStart = 0, const uint32_t resultEnd = 10); void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID);
void SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0); void SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0);

View File

@ -16,6 +16,7 @@
#include "DestroyableComponent.h" #include "DestroyableComponent.h"
#include <algorithm> #include <algorithm>
#include <ranges>
#include <sstream> #include <sstream>
#include <vector> #include <vector>
@ -37,6 +38,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id):
m_Disabled = false; m_Disabled = false;
m_SkillEntries = {}; m_SkillEntries = {};
m_SoftTimer = 5.0f; m_SoftTimer = 5.0f;
m_ForcedTetherTime = 0.0f;
//Grab the aggro information from BaseCombatAI: //Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt( auto componentQuery = CDClientDatabase::CreatePreppedStmt(
@ -170,6 +172,17 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
GameMessages::SendStopFXEffect(m_Parent, true, "tether"); GameMessages::SendStopFXEffect(m_Parent, true, "tether");
m_TetherEffectActive = false; 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) { if (m_SoftTimer <= 0.0f) {
@ -287,40 +300,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
} }
if (!m_TetherEffectActive && m_OutOfCombat && (m_OutOfCombatTime -= deltaTime) <= 0) { if (!m_TetherEffectActive && m_OutOfCombat && (m_OutOfCombatTime -= deltaTime) <= 0) {
auto* destroyableComponent = m_Parent->GetComponent<DestroyableComponent>(); TetherLogic();
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);
}
m_OutOfCombat = false; m_OutOfCombat = false;
m_OutOfCombatTime = 0.0f; m_OutOfCombatTime = 0.0f;
@ -499,7 +479,7 @@ std::vector<LWOOBJID> BaseCombatAIComponent::GetTargetWithinAggroRange() const {
const auto distance = Vector3::DistanceSquared(m_Parent->GetPosition(), other->GetPosition()); 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); targets.push_back(id);
} }
@ -626,6 +606,7 @@ const NiPoint3& BaseCombatAIComponent::GetStartPosition() const {
void BaseCombatAIComponent::ClearThreat() { void BaseCombatAIComponent::ClearThreat() {
m_ThreatEntries.clear(); m_ThreatEntries.clear();
m_Target = LWOOBJID_EMPTY;
m_DirtyThreat = true; m_DirtyThreat = true;
} }
@ -806,3 +787,55 @@ void BaseCombatAIComponent::Wake() {
m_dpEntity->SetSleeping(false); m_dpEntity->SetSleeping(false);
m_dpEntityEnemy->SetSleeping(false); m_dpEntityEnemy->SetSleeping(false);
} }
void BaseCombatAIComponent::TetherLogic() {
auto* destroyableComponent = m_Parent->GetComponent<DestroyableComponent>();
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;
}

View File

@ -224,6 +224,16 @@ public:
*/ */
void Wake(); 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: private:
/** /**
* Returns the current target or the target that currently is the largest threat to this entity * 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; 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<LWOOBJID, float> m_RemovedThreatList;
/** /**
* Whether the current entity is a mech enemy, needed as mechs tether radius works differently * Whether the current entity is a mech enemy, needed as mechs tether radius works differently
* @return whether this entity is a mech * @return whether this entity is a mech

View File

@ -7,7 +7,6 @@ set(DGAME_DCOMPONENTS_SOURCES
"BuildBorderComponent.cpp" "BuildBorderComponent.cpp"
"CharacterComponent.cpp" "CharacterComponent.cpp"
"CollectibleComponent.cpp" "CollectibleComponent.cpp"
"Component.cpp"
"ControllablePhysicsComponent.cpp" "ControllablePhysicsComponent.cpp"
"DestroyableComponent.cpp" "DestroyableComponent.cpp"
"DonationVendorComponent.cpp" "DonationVendorComponent.cpp"
@ -65,7 +64,6 @@ target_include_directories(dComponents PUBLIC "."
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
"${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include"
# dPhysics (via dpWorld.h) # dPhysics (via dpWorld.h)
"${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Recast/Include" "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Recast/Include"
"${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Detour/Include" "${PROJECT_SOURCE_DIR}/thirdparty/recastnavigation/Detour/Include"

View File

@ -1,34 +0,0 @@
#include "Component.h"
Component::Component(Entity* parent) {
m_Parent = parent;
}
Component::~Component() {
}
Entity* Component::GetParent() const {
return m_Parent;
}
void Component::Update(float deltaTime) {
}
void Component::OnUse(Entity* originator) {
}
void Component::UpdateXml(tinyxml2::XMLDocument& doc) {
}
void Component::LoadFromXml(const tinyxml2::XMLDocument& doc) {
}
void Component::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {
}

View File

@ -1,6 +1,12 @@
#pragma once #pragma once
#include "tinyxml2.h" namespace tinyxml2 {
class XMLDocument;
}
namespace RakNet {
class BitStream;
}
class Entity; class Entity;
@ -9,40 +15,40 @@ class Entity;
*/ */
class Component { class Component {
public: public:
Component(Entity* parent); Component(Entity* parent) : m_Parent{ parent } {}
virtual ~Component(); virtual ~Component() = default;
/** /**
* Gets the owner of this component * Gets the owner of this component
* @return the owner of this component * @return the owner of this component
*/ */
Entity* GetParent() const; Entity* GetParent() const { return m_Parent; }
/** /**
* Updates the component in the game loop * Updates the component in the game loop
* @param deltaTime time passed since last update * @param deltaTime time passed since last update
*/ */
virtual void Update(float deltaTime); virtual void Update(float deltaTime) {}
/** /**
* Event called when this component is being used, e.g. when some entity interacted with it * Event called when this component is being used, e.g. when some entity interacted with it
* @param originator * @param originator
*/ */
virtual void OnUse(Entity* originator); virtual void OnUse(Entity* originator) {}
/** /**
* Save data from this componennt to character XML * Save data from this componennt to character XML
* @param doc the document to write data to * @param doc the document to write data to
*/ */
virtual void UpdateXml(tinyxml2::XMLDocument& doc); virtual void UpdateXml(tinyxml2::XMLDocument& doc) {}
/** /**
* Load base data for this component from character XML * Load base data for this component from character XML
* @param doc the document to read data from * @param doc the document to read data from
*/ */
virtual void LoadFromXml(const tinyxml2::XMLDocument& doc); virtual void LoadFromXml(const tinyxml2::XMLDocument& doc) {}
virtual void Serialize(RakNet::BitStream& outBitStream, bool isConstruction); virtual void Serialize(RakNet::BitStream& outBitStream, bool isConstruction) {}
protected: protected:

View File

@ -15,7 +15,7 @@
#include "LevelProgressionComponent.h" #include "LevelProgressionComponent.h"
#include "eStateChangeType.h" #include "eStateChangeType.h"
ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity) : PhysicsComponent(entity) { ControllablePhysicsComponent::ControllablePhysicsComponent(Entity* entity, int32_t componentId) : PhysicsComponent(entity, componentId) {
m_Velocity = {}; m_Velocity = {};
m_AngularVelocity = {}; m_AngularVelocity = {};
m_InJetpackMode = false; m_InJetpackMode = false;

View File

@ -23,7 +23,7 @@ class ControllablePhysicsComponent : public PhysicsComponent {
public: public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::CONTROLLABLE_PHYSICS; static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::CONTROLLABLE_PHYSICS;
ControllablePhysicsComponent(Entity* entity); ControllablePhysicsComponent(Entity* entity, int32_t componentId);
~ControllablePhysicsComponent() override; ~ControllablePhysicsComponent() override;
void Update(float deltaTime) override; void Update(float deltaTime) override;

View File

@ -1,7 +1,7 @@
#include "HavokVehiclePhysicsComponent.h" #include "HavokVehiclePhysicsComponent.h"
#include "EntityManager.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_Velocity = NiPoint3Constant::ZERO;
m_AngularVelocity = NiPoint3Constant::ZERO; m_AngularVelocity = NiPoint3Constant::ZERO;
m_IsOnGround = true; m_IsOnGround = true;

View File

@ -13,7 +13,7 @@ class HavokVehiclePhysicsComponent : public PhysicsComponent {
public: public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::HAVOK_VEHICLE_PHYSICS; 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; void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;

View File

@ -31,6 +31,7 @@
#include "eStateChangeType.h" #include "eStateChangeType.h"
#include "eUseItemResponse.h" #include "eUseItemResponse.h"
#include "Mail.h" #include "Mail.h"
#include "ProximityMonitorComponent.h"
#include "CDComponentsRegistryTable.h" #include "CDComponentsRegistryTable.h"
#include "CDInventoryComponentTable.h" #include "CDInventoryComponentTable.h"
@ -829,6 +830,30 @@ void InventoryComponent::EquipItem(Item* item, const bool skipChecks) {
break; 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<ProximityMonitorComponent>();
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; return;
} }
@ -1141,6 +1166,25 @@ void InventoryComponent::AddItemSkills(const LOT lot) {
SetSkill(slot, skill); SetSkill(slot, skill);
} }
void InventoryComponent::FixInvisibleItems() {
const auto numberItemsLoadedPerFrame = 12.0f;
const auto callbackTime = 0.125f;
const auto arbitaryInventorySize = 300.0f; // max in live + dlu is less than 300, seems like a good number.
auto* const items = GetInventory(eInventoryType::ITEMS);
if (!items) return;
// Add an extra update to make sure the client can see all the items.
const auto something = static_cast<int32_t>(std::ceil(items->GetItems().size() / arbitaryInventorySize)) + 1;
LOG_DEBUG("Fixing invisible items with %i updates", something);
for (int32_t i = 1; i < something + 1; i++) {
// client loads 12 items every 1/8 seconds, we're adding a small hack to fix invisible inventory items due to closing the news screen too fast.
m_Parent->AddCallbackTimer((arbitaryInventorySize / numberItemsLoadedPerFrame) * callbackTime * i, [this]() {
GameMessages::SendUpdateInventoryUi(m_Parent->GetObjectID(), m_Parent->GetSystemAddress());
});
}
}
void InventoryComponent::RemoveItemSkills(const LOT lot) { void InventoryComponent::RemoveItemSkills(const LOT lot) {
const auto info = Inventory::FindItemComponent(lot); const auto info = Inventory::FindItemComponent(lot);

View File

@ -404,6 +404,8 @@ public:
void UpdateGroup(const GroupUpdate& groupUpdate); void UpdateGroup(const GroupUpdate& groupUpdate);
void RemoveGroup(const std::string& groupId); void RemoveGroup(const std::string& groupId);
void FixInvisibleItems();
~InventoryComponent() override; ~InventoryComponent() override;
private: private:

View File

@ -27,7 +27,7 @@
#include "dpShapeBox.h" #include "dpShapeBox.h"
#include "dpShapeSphere.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_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation(); m_Rotation = m_Parent->GetDefaultRotation();
m_Scale = m_Parent->GetDefaultScale(); m_Scale = m_Parent->GetDefaultScale();

View File

@ -30,7 +30,7 @@ class PhantomPhysicsComponent final : public PhysicsComponent {
public: public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::PHANTOM_PHYSICS; static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::PHANTOM_PHYSICS;
PhantomPhysicsComponent(Entity* parent); PhantomPhysicsComponent(Entity* parent, int32_t componentId);
~PhantomPhysicsComponent() override; ~PhantomPhysicsComponent() override;
void Update(float deltaTime) override; void Update(float deltaTime) override;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;

View File

@ -14,10 +14,21 @@
#include "EntityInfo.h" #include "EntityInfo.h"
PhysicsComponent::PhysicsComponent(Entity* parent) : Component(parent) { PhysicsComponent::PhysicsComponent(Entity* parent, int32_t componentId) : Component(parent) {
m_Position = NiPoint3Constant::ZERO; m_Position = NiPoint3Constant::ZERO;
m_Rotation = NiQuaternionConstant::IDENTITY; m_Rotation = NiQuaternionConstant::IDENTITY;
m_DirtyPosition = false; m_DirtyPosition = false;
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable<CDPhysicsComponentTable>();
if (physicsComponentTable) {
auto* info = physicsComponentTable->GetByID(componentId);
if (info) {
m_CollisionGroup = info->collisionGroup;
}
}
if (m_Parent->HasVar(u"CollisionGroupID")) m_CollisionGroup = m_Parent->GetVar<int32_t>(u"CollisionGroupID");
} }
void PhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { void PhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {

View File

@ -15,7 +15,7 @@ class dpEntity;
class PhysicsComponent : public Component { class PhysicsComponent : public Component {
public: public:
PhysicsComponent(Entity* parent); PhysicsComponent(Entity* parent, int32_t componentId);
virtual ~PhysicsComponent() = default; virtual ~PhysicsComponent() = default;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
@ -25,6 +25,9 @@ public:
const NiQuaternion& GetRotation() const { return m_Rotation; } const NiQuaternion& GetRotation() const { return m_Rotation; }
virtual void SetRotation(const NiQuaternion& rot) { if (m_Rotation == rot) return; m_Rotation = rot; m_DirtyPosition = true; } 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: protected:
dpEntity* CreatePhysicsEntity(eReplicaComponentType type); dpEntity* CreatePhysicsEntity(eReplicaComponentType type);
@ -37,6 +40,8 @@ protected:
NiQuaternion m_Rotation; NiQuaternion m_Rotation;
bool m_DirtyPosition; bool m_DirtyPosition;
int32_t m_CollisionGroup{};
}; };
#endif //!__PHYSICSCOMPONENT__H__ #endif //!__PHYSICSCOMPONENT__H__

View File

@ -38,7 +38,7 @@ void ProximityMonitorComponent::SetProximityRadius(dpEntity* entity, const std::
m_ProximitiesData.insert(std::make_pair(name, entity)); m_ProximitiesData.insert(std::make_pair(name, entity));
} }
const std::unordered_set<LWOOBJID>& ProximityMonitorComponent::GetProximityObjects(const std::string& name) { const std::unordered_set<LWOOBJID>& ProximityMonitorComponent::GetProximityObjects(const std::string& name) const {
const auto iter = m_ProximitiesData.find(name); const auto iter = m_ProximitiesData.find(name);
if (iter == m_ProximitiesData.cend()) { if (iter == m_ProximitiesData.cend()) {

View File

@ -46,7 +46,7 @@ public:
* @param name the proximity name to retrieve physics objects for * @param name the proximity name to retrieve physics objects for
* @return a set of physics entity object IDs for this name * @return a set of physics entity object IDs for this name
*/ */
const std::unordered_set<LWOOBJID>& GetProximityObjects(const std::string& name); const std::unordered_set<LWOOBJID>& GetProximityObjects(const std::string& name) const;
/** /**
* Checks if the passed object is in proximity of the named proximity sensor * Checks if the passed object is in proximity of the named proximity sensor

View File

@ -12,7 +12,7 @@
#include "dpShapeSphere.h" #include "dpShapeSphere.h"
#include"EntityInfo.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_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation(); m_Rotation = m_Parent->GetDefaultRotation();
m_Scale = m_Parent->GetDefaultScale(); m_Scale = m_Parent->GetDefaultScale();

View File

@ -21,7 +21,7 @@ class RigidbodyPhantomPhysicsComponent : public PhysicsComponent {
public: public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS; static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::RIGID_BODY_PHANTOM_PHYSICS;
RigidbodyPhantomPhysicsComponent(Entity* parent); RigidbodyPhantomPhysicsComponent(Entity* parent, int32_t componentId);
void Update(const float deltaTime) override; void Update(const float deltaTime) override;

View File

@ -13,7 +13,7 @@
#include "Entity.h" #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_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation(); m_Rotation = m_Parent->GetDefaultRotation();

View File

@ -30,7 +30,7 @@ class SimplePhysicsComponent : public PhysicsComponent {
public: public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::SIMPLE_PHYSICS; static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::SIMPLE_PHYSICS;
SimplePhysicsComponent(Entity* parent, uint32_t componentID); SimplePhysicsComponent(Entity* parent, int32_t componentID);
~SimplePhysicsComponent() override; ~SimplePhysicsComponent() override;
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;

View File

@ -82,7 +82,6 @@ void SwitchComponent::EntityEnter(Entity* entity) {
RenderComponent::PlayAnimation(m_Parent, u"engaged"); RenderComponent::PlayAnimation(m_Parent, u"engaged");
m_PetBouncer->SetPetBouncerEnabled(true); m_PetBouncer->SetPetBouncerEnabled(true);
} else { } else {
GameMessages::SendKnockback(entity->GetObjectID(), m_Parent->GetObjectID(), m_Parent->GetObjectID(), 0.0f, NiPoint3(0.0f, 17.0f, 0.0f));
Game::entityManager->SerializeEntity(m_Parent); Game::entityManager->SerializeEntity(m_Parent);
} }

View File

@ -104,6 +104,18 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
break; break;
} }
// Currently not actually used for our implementation, however its used right now to get around invisible inventory items in the client.
case MessageType::Game::SELECT_SKILL: {
auto var = entity->GetVar<bool>(u"dlu_first_time_load");
if (var) {
entity->SetVar<bool>(u"dlu_first_time_load", false);
InventoryComponent* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) inventoryComponent->FixInvisibleItems();
}
break;
}
case MessageType::Game::PLAYER_LOADED: { case MessageType::Game::PLAYER_LOADED: {
GameMessages::SendRestoreToPostLoadStats(entity, sysAddr); GameMessages::SendRestoreToPostLoadStats(entity, sysAddr);
entity->SetPlayerReadyForUpdates(); entity->SetPlayerReadyForUpdates();

View File

@ -1691,7 +1691,7 @@ void GameMessages::HandleRequestActivitySummaryLeaderboardData(RakNet::BitStream
bool weekly = inStream.ReadBit(); bool weekly = inStream.ReadBit();
LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID(), resultsStart, resultsEnd); LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID());
} }
void GameMessages::HandleActivityStateChangeRequest(RakNet::BitStream& inStream, Entity* entity) { void GameMessages::HandleActivityStateChangeRequest(RakNet::BitStream& inStream, Entity* entity) {
@ -5066,9 +5066,7 @@ void GameMessages::HandleModularBuildConvertModel(RakNet::BitStream& inStream, E
item->Disassemble(TEMP_MODELS); item->Disassemble(TEMP_MODELS);
std::unique_ptr<sql::PreparedStatement> stmt(Database::Get()->CreatePreppedStmt("DELETE FROM ugc_modular_build where ugc_id = ?")); Database::Get()->DeleteUgcBuild(item->GetSubKey());
stmt->setUInt64(1, item->GetSubKey());
stmt->execute();
item->SetCount(item->GetCount() - 1, false, false, true, eLootSourceType::QUICKBUILD); item->SetCount(item->GetCount() - 1, false, false, true, eLootSourceType::QUICKBUILD);
} }
@ -5082,6 +5080,12 @@ void GameMessages::HandleSetFlag(RakNet::BitStream& inStream, Entity* entity) {
auto character = entity->GetCharacter(); auto character = entity->GetCharacter();
if (character) character->SetPlayerFlag(iFlagID, bFlag); if (character) character->SetPlayerFlag(iFlagID, bFlag);
// This is always set the first time a player loads into a world from character select
// and is used to know when to refresh the players inventory items so they show up.
if (iFlagID == ePlayerFlag::IS_NEWS_SCREEN_VISIBLE && bFlag) {
entity->SetVar<bool>(u"dlu_first_time_load", true);
}
} }
void GameMessages::HandleRespondToMission(RakNet::BitStream& inStream, Entity* entity) { void GameMessages::HandleRespondToMission(RakNet::BitStream& inStream, Entity* entity) {
@ -5394,6 +5398,8 @@ void GameMessages::HandleRemoveItemFromInventory(RakNet::BitStream& inStream, En
const auto itemType = static_cast<eItemType>(item->GetInfo().itemType); const auto itemType = static_cast<eItemType>(item->GetInfo().itemType);
if (itemType == eItemType::MODEL || itemType == eItemType::LOOT_MODEL) { if (itemType == eItemType::MODEL || itemType == eItemType::LOOT_MODEL) {
item->DisassembleModel(iStackCount); item->DisassembleModel(iStackCount);
} else if (itemType == eItemType::VEHICLE) {
Database::Get()->DeleteUgcBuild(item->GetSubKey());
} }
auto lot = item->GetLot(); auto lot = item->GetLot();
item->SetCount(item->GetCount() - iStackCount, true); item->SetCount(item->GetCount() - iStackCount, true);
@ -5569,12 +5575,8 @@ void GameMessages::HandleModularBuildFinish(RakNet::BitStream& inStream, Entity*
inv->AddItem(8092, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newIdBig); inv->AddItem(8092, 1, eLootSourceType::QUICKBUILD, eInventoryType::MODELS, config, LWOOBJID_EMPTY, true, false, newIdBig);
} }
std::unique_ptr<sql::PreparedStatement> stmt(Database::Get()->CreatePreppedStmt("INSERT INTO ugc_modular_build (ugc_id, ldf_config, character_id) VALUES (?,?,?)"));
stmt->setUInt64(1, newIdBig);
stmt->setString(2, GeneralUtils::UTF16ToWTF8(modules).c_str());
auto* pCharacter = character->GetCharacter(); auto* pCharacter = character->GetCharacter();
pCharacter ? stmt->setUInt(3, pCharacter->GetID()) : stmt->setNull(3, sql::DataType::BIGINT); Database::Get()->InsertUgcBuild(GeneralUtils::UTF16ToWTF8(modules), newIdBig, pCharacter ? std::optional(character->GetCharacter()->GetID()) : std::nullopt);
stmt->execute();
auto* missionComponent = character->GetComponent<MissionComponent>(); auto* missionComponent = character->GetComponent<MissionComponent>();
@ -6328,3 +6330,69 @@ void GameMessages::SendForceCameraTargetCycle(Entity* entity, bool bForceCycling
auto sysAddr = entity->GetSystemAddress(); auto sysAddr = entity->GetSystemAddress();
SEND_PACKET; SEND_PACKET;
} }
void GameMessages::SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr) {
CBITSTREAM;
CMSGHEADER;
bitStream.Write(objectId);
bitStream.Write(MessageType::Game::UPDATE_INVENTORY_UI);
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<int32_t>(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<int32_t>(toWrite.size());
bitStream.Write(GeneralUtils::ASCIIToUTF16(toWrite));
if (!toWrite.empty()) bitStream.Write<uint16_t>(0x00); // Null Terminator
bitStream.Write<int32_t>(imageName.size());
bitStream.Write(imageName);
bitStream.Write<int32_t>(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);
}
}

View File

@ -11,6 +11,7 @@
#include "eCyclingMode.h" #include "eCyclingMode.h"
#include "eLootSourceType.h" #include "eLootSourceType.h"
#include "Brick.h" #include "Brick.h"
#include "MessageType/Game.h"
class AMFBaseValue; class AMFBaseValue;
class Entity; class Entity;
@ -20,6 +21,7 @@ class User;
class Leaderboard; class Leaderboard;
class PropertySelectQueryProperty; class PropertySelectQueryProperty;
class TradeItem; class TradeItem;
class LDFBaseData;
enum class eAnimationFlags : uint32_t; enum class eAnimationFlags : uint32_t;
@ -47,6 +49,15 @@ enum class eCameraTargetCyclingMode : int32_t {
}; };
namespace GameMessages { 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; 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 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); void SendTeleport(const LWOOBJID& objectID, const NiPoint3& pos, const NiQuaternion& rot, const SystemAddress& sysAddr, bool bSetRotation = false);
@ -677,6 +688,35 @@ namespace GameMessages {
void HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
void HandleUpdateInventoryGroupContents(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleUpdateInventoryGroupContents(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr);
void SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID); void SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID);
// 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<LDFBaseData*> 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 #endif // GAMEMESSAGES_H

View File

@ -8,7 +8,6 @@ class AMFArrayValue;
/** /**
* @brief Sent when a player moves a Behavior A at position B to their inventory. * @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 { class MoveToInventoryMessage : public BehaviorMessageBase {
public: public:
MoveToInventoryMessage(const AMFArrayValue& arguments); MoveToInventoryMessage(const AMFArrayValue& arguments);

View File

@ -137,7 +137,7 @@ bool Precondition::CheckValue(Entity* player, const uint32_t value, bool evaluat
return inventoryComponent->GetLotCount(value) >= count; return inventoryComponent->GetLotCount(value) >= count;
case PreconditionType::DoesNotHaveItem: case PreconditionType::DoesNotHaveItem:
return inventoryComponent->IsEquipped(value) < count; return inventoryComponent->IsEquipped(value) && count > 0;
case PreconditionType::HasAchievement: case PreconditionType::HasAchievement:
if (missionComponent == nullptr) return false; if (missionComponent == nullptr) return false;
return missionComponent->GetMissionState(value) >= eMissionState::COMPLETE; return missionComponent->GetMissionState(value) >= eMissionState::COMPLETE;

View File

@ -287,8 +287,8 @@ void SlashCommandHandler::Startup() {
RegisterCommand(SpawnPhysicsVertsCommand); RegisterCommand(SpawnPhysicsVertsCommand);
Command TeleportCommand{ Command TeleportCommand{
.help = "Teleports you", .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)", .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" }, .aliases = { "teleport", "tele", "tp" },
.handle = DEVGMCommands::Teleport, .handle = DEVGMCommands::Teleport,
.requiredLevel = eGameMasterLevel::JUNIOR_DEVELOPER .requiredLevel = eGameMasterLevel::JUNIOR_DEVELOPER
@ -1056,6 +1056,15 @@ void SlashCommandHandler::Startup() {
}; };
RegisterCommand(InstanceInfoCommand); 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 //Commands that are handled by the client
Command faqCommand{ Command faqCommand{

View File

@ -555,25 +555,45 @@ namespace DEVGMCommands {
} }
} }
std::optional<float> 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<float>(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<float>(toParse);
}
void Teleport(Entity* entity, const SystemAddress& sysAddr, const std::string args) { void Teleport(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' '); const auto splitArgs = GeneralUtils::SplitString(args, ' ');
const auto& sourcePos = entity->GetPosition();
NiPoint3 pos{}; NiPoint3 pos{};
auto* sourceEntity = entity;
if (splitArgs.size() == 3) { if (splitArgs.size() == 3) {
const auto x = ParseRelativeAxis(sourcePos.x, splitArgs[0]);
const auto x = GeneralUtils::TryParse<float>(splitArgs.at(0));
if (!x) { if (!x) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid x."); ChatPackets::SendSystemMessage(sysAddr, u"Invalid x.");
return; return;
} }
const auto y = GeneralUtils::TryParse<float>(splitArgs.at(1)); const auto y = ParseRelativeAxis(sourcePos.y, splitArgs[1]);
if (!y) { if (!y) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid y."); ChatPackets::SendSystemMessage(sysAddr, u"Invalid y.");
return; return;
} }
const auto z = GeneralUtils::TryParse<float>(splitArgs.at(2)); const auto z = ParseRelativeAxis(sourcePos.z, splitArgs[2]);
if (!z) { if (!z) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid z."); ChatPackets::SendSystemMessage(sysAddr, u"Invalid z.");
return; return;
@ -584,32 +604,39 @@ namespace DEVGMCommands {
pos.SetZ(z.value()); pos.SetZ(z.value());
LOG("Teleporting objectID: %llu to %f, %f, %f", entity->GetObjectID(), pos.x, pos.y, pos.z); 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) { } 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<float>(splitArgs.at(0)); const auto z = ParseRelativeAxis(sourcePos.z, splitArgs[1]);
if (!x) { const auto* const targetPlayer = PlayerManager::GetPlayer(splitArgs[1]);
ChatPackets::SendSystemMessage(sysAddr, u"Invalid x."); if (!z && !targetPlayer) {
return; ChatPackets::SendSystemMessage(sysAddr, u"Invalid z or target player not found.");
}
const auto z = GeneralUtils::TryParse<float>(splitArgs.at(1));
if (!z) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid z.");
return; return;
} }
if (x && z) {
pos.SetX(x.value()); pos.SetX(x.value());
pos.SetY(0.0f); pos.SetY(0.0f);
pos.SetZ(z.value()); pos.SetZ(z.value());
} else if (sourcePlayer && targetPlayer) {
pos = targetPlayer->GetPosition();
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Unable to teleport.");
return;
}
LOG("Teleporting objectID: %llu to X: %f, Z: %f", entity->GetObjectID(), pos.x, pos.z); LOG("Teleporting objectID: %llu to X: %f, Z: %f", entity->GetObjectID(), pos.x, pos.z);
GameMessages::SendTeleport(entity->GetObjectID(), pos, NiQuaternion(), sysAddr);
} else { } else {
ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /teleport <x> (<y>) <z> - if no Y given, will teleport to the height of the terrain (or any physics object)."); ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /teleport <x> (<y>) <z> - 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<PossessorComponent>(); auto* possessorComponent = sourceEntity->GetComponent<PossessorComponent>();
if (possessorComponent) { if (possessorComponent) {
auto* possassableEntity = Game::entityManager->GetEntity(possessorComponent->GetPossessable()); auto* possassableEntity = Game::entityManager->GetEntity(possessorComponent->GetPossessable());

View File

@ -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()))); 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<std::chrono::seconds>(time).count();
ChatPackets::SendSystemMessage(sysAddr, u"Server has been up for " + GeneralUtils::to_u16string(seconds) + u" s");
}
//For client side commands //For client side commands
void ClientHandled(Entity* entity, const SystemAddress& sysAddr, const std::string args) {} void ClientHandled(Entity* entity, const SystemAddress& sysAddr, const std::string args) {}
}; };

View File

@ -15,6 +15,7 @@ namespace GMZeroCommands {
void LeaveZone(Entity* entity, const SystemAddress& sysAddr, const std::string args); void LeaveZone(Entity* entity, const SystemAddress& sysAddr, const std::string args);
void Resurrect(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 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); void ClientHandled(Entity* entity, const SystemAddress& sysAddr, const std::string args);
} }

View File

@ -403,26 +403,39 @@ void SetupNPCTalk(Entity* npc) {
npc->SetProximityRadius(20.0f, "talk"); npc->SetProximityRadius(20.0f, "talk");
} }
void NPCTalk(Entity* npc) { void VanityUtilities::OnProximityUpdate(Entity* entity, Entity* other, const std::string& proxName, const std::string& name) {
auto* proximityMonitorComponent = npc->GetComponent<ProximityMonitorComponent>(); if (proxName != "talk") return;
const auto* const proximityMonitorComponent = entity->GetComponent<ProximityMonitorComponent>();
if (!proximityMonitorComponent) return;
if (!proximityMonitorComponent->GetProximityObjects("talk").empty()) { if (name == "ENTER" && !entity->HasTimer("talk")) {
NPCTalk(entity);
}
}
void VanityUtilities::OnTimerDone(Entity* npc, const std::string& name) {
if (name == "talk") {
const auto* const proximityMonitorComponent = npc->GetComponent<ProximityMonitorComponent>();
if (!proximityMonitorComponent || proximityMonitorComponent->GetProximityObjects("talk").empty()) return;
NPCTalk(npc);
}
}
void NPCTalk(Entity* npc) {
const auto& chats = npc->GetVar<std::vector<std::string>>(u"chats"); const auto& chats = npc->GetVar<std::vector<std::string>>(u"chats");
if (chats.empty()) { if (chats.empty()) return;
return;
}
const auto& selected const auto& selected
= chats[GeneralUtils::GenerateRandomNumber<int32_t>(0, static_cast<int32_t>(chats.size() - 1))]; = chats[GeneralUtils::GenerateRandomNumber<int32_t>(0, static_cast<int32_t>(chats.size() - 1))];
GameMessages::SendNotifyClientZoneObject( GameMessages::SendNotifyClientZoneObject(
npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS); npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS);
}
Game::entityManager->SerializeEntity(npc); Game::entityManager->SerializeEntity(npc);
const float nextTime = GeneralUtils::GenerateRandomNumber<float>(15, 60); const float nextTime = GeneralUtils::GenerateRandomNumber<float>(15, 60);
npc->AddCallbackTimer(nextTime, [npc]() { NPCTalk(npc); }); npc->AddTimer("talk", nextTime);
} }

View File

@ -31,4 +31,8 @@ namespace VanityUtilities {
std::string ParseMarkdown( std::string ParseMarkdown(
const std::string& file const std::string& file
); );
void OnProximityUpdate(Entity* entity, Entity* other, const std::string& proxName, const std::string& name);
void OnTimerDone(Entity* entity, const std::string& name);
}; };

View File

@ -103,7 +103,7 @@ int main(int argc, char** argv) {
//Connect to the MySQL Database //Connect to the MySQL Database
try { try {
Database::Connect(); Database::Connect();
} catch (sql::SQLException& ex) { } catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what()); LOG("Got an error while connecting to the database: %s", ex.what());
LOG("Migrations not run"); LOG("Migrations not run");
return EXIT_FAILURE; return EXIT_FAILURE;
@ -126,6 +126,7 @@ int main(int argc, char** argv) {
MigrationRunner::RunMigrations(); MigrationRunner::RunMigrations();
const auto resServerPath = BinaryPathFinder::GetBinaryDir() / "resServer"; const auto resServerPath = BinaryPathFinder::GetBinaryDir() / "resServer";
std::filesystem::create_directories(resServerPath);
const bool cdServerExists = std::filesystem::exists(resServerPath / "CDServer.sqlite"); const bool cdServerExists = std::filesystem::exists(resServerPath / "CDServer.sqlite");
const bool oldCDServerExists = std::filesystem::exists(Game::assetManager->GetResPath() / "CDServer.sqlite"); const bool oldCDServerExists = std::filesystem::exists(Game::assetManager->GetResPath() / "CDServer.sqlite");
const bool fdbExists = std::filesystem::exists(Game::assetManager->GetResPath() / "cdclient.fdb"); const bool fdbExists = std::filesystem::exists(Game::assetManager->GetResPath() / "cdclient.fdb");
@ -180,8 +181,12 @@ int main(int argc, char** argv) {
//If the first command line argument is -a or --account then make the user //If the first command line argument is -a or --account then make the user
//input a username and password, with the password being hidden. //input a username and password, with the password being hidden.
if (argc > 1 && bool createAccount = Database::Get()->GetAccountCount() == 0 && Game::config->GetValue("skip_account_creation") != "1";
(strcmp(argv[1], "-a") == 0 || strcmp(argv[1], "--account") == 0)) { 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 username;
std::string password; std::string password;
@ -264,7 +269,7 @@ int main(int argc, char** argv) {
//Create account //Create account
try { try {
Database::Get()->InsertNewAccount(username, std::string(hash, BCRYPT_HASHSIZE)); Database::Get()->InsertNewAccount(username, std::string(hash, BCRYPT_HASHSIZE));
} catch (sql::SQLException& e) { } catch (std::exception& e) {
LOG("A SQL error occurred!:\n %s", e.what()); LOG("A SQL error occurred!:\n %s", e.what());
return EXIT_FAILURE; return EXIT_FAILURE;
} }

View File

@ -24,9 +24,9 @@ void PersistentIDManager::Initialize() {
LOG("Invalid persistent object ID in database. Aborting to prevent bad id generation."); LOG("Invalid persistent object ID in database. Aborting to prevent bad id generation.");
throw std::runtime_error("Invalid persistent object ID in database. Aborting to prevent bad id generation."); throw std::runtime_error("Invalid persistent object ID in database. Aborting to prevent bad id generation.");
} }
} catch (sql::SQLException& e) { } catch (std::exception& e) {
LOG("Unable to fetch max persistent object ID in use. This will cause issues. Aborting to prevent collisions."); LOG("Unable to fetch max persistent object ID in use. This will cause issues. Aborting to prevent collisions.");
LOG("SQL error: %s", e.what()); LOG("Error: %s", e.what());
throw e; throw e;
} }
} }

View File

@ -13,7 +13,7 @@ void StartChatServer() {
//macOS doesn't need sudo to run on ports < 1024 //macOS doesn't need sudo to run on ports < 1024
auto result = system(((BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str()); auto result = system(((BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str());
#elif _WIN32 #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 #else
if (std::atoi(Game::config->GetValue("use_sudo_chat").c_str())) { if (std::atoi(Game::config->GetValue("use_sudo_chat").c_str())) {
auto result = system(("sudo " + (BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str()); auto result = system(("sudo " + (BinaryPathFinder::GetBinaryDir() / "ChatServer").string() + "&").c_str());
@ -31,7 +31,7 @@ void StartAuthServer() {
#ifdef __APPLE__ #ifdef __APPLE__
auto result = system(((BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").c_str()); auto result = system(((BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").c_str());
#elif _WIN32 #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 #else
if (std::atoi(Game::config->GetValue("use_sudo_auth").c_str())) { if (std::atoi(Game::config->GetValue("use_sudo_auth").c_str())) {
auto result = system(("sudo " + (BinaryPathFinder::GetBinaryDir() / "AuthServer").string() + "&").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) { void StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID) {
#ifdef _WIN32 #ifdef _WIN32
std::string cmd = "start " + (BinaryPathFinder::GetBinaryDir() / "WorldServer.exe").string() + " -zone "; std::string cmd = "start /B " + (BinaryPathFinder::GetBinaryDir() / "WorldServer.exe").string() + " -zone ";
#else #else
std::string cmd; std::string cmd;
if (std::atoi(Game::config->GetValue("use_sudo_world").c_str())) { if (std::atoi(Game::config->GetValue("use_sudo_world").c_str())) {

View File

@ -82,11 +82,18 @@ void AuthPackets::SendHandshake(dServer* server, const SystemAddress& sysAddr, c
if (serverType == ServerType::Auth) bitStream.Write(ServiceId::Auth); if (serverType == ServerType::Auth) bitStream.Write(ServiceId::Auth);
else if (serverType == ServerType::World) bitStream.Write(ServiceId::World); else if (serverType == ServerType::World) bitStream.Write(ServiceId::World);
else bitStream.Write(ServiceId::General); else bitStream.Write(ServiceId::General);
bitStream.Write<uint64_t>(215523470896); bitStream.Write<uint64_t>(219818241584);
server->Send(bitStream, sysAddr, false); server->Send(bitStream, sysAddr, false);
} }
std::string CleanReceivedString(const std::string& str) {
std::string toReturn = str;
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;
}
void AuthPackets::HandleLoginRequest(dServer* server, Packet* packet) { void AuthPackets::HandleLoginRequest(dServer* server, Packet* packet) {
CINSTREAM_SKIP_HEADER; CINSTREAM_SKIP_HEADER;
@ -111,11 +118,11 @@ void AuthPackets::HandleLoginRequest(dServer* server, Packet* packet) {
LUWString memoryStats(256); LUWString memoryStats(256);
inStream.Read(memoryStats); inStream.Read(memoryStats);
LOG_DEBUG("Memory Stats [%s]", memoryStats.GetAsString().c_str()); LOG_DEBUG("Memory Stats [%s]", CleanReceivedString(memoryStats.GetAsString()).c_str());
LUWString videoCard(128); LUWString videoCard(128);
inStream.Read(videoCard); inStream.Read(videoCard);
LOG_DEBUG("VideoCard Info: [%s]", videoCard.GetAsString().c_str()); LOG_DEBUG("VideoCard Info: [%s]", CleanReceivedString(videoCard.GetAsString()).c_str());
// Processor/CPU info // Processor/CPU info
uint32_t numOfProcessors; uint32_t numOfProcessors;

View File

@ -19,7 +19,6 @@ target_include_directories(dNet PRIVATE
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables" "${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables" "${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
"${PROJECT_SOURCE_DIR}/thirdparty/mariadb-connector-cpp/include"
"${PROJECT_SOURCE_DIR}/dGame" # UserManager.h "${PROJECT_SOURCE_DIR}/dGame" # UserManager.h
"${PROJECT_SOURCE_DIR}/dGame/dComponents" "${PROJECT_SOURCE_DIR}/dGame/dComponents"

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <string> #include <string>
#include <chrono>
#include <csignal> #include <csignal>
#include "RakPeerInterface.h" #include "RakPeerInterface.h"
#include "ReplicaManager.h" #include "ReplicaManager.h"
@ -80,6 +81,11 @@ public:
const ServerType GetServerType() const { return mServerType; } const ServerType GetServerType() const { return mServerType; }
[[nodiscard]]
std::chrono::steady_clock::duration GetUptime() const {
return std::chrono::steady_clock::now() - mStartTime;
}
private: private:
bool Startup(); bool Startup();
void Shutdown(); void Shutdown();
@ -114,4 +120,5 @@ protected:
SystemAddress mMasterSystemAddress; SystemAddress mMasterSystemAddress;
std::string mMasterIP; std::string mMasterIP;
int mMasterPort; int mMasterPort;
std::chrono::steady_clock::time_point mStartTime = std::chrono::steady_clock::now();
}; };

View File

@ -3,104 +3,107 @@
#include "ScriptedActivityComponent.h" #include "ScriptedActivityComponent.h"
#include "GameMessages.h" #include "GameMessages.h"
#include "LeaderboardManager.h" #include "LeaderboardManager.h"
#include "dServer.h"
#include "eMissionTaskType.h" #include "eMissionTaskType.h"
#include "eMissionState.h" #include "eMissionState.h"
#include "MissionComponent.h" #include "MissionComponent.h"
#include <ctime> #include <chrono>
void NpcAgCourseStarter::OnStartup(Entity* self) { void NpcAgCourseStarter::OnStartup(Entity* self) {}
}
void NpcAgCourseStarter::OnUse(Entity* self, Entity* user) { void NpcAgCourseStarter::OnUse(Entity* self, Entity* user) {
auto* scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>(); auto* const scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>();
if (!scriptedActivityComponent) return;
if (scriptedActivityComponent == nullptr) { const auto selfId = self->GetObjectID();
return; const auto userId = user->GetObjectID();
} const auto& userSysAddr = user->GetSystemAddress();
if (scriptedActivityComponent->GetActivityPlayerData(user->GetObjectID()) != nullptr) { if (scriptedActivityComponent->GetActivityPlayerData(userId) != nullptr) {
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"exit", 0, 0, LWOOBJID_EMPTY, "", user->GetSystemAddress()); GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", userSysAddr);
} else { } 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) { void NpcAgCourseStarter::OnMessageBoxResponse(Entity* self, Entity* sender, int32_t button, const std::u16string& identifier, const std::u16string& userData) {
auto* scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>(); auto* const scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>();
if (!scriptedActivityComponent) return;
if (scriptedActivityComponent == nullptr) { const auto selfId = self->GetObjectID();
return; const auto senderId = sender->GetObjectID();
} const auto& senderSysAddr = sender->GetSystemAddress();
if (identifier == u"player_dialog_cancel_course" && button == 1) { 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(senderId);
scriptedActivityComponent->RemoveActivityPlayerData(sender->GetObjectID());
Game::entityManager->SerializeEntity(self); Game::entityManager->SerializeEntity(self);
} else if (identifier == u"player_dialog_start_course" && button == 1) { } else if (identifier == u"player_dialog_start_course" && button == 1) {
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"start_timer", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress()); GameMessages::SendNotifyClientObject(selfId, u"start_timer", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
GameMessages::SendActivityStart(selfId, senderSysAddr);
GameMessages::SendActivityStart(self->GetObjectID(), sender->GetSystemAddress());
auto* data = scriptedActivityComponent->AddActivityPlayerData(sender->GetObjectID());
auto* const data = scriptedActivityComponent->AddActivityPlayerData(senderId);
if (data->values[1] != 0) return; if (data->values[1] != 0) return;
// Convert to 32 bit time (Note: could try and fix the 2038 problem here by using a different epoch maybe?) const auto raceStartTime = Game::server->GetUptime() + std::chrono::seconds(4); // Offset for starting timer
const time_t startTime = std::time(0) + 4; // Offset for starting timer const auto fRaceStartTime = std::chrono::duration<float, std::ratio<1>>(raceStartTime).count();
data->values[1] = std::bit_cast<float>(static_cast<int32_t>(startTime)); data->values[1] = fRaceStartTime;
Game::entityManager->SerializeEntity(self); Game::entityManager->SerializeEntity(self);
} else if (identifier == u"FootRaceCancel") { } 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) { if (scriptedActivityComponent->GetActivityPlayerData(senderId) != nullptr) {
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"exit", 0, 0, LWOOBJID_EMPTY, "", sender->GetSystemAddress()); GameMessages::SendNotifyClientObject(selfId, u"exit", 0, 0, LWOOBJID_EMPTY, "", senderSysAddr);
} else { } 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) { void NpcAgCourseStarter::OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) {
auto* scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>(); auto* const scriptedActivityComponent = self->GetComponent<ScriptedActivityComponent>();
if (scriptedActivityComponent == nullptr) return; if (!scriptedActivityComponent) return;
auto* data = scriptedActivityComponent->GetActivityPlayerData(sender->GetObjectID()); const auto selfId = self->GetObjectID();
if (data == nullptr) return; const auto senderId = sender->GetObjectID();
const auto& senderSysAddr = sender->GetSystemAddress();
auto* const data = scriptedActivityComponent->GetActivityPlayerData(senderId);
if (!data) return;
if (args == "course_cancel") { if (args == "course_cancel") {
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"cancel_timer", 0, 0, GameMessages::SendNotifyClientObject(selfId, u"cancel_timer", 0, 0,
LWOOBJID_EMPTY, "", sender->GetSystemAddress()); LWOOBJID_EMPTY, "", senderSysAddr);
scriptedActivityComponent->RemoveActivityPlayerData(sender->GetObjectID()); scriptedActivityComponent->RemoveActivityPlayerData(senderId);
} else if (args == "course_finish") { } else if (args == "course_finish") {
const time_t endTime = std::time(0);
const time_t startTime = std::bit_cast<int32_t>(data->values[1]);
const time_t finish = endTime - startTime;
data->values[2] = std::bit_cast<float>(static_cast<int32_t>(finish));
auto* missionComponent = sender->GetComponent<MissionComponent>(); const auto raceEndTime = Game::server->GetUptime();
const auto fRaceEndTime = std::chrono::duration<float, std::ratio<1>>(raceEndTime).count();
const auto raceTimeElapsed = fRaceEndTime - data->values[1];
data->values[2] = raceTimeElapsed;
auto* const missionComponent = sender->GetComponent<MissionComponent>();
if (missionComponent != nullptr) { if (missionComponent != nullptr) {
missionComponent->ForceProgressTaskType(1884, 1, 1, false); missionComponent->ForceProgressTaskType(1884, 1, 1, false);
missionComponent->Progress(eMissionTaskType::PERFORM_ACTIVITY, -finish, self->GetObjectID(), missionComponent->Progress(eMissionTaskType::PERFORM_ACTIVITY, -raceTimeElapsed, selfId,
"performact_time"); "performact_time");
} }
Game::entityManager->SerializeEntity(self); Game::entityManager->SerializeEntity(self);
LeaderboardManager::SaveScore(sender->GetObjectID(), scriptedActivityComponent->GetActivityID(), static_cast<float>(finish)); LeaderboardManager::SaveScore(senderId, scriptedActivityComponent->GetActivityID(), raceTimeElapsed);
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"ToggleLeaderBoard", GameMessages::SendNotifyClientObject(selfId, u"ToggleLeaderBoard",
scriptedActivityComponent->GetActivityID(), 0, sender->GetObjectID(), scriptedActivityComponent->GetActivityID(), 0, senderId,
"", sender->GetSystemAddress()); "", senderSysAddr);
GameMessages::SendNotifyClientObject(self->GetObjectID(), u"stop_timer", 1, finish, LWOOBJID_EMPTY, "", GameMessages::SendNotifyClientObject(selfId, u"stop_timer", 1, raceTimeElapsed, LWOOBJID_EMPTY, "",
sender->GetSystemAddress()); senderSysAddr);
scriptedActivityComponent->RemoveActivityPlayerData(sender->GetObjectID()); scriptedActivityComponent->RemoveActivityPlayerData(senderId);
} }
} }

View File

@ -2,6 +2,7 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_GENERAL
"BankInteractServer.cpp" "BankInteractServer.cpp"
"BaseInteractDropLootServer.cpp" "BaseInteractDropLootServer.cpp"
"Binoculars.cpp" "Binoculars.cpp"
"EnemyClearThreat.cpp"
"ExplodingAsset.cpp" "ExplodingAsset.cpp"
"FrictionVolumeServer.cpp" "FrictionVolumeServer.cpp"
"ForceVolumeServer.cpp" "ForceVolumeServer.cpp"

Some files were not shown because too many files have changed in this diff Show More