Compare commits

..

57 Commits

Author SHA1 Message Date
jadebenn
1dbaf6a2bc enable sanitizer on Mac 2024-12-31 19:42:59 -06:00
jadebenn
4860410f6f Merge branch 'main' into ub-fixes 2024-12-31 15:12:55 -06:00
David Markowitz
0b261e934f fix shooting gallery bugs (#1702) 2024-12-29 18:21:22 -06:00
David Markowitz
1b9f7e44c7 remove dead loop (#1700) 2024-12-28 17:11:44 -06:00
David Markowitz
08a168de88 update cdclient.fdb file check (#1699) 2024-12-27 22:15:32 -06:00
jadebenn
701fc8061b Merge remote-tracking branch 'origin/main' into ub-fixes 2024-12-26 17:16:48 -06:00
David Markowitz
0c948a8df6 use simpler converter (#1695) 2024-12-24 22:23:14 -08:00
Gie "Max" Vanommeslaeghe
6ed6efa921 Merge pull request #1694 from DarkflameUniverse/latin1
fix: use encoding on latin1 strings from cdclient
2024-12-25 00:27:00 +01:00
Gie "Max" Vanommeslaeghe
dcc9e023a6 Merge pull request #1693 from DarkflameUniverse/bandwidth
fix: remove bandwidth limit
2024-12-25 00:26:51 +01:00
Gie "Max" Vanommeslaeghe
8509ec8856 Merge pull request #1692 from DarkflameUniverse/really
fix: folder and file checks
2024-12-25 00:25:48 +01:00
David Markowitz
18295017c1 use encoding
use template function

Update GeneralUtils.cpp

consolidate duplicate code

Update GeneralUtils.cpp

Update BinaryIO.cpp

compilers
2024-12-24 14:32:08 -08:00
David Markowitz
e8f011b830 Update sharedconfig.ini 2024-12-24 13:07:55 -08:00
David Markowitz
a787673baf show error box for windows 2024-12-24 12:57:20 -08:00
David Markowitz
e869c0ad03 Add parenthesis around path 2024-12-24 12:39:18 -08:00
David Markowitz
b2af3fa9d4 use binary dir paths, create ones that dont exist, do not run if critical ones do not exist. 2024-12-24 12:36:54 -08:00
David Markowitz
2560bb00da feat: add ns race server script and ignore 3 scripts from pet cove (#1682)
* brother

* use some better logic

* Implement spider boss msg script

tested that the message now shows up when hitting the survival spider entrance area

* add drag to start race feature

* ignore 3 more scripts

* add Ns race server script

* remove logs

* unique

* Update RaceImaginationServer.cpp

* Update CppScripts.cpp
2024-12-20 01:59:22 -06:00
jadebenn
6699081080 Merge branch 'main' into ub-fixes 2024-12-20 01:58:51 -06:00
David Markowitz
1ae21c423f skip non-files (#1690) 2024-12-19 12:19:41 -06:00
jadebenn
0ae9eb4a96 remove unneeded Component.cpp, forward declare dependencies, and make Component definition header-only (#1688) 2024-12-18 00:45:56 -08:00
David Markowitz
fced6d753a fix: Create resServer and logs if it doesnt exist and update readme (#1686)
* create resServer if not exist

* Update README.md

* Update README.md
2024-12-17 21:06:07 -06:00
David Markowitz
15dc5feeb5 feat: start car races if you "equip" the car near the car pad; add more old ns scripts to ignore list (#1681)
* brother

* use some better logic

* Implement spider boss msg script

tested that the message now shows up when hitting the survival spider entrance area

* add drag to start race feature
2024-12-17 21:04:35 -06:00
David Markowitz
a60865cd19 feat: allow SQLite database backend (#1663)
* simplify leaderboard code, fully abstract database

* update exception catching

* update exception catching and sql references, remove ugc from gamemessages

fix deleting model

remove unrelated changes

Update GameMessages.cpp

* remove ugc from gamemessages

* Update GameMessages.cpp

* Update Leaderboard.cpp

* bug fixes

* fix racing leaderboard

* remove extra stuff

* update

* add sqlite

* use a default for optimizations

* update sqlite

* Fix limits on update and delete

* fix bugs

* use definition to switch between databases

* add switch for different backends

* fix include guard and includes

* always build both

* add mysql if block

* Update Database.cpp

* add new options and add check to prevent overriding mysql

* correct config names

* Update README.md

* Update README.md

* merge to 1 sql file for sqlite database

* move to sqlite folder

* add back mysql migrations

* Update README.md

* add migration to correct the folder name or mysql

* yes aron

* updates

* Update CMakeLists.txt

* dont use paths at all, add where check to only update if folder name still exist

check also doesnt check for slashes and assumes one will be there since it will be.

* default dont auto create account

for releases we can change this flag

* default 0

* add times played query

* fix leaderboard not incrementing on a not better score

* add env vars with defaults for docker

* use an "enum"

* default to mariadb

* Update .env.example
2024-12-17 16:07:07 -08:00
jadebenn
77b42daca1 feat: Remove reinterpret_casts from AG race timer script and add method and chat command to get current server uptime (#1673)
* use steady_clock race timer

* formatting and const

* improve time interface

* added uptime chat command

* fix bug and update documentation

* inrease /uptime GM level requirement

* update GM level for /uptime (again)

* made changes according to feedback
2024-12-17 14:06:16 -06:00
David Markowitz
ba364800fe feat: allow for teleporting to player or relative position (#1683)
* allow for teleporting to player or relative position

* Update Commands.md

* Update Commands.md

* Update SlashCommandHandler.cpp
2024-12-17 13:39:28 -06:00
David Markowitz
e1c20192f7 fix: Implement missing survival tooltip script (#1679)
* brother

* use some better logic

* Implement spider boss msg script

tested that the message now shows up when hitting the survival spider entrance area
2024-12-16 13:35:36 -06:00
David Markowitz
0f8c5b436d fix: implement enemy clear threat script (#1678)
* brother

* use some better logic
2024-12-15 23:44:57 -06:00
53242ad5d5 Merge pull request #1680 from DarkflameUniverse/warn
fix: warnings
2024-12-15 22:45:52 -06:00
David Markowitz
a8919c8c14 Update dGame/dUtilities/Preconditions.cpp
Co-authored-by: jadebenn <jadebenn@users.noreply.github.com>
2024-12-14 21:14:07 -08:00
David Markowitz
5ff121612e use a cast
fix warning

remove pragma

we dont need this tbh
2024-12-14 17:55:41 -08:00
34618607c3 Merge pull request #1675 from DarkflameUniverse/invisible-items
the client code for this is a mess and should load everything at once or use non race condition code
2024-12-11 11:32:20 -06:00
Jett
02b76adb7a Replace keygen with CyberChef (#1677) 2024-12-11 16:58:37 +00:00
David Markowitz
3beb414b55 good enough
the client code for this is a mess and should load everything at once or use non race condition code
2024-12-10 19:10:54 -08:00
David Markowitz
1dadeeb36f fix leaderboard not incrementing on a not better score (#1674) 2024-12-10 05:37:49 -06:00
David Markowitz
aa7c3b9061 better vanity checks (#1666)
tested that vanity npcs now chat when close, and then on a cooldown
2024-12-08 16:27:04 -06:00
David Markowitz
1644d9448d Update .gitignore (#1672) 2024-12-08 16:23:55 -06:00
David Markowitz
8b56b0b7ba fix: use current binary directory mariadb shared object and dont override env variable (#1669)
* mac stuff

grab correct mariadb file and dont override env variable

fix for unix

Update CMakeLists.txt

unix only

* get that dylib
2024-12-08 00:36:49 -06:00
David Markowitz
4a1c289fb1 fix: avant gardens switches (#1667)
* yep

* remove dumb knockback

idk how the 1 switch in pet cove does it
2024-12-07 19:11:13 -06:00
David Markowitz
32a1e5ece5 update sqlite (#1665) 2024-12-06 15:04:23 -06:00
David Markowitz
7fcbb9507b feat: re-write leaderboards again and fully remove mysql dependency outside of database (#1662)
* simplify leaderboard code, fully abstract database

* update exception catching

* update exception catching and sql references, remove ugc from gamemessages

fix deleting model

remove unrelated changes

Update GameMessages.cpp

* remove ugc from gamemessages

* Update GameMessages.cpp

* Update Leaderboard.cpp

* bug fixes

* fix racing leaderboard

* remove extra stuff

* update
2024-12-06 05:03:47 -06:00
David Markowitz
730533c690 fix: abstract ugc rockets and cars from GameMessages (#1660)
* update exception catching and sql references, remove ugc from gamemessages

fix deleting model

remove unrelated changes

Update GameMessages.cpp

* remove ugc from gamemessages
2024-12-05 20:00:54 -08:00
David Markowitz
129d9fd0b9 update exception catching (#1661) 2024-12-04 03:30:14 -06:00
jadebenn
4922e2edb1 Merge remote-tracking branch 'upstream/main' into ub-fixes 2024-11-27 20:51:24 -06:00
jadebenn
c05c2543a9 Merge branch 'ub-fixes' of https://github.com/DarkflameUniverse/DarkflameServer into ub-fixes 2024-11-27 01:38:03 -06:00
jadebenn
5599bd946b reduce memcpying 2024-11-27 01:38:00 -06:00
jadebenn
a568e66e41 Stop annoying mac build failure 2024-11-27 00:34:37 -06:00
jadebenn
66fa3ff4ba created FromBitsUnchecked memcpy wrapper 2024-11-25 01:40:58 -06:00
jadebenn
6fa719c679 minor updates 2024-11-23 13:56:47 -08:00
jadebenn
7740bbbaab reinterpret_cast-based type-punning is almost always UB 2024-11-23 02:29:11 -08:00
jadebenn
8eb3488812 update AMF3 logic and disable sanitizers on darwin 2024-11-22 18:14:00 -08:00
jadebenn
ac4fd02a6c use decltype 2024-11-22 16:33:04 -08:00
jadebenn
b799f8967c Remove const char* templated test cases from Amf tests (no longer used) 2024-11-21 21:06:22 -06:00
jadebenn
12387ba07d Update Amf3.h for macos 2024-11-21 18:52:12 -06:00
jadebenn
051a0ba05e default-initialize message-id buffer to an invalid (but defined) state and trust the compiler to optimize it out 2024-11-21 18:48:02 -06:00
jadebenn
5aa8b1395b memcpy and template deduction guide 2024-11-21 18:37:50 -06:00
jadebenn
3a0e37ee8a update configure script 2024-11-21 02:08:21 -06:00
jadebenn
0a06f309f6 utils 2024-11-21 02:05:40 -06:00
jadebenn
30d4076808 fix: Remove instances of undefined behavior 2024-11-21 02:05:29 -06:00
184 changed files with 119703 additions and 42611 deletions

View File

@@ -3,12 +3,20 @@ CLIENT_PATH=./client
# Updates NET_VERSION in CMakeVariables.txt
NET_VERSION=171022
# make sure this is a long random string
# grab a "SHA 256-bit Key" from here: https://keygen.io/
# generate a "SHA 256-bit Key" from here: https://gchq.github.io/CyberChef/#recipe=Pseudo-Random_Number_Generator(256,'Hex')
ACCOUNT_MANAGER_SECRET=
# Should be the externally facing IP of your server host
EXTERNAL_IP=localhost
# The database type that will be used.
# Acceptable values are `sqlite`, `mysql`, `mariadb`, `maria`.
# Case insensitive.
DATABASE_TYPE=mariadb
SQLITE_DATABASE_PATH=resServer/dlu.sqlite
# Database values
# Be careful with special characters here. It is more safe to use normal characters and/or numbers.
MARIADB_USER=darkflame
MARIADB_PASSWORD=
MARIADB_DATABASE=darkflame
SKIP_ACCOUNT_CREATION=1

View File

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

3
.gitignore vendored
View File

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

View File

@@ -4,6 +4,18 @@ project(Darkflame
LANGUAGES C CXX
)
# Sanitizer flags - TODO: Make CMake preset before finalizing PR
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
add_compile_options("-fsanitize=undefined")
add_link_options("-fsanitize=undefined")
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_link_options("-static-libsan")
else()
add_link_options("-static-libasan")
endif()
endif()
# check if the path to the source directory contains a space
if("${CMAKE_SOURCE_DIR}" MATCHES " ")
message(FATAL_ERROR "The server cannot build in the path (" ${CMAKE_SOURCE_DIR} ") because it contains a space. Please move the server to a path without spaces.")
@@ -17,7 +29,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debugging
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions
set(CMAKE_VISIBILITY_INLINES_HIDDEN OFF) # Set C and C++ symbol visibility to hide inlined functions
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Read variables from file
@@ -66,6 +78,7 @@ set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
# Disabled no-register
# Disabled unknown pragmas because Linux doesn't understand Windows pragmas.
if(UNIX)
add_link_options("-Wl,-rpath,$ORIGIN/")
add_compile_options("-fPIC")
add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0 _GLIBCXX_USE_CXX17_ABI=0)
@@ -174,16 +187,18 @@ foreach(resource_file ${RESOURCE_FILES})
list(GET line_split 0 variable_name)
if(NOT ${parsed_current_file_contents} MATCHES ${variable_name})
message(STATUS "Adding missing config option " ${variable_name} " to " ${resource_file})
set(line_to_add ${line_to_add} ${line})
# For backwards compatibility with older setup versions, dont add this option.
if(NOT ${variable_name} MATCHES "database_type")
message(STATUS "Adding missing config option " ${variable_name} " to " ${resource_file})
set(line_to_add ${line_to_add} ${line})
foreach(line_to_append ${line_to_add})
file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n" ${line_to_append})
endforeach()
foreach(line_to_append ${line_to_add})
file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n" ${line_to_append})
endforeach()
file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n")
file(APPEND ${DLU_CONFIG_DIR}/${resource_file} "\n")
endif()
endif()
set(line_to_add "")
else()
set(line_to_add ${line_to_add} ${line})
@@ -213,21 +228,8 @@ foreach(file ${VANITY_FILES})
endforeach()
# Move our migrations for MasterServer to run
file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/migrations/dlu/)
file(GLOB SQL_FILES ${CMAKE_SOURCE_DIR}/migrations/dlu/*.sql)
foreach(file ${SQL_FILES})
get_filename_component(file ${file} NAME)
configure_file(${CMAKE_SOURCE_DIR}/migrations/dlu/${file} ${PROJECT_BINARY_DIR}/migrations/dlu/${file})
endforeach()
file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/migrations/cdserver/)
file(GLOB SQL_FILES ${CMAKE_SOURCE_DIR}/migrations/cdserver/*.sql)
foreach(file ${SQL_FILES})
get_filename_component(file ${file} NAME)
configure_file(${CMAKE_SOURCE_DIR}/migrations/cdserver/${file} ${PROJECT_BINARY_DIR}/migrations/cdserver/${file})
endforeach()
file(REMOVE_RECURSE ${PROJECT_BINARY_DIR}/migrations)
file(COPY ${CMAKE_SOURCE_DIR}/migrations DESTINATION ${CMAKE_BINARY_DIR})
# Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux)
if (APPLE)
@@ -272,7 +274,7 @@ if(MSVC)
# add_compile_options("/W4")
# Want to enable warnings eventually, but WAY too much noise right now
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
add_compile_options("-Wuninitialized" "-Wold-style-cast")
add_compile_options("-Wuninitialized" "-Wold-style-cast" "-Wstrict-aliasing=01")
else()
message(WARNING "Unknown compiler: '${CMAKE_CXX_COMPILER_ID}' - No warning flags enabled.")
endif()
@@ -312,7 +314,7 @@ add_subdirectory(dPhysics)
add_subdirectory(dServer)
# Create a list of common libraries shared between all binaries
set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "MariaDB::ConnCpp" "magic_enum")
set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
# Add platform specific common libraries
if(UNIX)

View File

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

View File

@@ -1,5 +1,5 @@
PROJECT_VERSION_MAJOR=2
PROJECT_VERSION_MINOR=3
PROJECT_VERSION_MAJOR=3
PROJECT_VERSION_MINOR=0
PROJECT_VERSION_PATCH=0
# Debugging

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
* Hosting a server for others counts as distribution
## Disclaimers
### Setup difficulty
Throughout the entire build and setup process a level of familiarity with the command line and preferably a Unix-like development environment is greatly advantageous.
### Hosting a server
We do not recommend hosting public servers. Darkflame Universe is intended for small scale deployment, for example within a group of friends. It has not been tested for large scale deployment which comes with additional security risks.
### Supply of resource files
Darkflame Universe is a server emulator and does not distribute any LEGO® Universe files. A separate game client is required to setup this server emulator and play the game, which we cannot supply. Users are strongly suggested to refer to the safe checksums listed [here](#verifying-your-client-files) to see if a client will work.
## Step by step walkthrough for a single-player server
If you would like a setup for a single player server only on a Windows machine, use the [Native Windows Setup Guide by HailStorm](https://gist.github.com/HailStorm32/169df65a47a104199b5cc57d10fa57de) and skip this README.
## Setting up a single player server
* If you don't know what WSL is, skip this warning.
Warning: WSL version 1 does NOT support using sqlite as a database due to how it handles filesystem synchronization.
You must use Version 2 if you must run the server under WSL. Not doing so will result in save data loss.
* Single player installs now no longer require building the server from source or installing development tools.
* Download the [latest windows release](https://github.com/DarkflameUniverse/DarkflameServer/releases) (or whichever release you need) and extract the files into a folder inside your client. Note that this setup is expecting that when double clicking the folder that you put in the same folder as `legouniverse.exe`, the file `MasterServer.exe` is in there.
* You should be able to see the folder with the server files in the same folder as `legouniverse.exe`.
* Go into the server files folder and open `sharedconfig.ini`. Find the line that says `client_location` and put `..` after it so the line reads `client_location=..`.
* To run the server, double-click `MasterServer.exe`.
* You will be asked to create an account the first time you run the server. After you have created the account, the server will shutdown and need to be restarted.
* To connect to the server, either delete the file `boot.cfg` which is found in your LEGO Universe client, rename the file `boot.cfg` to something else or follow the steps [here](#allowing-a-user-to-connect-to-your-server) if you wish to keep the file.
* When shutting down the server, it is highly recommended to click the `MasterServer.exe` window and hold `ctrl` while pressing `c` to stop the server.
* We are working on a way to make it so when you close the game, the server stops automatically alongside when you open the game, the server starts automatically.
## Steps to setup server
<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)
* [Setting up a development environment](#setting-up-a-development-environment)
* [Install dependencies](#install-dependencies)
* [Database setup](#database-setup)
* [Build the server](#build-the-server)
@@ -39,6 +51,13 @@ If you would like a setup for a single player server only on a Windows machine,
* [User Guide](#user-guide)
* [Docker](#docker)
## Disclaimers
### Setup difficulty
Throughout the entire build and setup process a level of familiarity with the command line and preferably a Unix-like development environment is greatly advantageous.
## Step by step walkthrough for building a single-player Windows server from source
If you would like a setup for a single player server only on a Windows machine built from source, use the [Native Windows Setup Guide by HailStorm](https://gist.github.com/HailStorm32/169df65a47a104199b5cc57d10fa57de) and skip this README.
## Clone the repository
If you are on Windows, you will need to download and install git from [here](https://git-scm.com/download/win)
@@ -266,8 +285,8 @@ systemctl stop darkflame.service
journalctl -xeu darkflame.service
```
### First admin user
Run `MasterServer -a` to get prompted to create an admin account. This method is only intended for the system administrator as a means to get started, do NOT use this method to create accounts for other users!
### First user or adding more users.
The first time you run `MasterServer`, you will be prompted to create an account. To create more accounts from the command line, `MasterServer -a` to get prompted to create an admin account. This method is only intended for the system administrator as a means to get started, do NOT use this method to create accounts for other users!
### Account management tool (Nexus Dashboard)
**If you are just using this server for yourself, you can skip setting up Nexus Dashboard**
@@ -371,7 +390,7 @@ at once. For that:
- Download the [.env.example](.env.example) file and place it next to `client` with the file name `.env`
- You may get warnings that this name starts with a dot, acknowledge those, this is intentional. Depending on your operating system, you may need to activate showing hidden files (e.g. Ctrl-H in Gnome on Linux) and/or file extensions ("File name extensions" in the "View" tab on Windows).
- Update the `ACCOUNT_MANAGER_SECRET` and `MARIADB_PASSWORD` with strong random passwords.
- Use a password generator like <https://keygen.io>
- Use a password generator <https://gchq.github.io/CyberChef/#recipe=Pseudo-Random_Number_Generator(256,'Hex')>
- Avoid `:` and `@` characters
- Once the database user is created, changing the password will not update it, so the server will just fail to connect.
- Set `EXTERNAL_IP` to your LAN IP or public IP if you want to host the game for friends & family

View File

@@ -1,6 +1,7 @@
# Try and find a clang-16 install, falling back to a generic clang install otherwise
find_program(CLANG_C_COMPILER clang-16 | clang REQUIRED)
find_program(CLANG_CXX_COMPILER clang++-16 | clang++ REQUIRED)
find_program(CLANG_CXX_LINKER lld REQUIRED)
# Debug messages
message(DEBUG "CLANG_C_COMPILER = ${CLANG_C_COMPILER}")

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
#ifndef __AMF3__H__
#define __AMF3__H__
#ifndef AMF3_H
#define AMF3_H
#include "dCommonVars.h"
#include "Logger.h"
#include "Game.h"
#include "GeneralUtils.h"
#include <unordered_map>
#include <vector>
@@ -74,28 +75,15 @@ template <> [[nodiscard]] constexpr eAmf AMFValue<double>::GetValueType() const
template <typename ValueType>
[[nodiscard]] constexpr eAmf AMFValue<ValueType>::GetValueType() const noexcept { return eAmf::Undefined; }
// As a string this is much easier to write and read from a BitStream.
template <>
class AMFValue<const char*> : public AMFBaseValue {
public:
AMFValue() = default;
AMFValue(const char* value) { m_Data = value; }
virtual ~AMFValue() override = default;
[[nodiscard]] constexpr eAmf GetValueType() const noexcept override { return eAmf::String; }
[[nodiscard]] const std::string& GetValue() const { return m_Data; }
void SetValue(const std::string& value) { m_Data = value; }
protected:
std::string m_Data;
};
using AMFNullValue = AMFValue<std::nullptr_t>;
using AMFBoolValue = AMFValue<bool>;
using AMFIntValue = AMFValue<int32_t>;
using AMFStringValue = AMFValue<std::string>;
using AMFDoubleValue = AMFValue<double>;
// Template deduction guide to ensure string literals deduce
AMFValue(const char*) -> AMFValue<std::string>; // AMFStringValue
/**
* The AMFArrayValue object holds 2 types of lists:
* An associative list where a key maps to a value
@@ -106,7 +94,7 @@ using AMFDoubleValue = AMFValue<double>;
*/
class AMFArrayValue : public AMFBaseValue {
using AMFAssociative =
std::unordered_map<std::string, std::unique_ptr<AMFBaseValue>, GeneralUtils::transparent_string_hash, std::equal_to<>>;
std::unordered_map<std::string, std::unique_ptr<AMFBaseValue>, GeneralUtils::transparent_string_hash, std::equal_to<void>>;
using AMFDense = std::vector<std::unique_ptr<AMFBaseValue>>;
@@ -137,17 +125,20 @@ public:
* @return The inserted element if the type matched,
* or nullptr if a key existed and was not the same type
*/
template <typename ValueType>
[[maybe_unused]] std::pair<AMFValue<ValueType>*, bool> Insert(const std::string_view key, const ValueType value) {
template <typename T>
[[maybe_unused]] auto Insert(const std::string_view key, const T value) -> std::pair<decltype(AMFValue(value))*, bool> {
// This ensures the deduced type matches the AMFValue constructor
using AMFValueType = decltype(AMFValue(value));
const auto element = m_Associative.find(key);
AMFValue<ValueType>* val = nullptr;
AMFValueType* val = nullptr;
bool found = true;
if (element == m_Associative.cend()) {
auto newVal = std::make_unique<AMFValue<ValueType>>(value);
auto newVal = std::make_unique<AMFValueType>(value);
val = newVal.get();
m_Associative.emplace(key, std::move(newVal));
} else {
val = dynamic_cast<AMFValue<ValueType>*>(element->second.get());
val = dynamic_cast<AMFValueType*>(element->second.get());
found = false;
}
return std::make_pair(val, found);
@@ -190,15 +181,18 @@ public:
* @return The inserted element, or nullptr if the type did not match
* what was at the index.
*/
template <typename ValueType>
[[maybe_unused]] std::pair<AMFValue<ValueType>*, bool> Insert(const size_t index, const ValueType value) {
template <typename T>
[[maybe_unused]] auto Insert(const size_t index, const T value) -> std::pair<decltype(AMFValue(value))*, bool> {
// This ensures the deduced type matches the AMFValue constructor
using AMFValueType = decltype(AMFValue(value));
bool inserted = false;
if (index >= m_Dense.size()) {
m_Dense.resize(index + 1);
m_Dense.at(index) = std::make_unique<AMFValue<ValueType>>(value);
m_Dense.at(index) = std::make_unique<AMFValueType>(value);
inserted = true;
}
return std::make_pair(dynamic_cast<AMFValue<ValueType>*>(m_Dense.at(index).get()), inserted);
return std::make_pair(dynamic_cast<AMFValueType*>(m_Dense.at(index).get()), inserted);
}
/**
@@ -245,8 +239,8 @@ public:
*
* @return The inserted pointer, or nullptr should the key already be in use.
*/
template <typename ValueType>
[[maybe_unused]] inline AMFValue<ValueType>* Push(const ValueType value) {
template <typename T>
[[maybe_unused]] inline auto Push(const T value) -> decltype(AMFValue(value))* {
return Insert(m_Dense.size(), value).first;
}
@@ -356,4 +350,4 @@ private:
AMFDense m_Dense;
};
#endif //!__AMF3__H__
#endif //!AMF3_H

View File

@@ -10,40 +10,54 @@ void RakNet::BitStream::Write<AMFBaseValue&>(AMFBaseValue& value) {
this->Write(type);
switch (type) {
case eAmf::Integer: {
this->Write<AMFIntValue&>(*static_cast<AMFIntValue*>(&value));
this->Write<AMFIntValue&>(static_cast<AMFIntValue&>(value));
break;
}
case eAmf::Double: {
this->Write<AMFDoubleValue&>(*static_cast<AMFDoubleValue*>(&value));
this->Write<AMFDoubleValue&>(static_cast<AMFDoubleValue&>(value));
break;
}
case eAmf::String: {
this->Write<AMFStringValue&>(*static_cast<AMFStringValue*>(&value));
this->Write<AMFStringValue&>(static_cast<AMFStringValue&>(value));
break;
}
case eAmf::Array: {
this->Write<AMFArrayValue&>(*static_cast<AMFArrayValue*>(&value));
this->Write<AMFArrayValue&>(static_cast<AMFArrayValue&>(value));
break;
}
default: {
LOG("Encountered unwritable AMFType %i!", type);
[[fallthrough]];
}
case eAmf::Undefined:
[[fallthrough]];
case eAmf::Null:
[[fallthrough]];
case eAmf::False:
[[fallthrough]];
case eAmf::True:
[[fallthrough]];
case eAmf::Date:
[[fallthrough]];
case eAmf::Object:
[[fallthrough]];
case eAmf::XML:
[[fallthrough]];
case eAmf::XMLDoc:
[[fallthrough]];
case eAmf::ByteArray:
[[fallthrough]];
case eAmf::VectorInt:
[[fallthrough]];
case eAmf::VectorUInt:
[[fallthrough]];
case eAmf::VectorDouble:
[[fallthrough]];
case eAmf::VectorObject:
[[fallthrough]];
case eAmf::Dictionary:
break;
}
@@ -145,8 +159,7 @@ void RakNet::BitStream::Write<AMFIntValue&>(AMFIntValue& value) {
// Writes an AMFDoubleValue to BitStream
template<>
void RakNet::BitStream::Write<AMFDoubleValue&>(AMFDoubleValue& value) {
double d = value.GetValue();
WriteAMFU64(*this, *reinterpret_cast<uint64_t*>(&d));
WriteAMFU64(*this, std::bit_cast<uint64_t>(value.GetValue()));
}
// Writes an AMFStringValue to BitStream

View File

@@ -2,16 +2,25 @@
#include <string>
//For reading null-terminated strings
std::string BinaryIO::ReadString(std::istream& instream) {
std::string toReturn;
char buffer;
template<typename StringType>
StringType ReadString(std::istream& instream) {
StringType toReturn{};
typename StringType::value_type buffer{};
BinaryIO::BinaryRead(instream, buffer);
while (buffer != 0x00) {
toReturn += buffer;
BinaryRead(instream, buffer);
BinaryIO::BinaryRead(instream, buffer);
}
return toReturn;
}
std::string BinaryIO::ReadString(std::istream& instream) {
return ::ReadString<std::string>(instream);
}
std::u8string BinaryIO::ReadU8String(std::istream& instream) {
return ::ReadString<std::u8string>(instream);
}

View File

@@ -65,6 +65,8 @@ namespace BinaryIO {
std::string ReadString(std::istream& instream);
std::u8string ReadU8String(std::istream& instream);
inline bool DoesFileExist(const std::string& name) {
std::ifstream f(name.c_str());
return f.good();

View File

@@ -123,7 +123,7 @@ uint32_t BrickByBrickFix::UpdateBrickByBrickModelsToSd0() {
Database::Get()->UpdateUgcModelData(model.id, outputStringStream);
LOG("Updated model %i to sd0", model.id);
updatedModels++;
} catch (sql::SQLException exception) {
} catch (std::exception& exception) {
LOG("Failed to update model %i. This model should be inspected manually to see why."
"The database error is %s", model.id, exception.what());
}
@@ -146,7 +146,7 @@ void WriteSd0Magic(char* input, uint32_t chunkSize) {
input[2] = '0';
input[3] = 0x01;
input[4] = 0xFF;
*reinterpret_cast<uint32_t*>(input + 5) = chunkSize; // Write the integer to the character array
std::memcpy(&input[5], &chunkSize, sizeof(uint32_t)); // Write the integer to the character array
}
bool CheckSd0Magic(std::istream& streamToCheck) {

View File

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

View File

@@ -65,13 +65,14 @@ int64_t FdbToSqlite::Convert::ReadInt64(std::istream& cdClientBuffer) {
return value;
}
// cdclient is encoded in latin1
std::string FdbToSqlite::Convert::ReadString(std::istream& cdClientBuffer) {
int32_t prevPosition = SeekPointer(cdClientBuffer);
auto readString = BinaryIO::ReadString(cdClientBuffer);
const auto readString = BinaryIO::ReadU8String(cdClientBuffer);
cdClientBuffer.seekg(prevPosition);
return readString;
return GeneralUtils::Latin1ToUTF8(readString);
}
int32_t FdbToSqlite::Convert::SeekPointer(std::istream& cdClientBuffer) {

View File

@@ -53,9 +53,9 @@ bool static _IsSuffixChar(const uint8_t c) {
bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out) {
const size_t rem = slice.length();
if (slice.empty()) return false;
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&slice.front());
const char* const bytes = slice.data();
if (rem > 0) {
const uint8_t first = bytes[0];
const uint8_t first = static_cast<uint8_t>(bytes[0]);
if (first < 0x80) { // 1 byte character
out = static_cast<uint32_t>(first & 0x7F);
slice.remove_prefix(1);
@@ -64,7 +64,7 @@ bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out
// middle byte, not valid at start, fall through
} else if (first < 0xE0) { // two byte character
if (rem > 1) {
const uint8_t second = bytes[1];
const uint8_t second = static_cast<uint8_t>(bytes[1]);
if (_IsSuffixChar(second)) {
out = (static_cast<uint32_t>(first & 0x1F) << 6)
+ static_cast<uint32_t>(second & 0x3F);
@@ -74,8 +74,8 @@ bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out
}
} else if (first < 0xF0) { // three byte character
if (rem > 2) {
const uint8_t second = bytes[1];
const uint8_t third = bytes[2];
const uint8_t second = static_cast<uint8_t>(bytes[1]);
const uint8_t third = static_cast<uint8_t>(bytes[2]);
if (_IsSuffixChar(second) && _IsSuffixChar(third)) {
out = (static_cast<uint32_t>(first & 0x0F) << 12)
+ (static_cast<uint32_t>(second & 0x3F) << 6)
@@ -86,9 +86,9 @@ bool GeneralUtils::details::_NextUTF8Char(std::string_view& slice, uint32_t& out
}
} else if (first < 0xF8) { // four byte character
if (rem > 3) {
const uint8_t second = bytes[1];
const uint8_t third = bytes[2];
const uint8_t fourth = bytes[3];
const uint8_t second = static_cast<uint8_t>(bytes[1]);
const uint8_t third = static_cast<uint8_t>(bytes[2]);
const uint8_t fourth = static_cast<uint8_t>(bytes[3]);
if (_IsSuffixChar(second) && _IsSuffixChar(third) && _IsSuffixChar(fourth)) {
out = (static_cast<uint32_t>(first & 0x07) << 18)
+ (static_cast<uint32_t>(second & 0x3F) << 12)
@@ -167,6 +167,15 @@ std::u16string GeneralUtils::ASCIIToUTF16(const std::string_view string, const s
return ret;
}
std::string GeneralUtils::Latin1ToUTF8(const std::u8string_view string, const size_t size) {
std::string toReturn{};
for (const auto u : string) {
PushUTF8CodePoint(toReturn, u);
}
return toReturn;
}
//! Converts a (potentially-ill-formed) UTF-16 string to UTF-8
//! See: <http://simonsapin.github.io/wtf-8/#decoding-ill-formed-utf-16>
std::string GeneralUtils::UTF16ToWTF8(const std::u16string_view string, const size_t size) {
@@ -175,9 +184,9 @@ std::string GeneralUtils::UTF16ToWTF8(const std::u16string_view string, const si
ret.reserve(newSize);
for (size_t i = 0; i < newSize; ++i) {
const char16_t u = string[i];
const auto u = string[i];
if (IsLeadSurrogate(u) && (i + 1) < newSize) {
const char16_t next = string[i + 1];
const auto next = string[i + 1];
if (IsTrailSurrogate(next)) {
i += 1;
const char32_t cp = 0x10000
@@ -291,11 +300,12 @@ std::u16string GeneralUtils::ReadWString(RakNet::BitStream& inStream) {
std::vector<std::string> GeneralUtils::GetSqlFileNamesFromFolder(const std::string_view folder) {
// Because we dont know how large the initial number before the first _ is we need to make it a map like so.
std::map<uint32_t, std::string> filenames{};
std::map<uint32_t, std::string> filenames{};
for (const auto& t : std::filesystem::directory_iterator(folder)) {
auto filename = t.path().filename().string();
const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0));
filenames.emplace(index, std::move(filename));
if (t.is_directory() || t.is_symlink()) continue;
auto filename = t.path().filename().string();
const auto index = std::stoi(GeneralUtils::SplitString(filename, '_').at(0));
filenames.emplace(index, std::move(filename));
}
// Now sort the map by the oldest migration.

View File

@@ -3,6 +3,7 @@
// C++
#include <charconv>
#include <cstdint>
#include <cstring>
#include <ctime>
#include <functional>
#include <optional>
@@ -51,6 +52,14 @@ namespace GeneralUtils {
bool _NextUTF8Char(std::string_view& slice, uint32_t& out);
}
//! Converts a Latin1 string to a UTF-8 string
/*!
\param string The string to convert
\param size A size to trim the string to. Default is SIZE_MAX (No trimming)
\return An UTF-8 representation of the string
*/
std::string Latin1ToUTF8(const std::u8string_view string, const size_t size = SIZE_MAX);
//! Converts a UTF-16 string to a UTF-8 string
/*!
\param string The string to convert
@@ -137,7 +146,7 @@ namespace GeneralUtils {
template <typename... Bases>
struct overload : Bases... {
using is_transparent = void;
using Bases::operator() ... ;
using Bases::operator()...;
};
struct char_pointer_hash {
@@ -152,6 +161,26 @@ namespace GeneralUtils {
char_pointer_hash
>;
/**
* A convenience wrapper around std::memcpy that creates a new object
* from the value representation of the provided bytes.Use when
* std::bit_cast is not applicable.
* @warning All restrictions of std::memcpy still apply. Accessing
* outside of the source object is undefined behavior.
* @param from The source of the value representation
* @returns A new object with the given value representation
*/
template <typename To, typename From>
[[nodiscard]]
inline To FromBitsUnchecked(const From* from) noexcept
requires (std::is_trivially_copyable_v<To>
&& std::is_trivially_copyable_v<From>)
{
To to{};
std::memcpy(&to, from, sizeof(To));
return to;
}
// Concept constraining to enum types
template <typename T>
concept Enum = std::is_enum_v<T>;

View File

@@ -54,6 +54,7 @@ struct AssetStream : std::istream {
}
operator bool() {
// NEED TO FIX THIS
return reinterpret_cast<AssetMemoryBuffer*>(rdbuf())->m_Success;
}
};

View File

@@ -957,6 +957,7 @@ namespace MessageType {
MODIFY_PLAYER_ZONE_STATISTIC = 1046,
APPLY_EXTERNAL_FORCE = 1049,
GET_APPLIED_EXTERNAL_FORCE = 1050,
ACTIVITY_NOTIFY = 1051,
ITEM_EQUIPPED = 1052,
ACTIVITY_STATE_CHANGE_REQUEST = 1053,
OVERRIDE_FRICTION = 1054,
@@ -1253,6 +1254,7 @@ namespace MessageType {
VEHICLE_NOTIFY_HIT_EXPLODER = 1385,
CHECK_NEAREST_ROCKET_LAUNCH_PRE_CONDITIONS = 1386,
REQUEST_NEAREST_ROCKET_LAUNCH_PRE_CONDITIONS = 1387,
CONFIGURE_RACING_CONTROL = 1388,
CONFIGURE_RACING_CONTROL_CLIENT = 1389,
NOTIFY_RACING_CLIENT = 1390,
RACING_PLAYER_HACK_CAR = 1391,

View File

@@ -5,7 +5,8 @@
namespace MessageType {
enum class World : uint32_t {
VALIDATION = 1, // Session info
INVALID = 0,
VALIDATION, // Session info
CHARACTER_LIST_REQUEST,
CHARACTER_CREATE_REQUEST,
LOGIN_REQUEST, // Character selected

View File

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

View File

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

View File

@@ -2,22 +2,46 @@
#include "Game.h"
#include "dConfig.h"
#include "Logger.h"
#include "MySQLDatabase.h"
#include "DluAssert.h"
#include "SQLiteDatabase.h"
#include "MySQLDatabase.h"
#include <ranges>
#pragma warning (disable:4251) //Disables SQL warnings
namespace {
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() {
if (database) {
LOG("Tried to connect to database when it's already connected!");
return;
}
database = new MySQLDatabase();
const auto databaseType = GetMigrationFolder();
if (databaseType == "sqlite") database = new SQLiteDatabase();
else if (databaseType == "mysql") database = new MySQLDatabase();
else {
LOG("Invalid database type specified in config, using MySQL");
database = new MySQLDatabase();
}
database->Connect();
}

View File

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

View File

@@ -24,14 +24,10 @@
#include "IIgnoreList.h"
#include "IAccountsRewardCodes.h"
#include "IBehaviors.h"
namespace sql {
class Statement;
class PreparedStatement;
};
#include "IUgcModularBuild.h"
#ifdef _DEBUG
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (sql::SQLException& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
#else
# define DLU_SQL_TRY_CATCH_RETHROW(x) x
#endif // _DEBUG
@@ -42,14 +38,13 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors {
public IBehaviors, public IUgcModularBuild {
public:
virtual ~GameDatabase() = default;
// TODO: These should be made private.
virtual void Connect() = 0;
virtual void Destroy(std::string source = "") = 0;
virtual void ExecuteCustomQuery(const std::string_view query) = 0;
virtual sql::PreparedStatement* CreatePreppedStmt(const std::string& query) = 0;
virtual void Commit() = 0;
virtual bool GetAutoCommit() = 0;
virtual void SetAutoCommit(bool value) = 0;

View File

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

View File

@@ -3,12 +3,45 @@
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
class ILeaderboard {
public:
struct Entry {
uint32_t charId{};
uint32_t lastPlayedTimestamp{};
float primaryScore{};
float secondaryScore{};
float tertiaryScore{};
uint32_t numWins{};
uint32_t numTimesPlayed{};
uint32_t ranking{};
std::string name{};
};
struct Score {
auto operator<=>(const Score& rhs) const = default;
float primaryScore{ 0.0f };
float secondaryScore{ 0.0f };
float tertiaryScore{ 0.0f };
};
// Get the donation total for the given activity id.
virtual std::optional<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__

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

View File

@@ -7,6 +7,7 @@
#include "GameDatabase.h"
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
// bind a parameter to a type that isn't defined.
@@ -29,7 +30,6 @@ public:
void Connect() override;
void Destroy(std::string source = "") override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query) override;
void Commit() override;
bool GetAutoCommit() override;
void SetAutoCommit(bool value) override;
@@ -113,6 +113,19 @@ public:
void RemoveBehavior(const int32_t characterId) override;
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
std::optional<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:
// 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) {
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"
"Servers.cpp"
"Ugc.cpp"
"UgcModularBuild.cpp"
PARENT_SCOPE
)

View File

@@ -1,5 +1,9 @@
#include "MySQLDatabase.h"
#include "Game.h"
#include "Logger.h"
#include "dConfig.h"
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);
@@ -9,3 +13,79 @@ std::optional<uint32_t> MySQLDatabase::GetDonationTotal(const uint32_t activityI
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,81 @@
#include "SQLiteDatabase.h"
#include "Database.h"
#include "Game.h"
#include "dConfig.h"
#include "Logger.h"
#include "dPlatforms.h"
#include "BinaryPathFinder.h"
// Static Variables
// Status Variables
namespace {
CppSQLite3DB* con = nullptr;
bool isConnected = false;
};
void SQLiteDatabase::Connect() {
LOG("Using SQLite database");
con = new CppSQLite3DB();
const auto path = BinaryPathFinder::GetBinaryDir() / Game::config->GetValue("sqlite_database_path");
if (!std::filesystem::exists(path)) {
LOG("Creating sqlite path %s", path.string().c_str());
std::filesystem::create_directories(path.parent_path());
}
con->open(path.string().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() {
}

View File

@@ -7,7 +7,6 @@ class TestSQLDatabase : public GameDatabase {
void Connect() override;
void Destroy(std::string source = "") override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query) override;
void Commit() override;
bool GetAutoCommit() override;
void SetAutoCommit(bool value) override;
@@ -91,6 +90,18 @@ class TestSQLDatabase : public GameDatabase {
void RemoveBehavior(const int32_t behaviorId) override;
void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) override;
std::optional<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

View File

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

View File

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

View File

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

@@ -13,6 +13,11 @@
#include "eKillType.h"
#include "Observable.h"
namespace GameMessages {
struct ActivityNotify;
struct ShootingGalleryFire;
};
namespace Loot {
class Info;
};
@@ -107,6 +112,11 @@ public:
const SystemAddress& GetSystemAddress() const;
// Returns the collision group for this entity.
// Because the collision group is stored on a base component, this will look for a physics component
// then return the collision group from that.
int32_t GetCollisionGroup() const;
/**
* Setters
*/
@@ -205,6 +215,8 @@ public:
void OnZonePropertyModelRemoved(Entity* player);
void OnZonePropertyModelRemovedWhileEquipped(Entity* player);
void OnZonePropertyModelRotated(Entity* player);
void OnActivityNotify(GameMessages::ActivityNotify& notify);
void OnShootingGalleryFire(GameMessages::ShootingGalleryFire& notify);
void OnMessageBoxResponse(Entity* sender, int32_t button, const std::u16string& identifier, const std::u16string& userData);
void OnChoiceBoxResponse(Entity* sender, int32_t button, const std::u16string& buttonIdentifier, const std::u16string& identifier);

View File

@@ -505,6 +505,7 @@ void EntityManager::UpdateGhosting(Entity* player) {
if (collectionId != 0) {
collectionId = static_cast<uint32_t>(collectionId) + static_cast<uint32_t>(Game::server->GetZoneID() << 8);
if (missionComponent->HasCollectible(collectionId)) {
continue;
}

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
#include "DestroyableComponent.h"
#include <algorithm>
#include <ranges>
#include <sstream>
#include <vector>
@@ -27,7 +28,7 @@
#include "CDPhysicsComponentTable.h"
#include "dNavMesh.h"
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id): Component(parent) {
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id) : Component(parent) {
m_Target = LWOOBJID_EMPTY;
m_DirtyStateOrTarget = true;
m_State = AiState::spawn;
@@ -37,6 +38,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id):
m_Disabled = false;
m_SkillEntries = {};
m_SoftTimer = 5.0f;
m_ForcedTetherTime = 0.0f;
//Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
@@ -170,6 +172,17 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
GameMessages::SendStopFXEffect(m_Parent, true, "tether");
m_TetherEffectActive = false;
}
m_ForcedTetherTime -= deltaTime;
if (m_ForcedTetherTime >= 0) return;
}
for (auto entry = m_RemovedThreatList.begin(); entry != m_RemovedThreatList.end();) {
entry->second -= deltaTime;
if (entry->second <= 0.0f) {
entry = m_RemovedThreatList.erase(entry);
} else {
++entry;
}
}
if (m_SoftTimer <= 0.0f) {
@@ -287,40 +300,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
}
if (!m_TetherEffectActive && m_OutOfCombat && (m_OutOfCombatTime -= deltaTime) <= 0) {
auto* destroyableComponent = m_Parent->GetComponent<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);
}
TetherLogic();
m_OutOfCombat = false;
m_OutOfCombatTime = 0.0f;
@@ -499,7 +479,7 @@ std::vector<LWOOBJID> BaseCombatAIComponent::GetTargetWithinAggroRange() const {
const auto distance = Vector3::DistanceSquared(m_Parent->GetPosition(), other->GetPosition());
if (distance > m_AggroRadius * m_AggroRadius) continue;
if (distance > m_AggroRadius * m_AggroRadius || m_RemovedThreatList.contains(id)) continue;
targets.push_back(id);
}
@@ -626,6 +606,7 @@ const NiPoint3& BaseCombatAIComponent::GetStartPosition() const {
void BaseCombatAIComponent::ClearThreat() {
m_ThreatEntries.clear();
m_Target = LWOOBJID_EMPTY;
m_DirtyThreat = true;
}
@@ -806,3 +787,55 @@ void BaseCombatAIComponent::Wake() {
m_dpEntity->SetSleeping(false);
m_dpEntityEnemy->SetSleeping(false);
}
void BaseCombatAIComponent::TetherLogic() {
auto* destroyableComponent = m_Parent->GetComponent<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();
// Force this entity to tether and ignore all other actions
void ForceTether();
// heals the entity to full health and armor
// and tethers them to their spawn point
void TetherLogic();
// Ignore a threat for a certain amount of time
void IgnoreThreat(const LWOOBJID target, const float time);
private:
/**
* Returns the current target or the target that currently is the largest threat to this entity
@@ -382,6 +392,12 @@ private:
*/
bool m_DirtyStateOrTarget = false;
// The amount of time the entity will be forced to tether for
float m_ForcedTetherTime = 0.0f;
// The amount of time a removed threat will be ignored for.
std::map<LWOOBJID, float> m_RemovedThreatList;
/**
* Whether the current entity is a mech enemy, needed as mechs tether radius works differently
* @return whether this entity is a mech

View File

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

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
#include "tinyxml2.h"
namespace tinyxml2 {
class XMLDocument;
}
namespace RakNet {
class BitStream;
}
class Entity;
@@ -9,40 +15,40 @@ class Entity;
*/
class Component {
public:
Component(Entity* parent);
virtual ~Component();
Component(Entity* parent) : m_Parent{ parent } {}
virtual ~Component() = default;
/**
* Gets 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
* @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
* @param originator
*/
virtual void OnUse(Entity* originator);
virtual void OnUse(Entity* originator) {}
/**
* Save data from this componennt to character XML
* @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
* @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:

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#include "HavokVehiclePhysicsComponent.h"
#include "EntityManager.h"
HavokVehiclePhysicsComponent::HavokVehiclePhysicsComponent(Entity* parent) : PhysicsComponent(parent) {
HavokVehiclePhysicsComponent::HavokVehiclePhysicsComponent(Entity* parent, int32_t componentId) : PhysicsComponent(parent, componentId) {
m_Velocity = NiPoint3Constant::ZERO;
m_AngularVelocity = NiPoint3Constant::ZERO;
m_IsOnGround = true;

View File

@@ -13,7 +13,7 @@ class HavokVehiclePhysicsComponent : public PhysicsComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::HAVOK_VEHICLE_PHYSICS;
HavokVehiclePhysicsComponent(Entity* parentEntity);
HavokVehiclePhysicsComponent(Entity* parentEntity, int32_t componentId);
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;

View File

@@ -31,6 +31,7 @@
#include "eStateChangeType.h"
#include "eUseItemResponse.h"
#include "Mail.h"
#include "ProximityMonitorComponent.h"
#include "CDComponentsRegistryTable.h"
#include "CDInventoryComponentTable.h"
@@ -68,9 +69,10 @@ InventoryComponent::InventoryComponent(Entity* parent) : Component(parent) {
auto slot = 0u;
for (const auto& item : items) {
if (!item.equip || !Inventory::IsValidItem(item.itemid)) {
continue;
}
if (!Inventory::IsValidItem(item.itemid)) continue;
AddItem(item.itemid, item.count);
if (!item.equip) continue;
const LWOOBJID id = ObjectIDManager::GenerateObjectID();
@@ -829,6 +831,30 @@ void InventoryComponent::EquipItem(Item* item, const bool skipChecks) {
break;
}
return;
} else if (item->GetLot() == 8092) {
// Trying to equip a car
const auto proximityObjects = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::PROXIMITY_MONITOR);
// look for car instancers and check if we are in its setup range
for (auto* const entity : proximityObjects) {
if (!entity) continue;
auto* proximityMonitorComponent = entity->GetComponent<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;
}
@@ -1141,6 +1167,25 @@ void InventoryComponent::AddItemSkills(const LOT lot) {
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) {
const auto info = Inventory::FindItemComponent(lot);

View File

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

View File

@@ -265,6 +265,7 @@ void MovementAIComponent::PullToPoint(const NiPoint3& point) {
void MovementAIComponent::SetPath(std::vector<PathWaypoint> path) {
if (path.empty()) return;
while (!m_CurrentPath.empty()) m_CurrentPath.pop();
std::for_each(path.rbegin(), path.rend() - 1, [this](const PathWaypoint& point) {
this->m_CurrentPath.push(point);
});

View File

@@ -524,7 +524,7 @@ void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) {
GameMessages::SendRegisterPetDBID(m_Tamer, petSubKey, tamer->GetSystemAddress());
inventoryComponent->AddItem(m_Parent->GetLOT(), 1, eLootSourceType::ACTIVITY, eInventoryType::MODELS, {}, LWOOBJID_EMPTY, true, false, petSubKey);
inventoryComponent->AddItem(m_Parent->GetLOT(), 1, eLootSourceType::INVENTORY, eInventoryType::MODELS, {}, LWOOBJID_EMPTY, true, false, petSubKey);
auto* item = inventoryComponent->FindItemBySubKey(petSubKey, MODELS);
if (item == nullptr) {

View File

@@ -27,7 +27,7 @@
#include "dpShapeBox.h"
#include "dpShapeSphere.h"
PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent) : PhysicsComponent(parent) {
PhantomPhysicsComponent::PhantomPhysicsComponent(Entity* parent, int32_t componentId) : PhysicsComponent(parent, componentId) {
m_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation();
m_Scale = m_Parent->GetDefaultScale();

View File

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

View File

@@ -14,10 +14,21 @@
#include "EntityInfo.h"
PhysicsComponent::PhysicsComponent(Entity* parent) : Component(parent) {
PhysicsComponent::PhysicsComponent(Entity* parent, int32_t componentId) : Component(parent) {
m_Position = NiPoint3Constant::ZERO;
m_Rotation = NiQuaternionConstant::IDENTITY;
m_DirtyPosition = false;
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::GetTable<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) {

View File

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

View File

@@ -38,7 +38,7 @@ void ProximityMonitorComponent::SetProximityRadius(dpEntity* entity, const std::
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);
if (iter == m_ProximitiesData.cend()) {

View File

@@ -46,7 +46,7 @@ public:
* @param name the proximity name to retrieve physics objects for
* @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

View File

@@ -35,7 +35,8 @@
RacingControlComponent::RacingControlComponent(Entity* parent)
: Component(parent) {
m_PathName = u"MainPath";
m_RemainingLaps = 3;
m_NumberOfLaps = 3;
m_RemainingLaps = m_NumberOfLaps;
m_LeadingPlayer = LWOOBJID_EMPTY;
m_RaceBestTime = 0;
m_RaceBestLap = 0;
@@ -658,23 +659,9 @@ void RacingControlComponent::Update(float deltaTime) {
}
}
// Spawn imagination pickups
auto* minSpawner = Game::zoneManager->GetSpawnersByName(
"ImaginationSpawn_Min")[0];
auto* medSpawner = Game::zoneManager->GetSpawnersByName(
"ImaginationSpawn_Med")[0];
auto* maxSpawner = Game::zoneManager->GetSpawnersByName(
"ImaginationSpawn_Max")[0];
minSpawner->Activate();
if (m_LoadedPlayers > 2) {
medSpawner->Activate();
}
if (m_LoadedPlayers > 4) {
maxSpawner->Activate();
}
GameMessages::ZoneLoadedInfo zoneLoadInfo{};
zoneLoadInfo.maxPlayers = m_LoadedPlayers;
m_Parent->GetScript()->OnZoneLoadedInfo(m_Parent, zoneLoadInfo);
// Reset players to their start location, without smashing them
for (auto& player : m_RacingPlayers) {
@@ -764,7 +751,7 @@ void RacingControlComponent::Update(float deltaTime) {
// new checkpoint
uint32_t respawnIndex = 0;
for (const auto& waypoint : path->pathWaypoints) {
if (player.lap == 3) {
if (player.lap == m_NumberOfLaps) {
break;
}
@@ -835,7 +822,7 @@ void RacingControlComponent::Update(float deltaTime) {
// Progress lap time tasks
missionComponent->Progress(eMissionTaskType::RACING, lapTime.count(), static_cast<LWOOBJID>(eRacingTaskParam::LAP_TIME));
if (player.lap == 3) {
if (player.lap == m_NumberOfLaps) {
m_Finished++;
player.finished = m_Finished;
@@ -882,3 +869,20 @@ void RacingControlComponent::Update(float deltaTime) {
}
}
}
void RacingControlComponent::MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg) {
for (const auto& dataUnique : msg.racingSettings) {
if (!dataUnique) continue;
const auto* const data = dataUnique.get();
if (data->GetKey() == u"Race_PathName" && data->GetValueType() == LDF_TYPE_UTF_16) {
m_PathName = static_cast<const LDFData<std::u16string>*>(data)->GetValue();
} else if (data->GetKey() == u"activityID" && data->GetValueType() == LDF_TYPE_S32) {
m_ActivityID = static_cast<const LDFData<int32_t>*>(data)->GetValue();
} else if (data->GetKey() == u"Number_of_Laps" && data->GetValueType() == LDF_TYPE_S32) {
m_NumberOfLaps = static_cast<const LDFData<int32_t>*>(data)->GetValue();
m_RemainingLaps = m_NumberOfLaps;
} else if (data->GetKey() == u"Minimum_Players_for_Group_Achievements" && data->GetValueType() == LDF_TYPE_S32) {
m_MinimumPlayersForGroupAchievements = static_cast<const LDFData<int32_t>*>(data)->GetValue();
}
}
}

View File

@@ -152,6 +152,8 @@ public:
*/
RacingPlayerInfo* GetPlayerData(LWOOBJID playerID);
void MsgConfigureRacingControl(const GameMessages::ConfigureRacingControl& msg);
private:
/**
@@ -161,11 +163,13 @@ private:
/**
* The paths that are followed for the camera scenes
* Configurable in the ConfigureRacingControl msg with the key `Race_PathName`.
*/
std::u16string m_PathName;
/**
* The ID of the activity for participating in this race
* Configurable in the ConfigureRacingControl msg with the key `activityID`.
*/
uint32_t m_ActivityID;
@@ -245,5 +249,20 @@ private:
* Value for message box response to know if we are exiting the race via the activity dialogue
*/
const int32_t m_ActivityExitConfirm = 1;
bool m_AllPlayersReady = false;
/**
* @brief The number of laps in this race. Configurable in the ConfigureRacingControl msg
* with the key `Number_of_Laps`.
*
*/
int32_t m_NumberOfLaps{ 3 };
/**
* @brief The minimum number of players required to progress group achievements.
* Configurable with the ConfigureRacingControl msg with the key `Minimum_Players_for_Group_Achievements`.
*
*/
int32_t m_MinimumPlayersForGroupAchievements{ 2 };
};

View File

@@ -12,7 +12,7 @@
#include "dpShapeSphere.h"
#include"EntityInfo.h"
RigidbodyPhantomPhysicsComponent::RigidbodyPhantomPhysicsComponent(Entity* parent) : PhysicsComponent(parent) {
RigidbodyPhantomPhysicsComponent::RigidbodyPhantomPhysicsComponent(Entity* parent, int32_t componentId) : PhysicsComponent(parent, componentId) {
m_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation();
m_Scale = m_Parent->GetDefaultScale();

View File

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

View File

@@ -13,7 +13,7 @@
#include "Entity.h"
SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, uint32_t componentID) : PhysicsComponent(parent) {
SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, int32_t componentID) : PhysicsComponent(parent, componentID) {
m_Position = m_Parent->GetDefaultPosition();
m_Rotation = m_Parent->GetDefaultRotation();

View File

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

View File

@@ -123,6 +123,11 @@ void SkillComponent::SyncPlayerProjectile(const LWOOBJID projectileId, RakNet::B
behavior->Handle(sync_entry.context, bitStream, branch);
this->m_managedProjectiles.erase(this->m_managedProjectiles.begin() + index);
GameMessages::ActivityNotify notify;
notify.notification.push_back( std::make_unique<LDFData<int32_t>>(u"shot_done", sync_entry.skillId));
m_Parent->OnActivityNotify(notify);
}
void SkillComponent::RegisterPlayerProjectile(const LWOOBJID projectileId, BehaviorContext* context, const BehaviorBranchContext& branch, const LOT lot) {
@@ -132,6 +137,7 @@ void SkillComponent::RegisterPlayerProjectile(const LWOOBJID projectileId, Behav
entry.branchContext = branch;
entry.lot = lot;
entry.id = projectileId;
entry.skillId = context->skillID;
this->m_managedProjectiles.push_back(entry);
}

View File

@@ -40,6 +40,8 @@ struct ProjectileSyncEntry {
BehaviorBranchContext branchContext{ 0, 0 };
int32_t skillId{ 0 };
explicit ProjectileSyncEntry();
};

View File

@@ -82,7 +82,6 @@ void SwitchComponent::EntityEnter(Entity* entity) {
RenderComponent::PlayAnimation(m_Parent, u"engaged");
m_PetBouncer->SetPetBouncerEnabled(true);
} 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);
}

View File

@@ -104,6 +104,18 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
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: {
GameMessages::SendRestoreToPostLoadStats(entity, sysAddr);
entity->SetPlayerReadyForUpdates();
@@ -691,6 +703,12 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System
case MessageType::Game::UPDATE_INVENTORY_GROUP_CONTENTS:
GameMessages::HandleUpdateInventoryGroupContents(inStream, entity, sysAddr);
break;
case MessageType::Game::SHOOTING_GALLERY_FIRE: {
GameMessages::ShootingGalleryFire fire{};
fire.Deserialize(inStream);
fire.Handle(*entity, sysAddr);
break;
}
default:
LOG_DEBUG("Received Unknown GM with ID: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data());

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