mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-17 04:04:21 +00:00
Compare commits
143 Commits
v3.1.0
...
web-dashbo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3467465b4 | ||
|
|
d532a9b063 | ||
|
|
5453d163a3 | ||
|
|
62ac65c520 | ||
|
|
5d5bce53d0 | ||
|
|
5791c55a9e | ||
|
|
17d0c45382 | ||
|
|
7dbbef81ac | ||
|
|
06958cb9cd | ||
|
|
69b1a694a6 | ||
|
|
b2609ff6cb | ||
|
|
e8c0b3e6da | ||
|
|
25418fd8b2 | ||
|
|
502c965d97 | ||
|
|
205c190c61 | ||
|
|
670cb124c0 | ||
|
|
76c2f380bf | ||
|
|
b5a3cc9187 | ||
|
|
74e1d36bb1 | ||
|
|
64faac714c | ||
|
|
4a5dd68e87 | ||
|
|
4a577f233d | ||
|
|
bb05b3ac0d | ||
|
|
06022e4b19 | ||
|
|
6389876c6e | ||
|
|
68f2e2dee2 | ||
|
|
b798da8ef8 | ||
|
|
154112050f | ||
|
|
6d3bf2fdc3 | ||
|
|
566a18df38 | ||
|
|
f6c13d9ee6 | ||
|
|
8198ad70f6 | ||
|
|
4c3bace601 | ||
|
|
6d2a21450b | ||
|
|
f9e74e6994 | ||
|
|
21a2ddcfd9 | ||
|
|
50e6cf9059 | ||
|
|
3364884126 | ||
|
|
3890c0a86c | ||
|
|
c083f21e44 | ||
|
|
c9e95839ee | ||
|
|
dd957ed0c7 | ||
|
|
12296ce553 | ||
|
|
24f4c9d413 | ||
|
|
ba964932b7 | ||
|
|
4c42eea819 | ||
|
|
6b52cf67a0 | ||
|
|
71f708f1b5 | ||
|
|
49aa632d42 | ||
|
|
5ec4142ca1 | ||
|
|
5e9fe40bec | ||
|
|
9524198044 | ||
|
|
a5d0788488 | ||
|
|
a1ba5b8f12 | ||
|
|
48510b7315 | ||
|
|
c697f8ad97 | ||
|
|
55d181ea4b | ||
|
|
ecbb465020 | ||
|
|
ec9927acbb | ||
|
|
1f580491c7 | ||
|
|
2618e9a864 | ||
|
|
0f0d0a6dee | ||
|
|
f63a9a6bea | ||
|
|
f0f98a6108 | ||
|
|
4ed7bd6767 | ||
|
|
9f92f48a0f | ||
|
|
48e3471831 | ||
|
|
3c244cce27 | ||
|
|
8ba35be64d | ||
|
|
f7c9267ba4 | ||
|
|
b6e9d6872d | ||
|
|
c83797984a | ||
|
|
04487efa25 | ||
|
|
2f315d9288 | ||
|
|
6ae1c7a376 | ||
|
|
c19ee04c8a | ||
|
|
2858345269 | ||
|
|
37e14979a4 | ||
|
|
b509fd4f10 | ||
|
|
820c0f0083 | ||
|
|
68eb20966f | ||
|
|
92155a3cb4 | ||
|
|
437362cce6 | ||
|
|
34665f6f5c | ||
|
|
32487dcd5f | ||
|
|
891b176b4f | ||
|
|
e42df5b02e | ||
|
|
61921cfb62 | ||
|
|
91f6b2bf81 | ||
|
|
01917841cb | ||
|
|
e18c504ee4 | ||
|
|
b6f7b4c092 | ||
|
|
522299c9ec | ||
|
|
0e551429d3 | ||
|
|
c77e9ce33a | ||
|
|
3ebc6709db | ||
|
|
841b754b01 | ||
|
|
62c3f489fe | ||
|
|
5ccb8357fd | ||
|
|
4bacb8a2ee | ||
|
|
89678c4a05 | ||
|
|
4f97ecc073 | ||
|
|
c9e4cde68d | ||
|
|
0a12672889 | ||
|
|
00a69909f8 | ||
|
|
7c8ca1c1cb | ||
|
|
4930fb93b3 | ||
|
|
b31f9670d1 | ||
|
|
1cc1782b35 | ||
|
|
55d409eb82 | ||
|
|
65f3c33ca5 | ||
|
|
93fa4e268f | ||
|
|
1fb1da101c | ||
|
|
6f94043b33 | ||
|
|
5785764a95 | ||
|
|
fa53fa7935 | ||
|
|
6b0f3a66e9 | ||
|
|
f5c212fb86 | ||
|
|
3d595ce4ac | ||
|
|
99f6cf2d92 | ||
|
|
bc0f3d9163 | ||
|
|
20d5a9b6d8 | ||
|
|
c490d45fe0 | ||
|
|
aa49aaae76 | ||
|
|
f78baee534 | ||
|
|
347fc46f01 | ||
|
|
d104559cc4 | ||
|
|
b702843011 | ||
|
|
14d7dec6a8 | ||
|
|
6eaf0a153e | ||
|
|
78e52904e5 | ||
|
|
b388b03251 | ||
|
|
ae37641635 | ||
|
|
566791e647 | ||
|
|
306d959a83 | ||
|
|
a07d54e513 | ||
|
|
e4c2eecbc7 | ||
|
|
b01b3cc38d | ||
|
|
b7c579fb84 | ||
|
|
7b1d6948c3 | ||
|
|
1b3cdc6d9c | ||
|
|
d860552776 | ||
|
|
6cd1310460 |
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -16,7 +16,10 @@ body:
|
||||
I have validated that this issue is not a syntax error of either MySQL or SQLite.
|
||||
required: true
|
||||
- label: >
|
||||
I have pulled the latest version of the main branch of DarkflameServer and have confirmed that the issue exists there.
|
||||
I have downloaded/pulled the latest version of the main branch of DarkflameServer and have confirmed that the issue exists there.
|
||||
required: true
|
||||
- label: >
|
||||
I have verified that my boot.cfg is configured as per the [README](https://github.com/DarkflameUniverse/DarkflameServer?tab=readme-ov-file#allowing-a-user-to-connect-to-your-server).
|
||||
required: true
|
||||
- type: input
|
||||
id: server-version
|
||||
|
||||
12
.github/workflows/build-and-test.yml
vendored
12
.github/workflows/build-and-test.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
os: [ windows-2022, ubuntu-22.04, macos-13 ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Add msbuild to PATH (Windows only)
|
||||
if: ${{ matrix.os == 'windows-2022' }}
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
uses: microsoft/setup-msbuild@767f00a3f09872d96a0cb9fcd5e6a4ff33311330
|
||||
with:
|
||||
vs-version: '[17,18)'
|
||||
msbuild-architecture: x64
|
||||
@@ -30,12 +30,16 @@ jobs:
|
||||
run: |
|
||||
brew install openssl@3
|
||||
sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
|
||||
- name: Get CMake 3.x
|
||||
uses: lukka/get-cmake@28983e0d3955dba2bb0a6810caae0c6cf268ec0c
|
||||
with:
|
||||
cmakeVersion: "~3.25.0" # <--= optional, use most recent 3.25.x version
|
||||
- name: cmake
|
||||
uses: lukka/run-cmake@v10
|
||||
uses: lukka/run-cmake@67c73a83a46f86c4e0b96b741ac37ff495478c38
|
||||
with:
|
||||
workflowPreset: "ci-${{matrix.os}}"
|
||||
- name: artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
|
||||
with:
|
||||
name: build-${{matrix.os}}
|
||||
path: |
|
||||
|
||||
@@ -19,6 +19,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debuggi
|
||||
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_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
set(FETCHCONTENT_QUIET FALSE) # GLM takes a long time to clone, this will at least show _something_ while its downloading
|
||||
|
||||
# Read variables from file
|
||||
FILE(READ "${CMAKE_SOURCE_DIR}/CMakeVariables.txt" variables)
|
||||
@@ -88,6 +89,7 @@ elseif(MSVC)
|
||||
add_compile_options("/wd4267" "/utf-8" "/volatile:iso" "/Zc:inline")
|
||||
elseif(WIN32)
|
||||
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
|
||||
add_compile_definitions(NOMINMAX)
|
||||
endif()
|
||||
|
||||
# Our output dir
|
||||
@@ -125,7 +127,7 @@ endif()
|
||||
message(STATUS "Variable: DLU_CONFIG_DIR = ${DLU_CONFIG_DIR}")
|
||||
|
||||
# Copy resource files on first build
|
||||
set(RESOURCE_FILES "sharedconfig.ini" "authconfig.ini" "chatconfig.ini" "worldconfig.ini" "masterconfig.ini" "blocklist.dcf")
|
||||
set(RESOURCE_FILES "sharedconfig.ini" "authconfig.ini" "chatconfig.ini" "dashboardconfig.ini" "worldconfig.ini" "masterconfig.ini" "blocklist.dcf")
|
||||
message(STATUS "Checking resource file integrity")
|
||||
|
||||
include(Utils)
|
||||
@@ -235,6 +237,8 @@ include_directories(
|
||||
|
||||
"dNet"
|
||||
|
||||
"dWeb"
|
||||
|
||||
"tests"
|
||||
"tests/dCommonTests"
|
||||
"tests/dGameTests"
|
||||
@@ -250,6 +254,7 @@ include_directories(
|
||||
"thirdparty/MD5"
|
||||
"thirdparty/nlohmann"
|
||||
"thirdparty/mongoose"
|
||||
"thirdparty/inja"
|
||||
)
|
||||
|
||||
# Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux)
|
||||
@@ -301,9 +306,10 @@ add_subdirectory(dZoneManager)
|
||||
add_subdirectory(dNavigation)
|
||||
add_subdirectory(dPhysics)
|
||||
add_subdirectory(dServer)
|
||||
add_subdirectory(dWeb)
|
||||
|
||||
# Create a list of common libraries shared between all binaries
|
||||
set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
|
||||
set(COMMON_LIBRARIES glm::glm "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
|
||||
|
||||
# Add platform specific common libraries
|
||||
if(UNIX)
|
||||
@@ -318,6 +324,7 @@ endif()
|
||||
add_subdirectory(dWorldServer)
|
||||
add_subdirectory(dAuthServer)
|
||||
add_subdirectory(dChatServer)
|
||||
add_subdirectory(dDashboardServer)
|
||||
add_subdirectory(dMasterServer) # Add MasterServer last so it can rely on the other binaries
|
||||
|
||||
target_precompile_headers(
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -11,7 +11,12 @@ COPY --chmod=0500 ./build.sh /app/
|
||||
|
||||
RUN sed -i 's/MARIADB_CONNECTOR_COMPILE_JOBS__=.*/MARIADB_CONNECTOR_COMPILE_JOBS__=2/' /app/CMakeVariables.txt
|
||||
|
||||
RUN ./build.sh
|
||||
RUN --mount=type=cache,target=/app/build,id=build-cache \
|
||||
mkdir -p /app/build /tmp/persisted-build && \
|
||||
cd /app/build && \
|
||||
cmake .. && \
|
||||
make -j$(nproc --ignore 1) && \
|
||||
cp -r /app/build/* /tmp/persisted-build/
|
||||
|
||||
FROM debian:12 as runtime
|
||||
|
||||
@@ -23,23 +28,23 @@ RUN --mount=type=cache,id=build-apt-cache,target=/var/cache/apt \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Grab libraries and load them
|
||||
COPY --from=build /app/build/mariadbcpp/libmariadbcpp.so /usr/local/lib/
|
||||
COPY --from=build /tmp/persisted-build/mariadbcpp/libmariadbcpp.so /usr/local/lib/
|
||||
RUN ldconfig
|
||||
|
||||
# Server bins
|
||||
COPY --from=build /app/build/*Server /app/
|
||||
COPY --from=build /tmp/persisted-build/*Server /app/
|
||||
|
||||
# Necessary suplimentary files
|
||||
COPY --from=build /app/build/*.ini /app/configs/
|
||||
COPY --from=build /app/build/vanity/*.* /app/vanity/
|
||||
COPY --from=build /app/build/navmeshes /app/navmeshes
|
||||
COPY --from=build /app/build/migrations /app/migrations
|
||||
COPY --from=build /app/build/*.dcf /app/
|
||||
COPY --from=build /tmp/persisted-build/*.ini /app/configs/
|
||||
COPY --from=build /tmp/persisted-build/vanity/*.* /app/vanity/
|
||||
COPY --from=build /tmp/persisted-build/navmeshes /app/navmeshes
|
||||
COPY --from=build /tmp/persisted-build/migrations /app/migrations
|
||||
COPY --from=build /tmp/persisted-build/*.dcf /app/
|
||||
|
||||
# backup of config and vanity files to copy to the host incase
|
||||
# of a mount clobbering the copy from above
|
||||
COPY --from=build /app/build/*.ini /app/default-configs/
|
||||
COPY --from=build /app/build/vanity/*.* /app/default-vanity/
|
||||
COPY --from=build /tmp/persisted-build/*.ini /app/default-configs/
|
||||
COPY --from=build /tmp/persisted-build/vanity/*.* /app/default-vanity/
|
||||
|
||||
# needed as the container runs with the root user
|
||||
# and therefore sudo doesn't exist
|
||||
|
||||
15
README.md
15
README.md
@@ -78,7 +78,7 @@ git clone --recursive https://github.com/DarkflameUniverse/DarkflameServer
|
||||
|
||||
### Windows packages
|
||||
Ensure that you have either the [MSVC C++ compiler](https://visualstudio.microsoft.com/vs/features/cplusplus/) (recommended) or the [Clang compiler](https://github.com/llvm/llvm-project/releases/) installed.
|
||||
You'll also need to download and install [CMake](https://cmake.org/download/) (version <font size="4">**CMake version 3.25**</font> or later!).
|
||||
You'll also need to download and install [CMake](https://cmake.org/download/) (<font size="4">**version 3.25**</font> up to <font size="4">**version 3.31**</font>!).
|
||||
|
||||
### MacOS packages
|
||||
Ensure you have [brew](https://brew.sh) installed.
|
||||
@@ -100,7 +100,7 @@ sudo apt install build-essential gcc zlib1g-dev libssl-dev openssl mariadb-serve
|
||||
```
|
||||
|
||||
#### Required CMake version
|
||||
This project uses <font size="4">**CMake version 3.25**</font> or higher and as such you will need to ensure you have this version installed.
|
||||
This project uses <font size="4">**CMake version 3.25**</font> up to <font size="4">**version 3.31**</font> and as such you will need to ensure you have this version installed.
|
||||
You can check your CMake version by using the following command in a terminal.
|
||||
```bash
|
||||
cmake --version
|
||||
@@ -187,7 +187,8 @@ Now that you are logged in, run the following commands.
|
||||
```bash
|
||||
# Creates a user for this computer which uses a password and grant said user all privileges.
|
||||
# Change mydarkflameuser to a custom username and password to a custom password.
|
||||
GRANT ALL ON *.* TO 'mydarkflameuser'@'localhost' IDENTIFIED BY 'password' WITH GRANT OPTION;
|
||||
CREATE USER 'mydarkflameuser'@'localhost' IDENTIFIED BY 'password';
|
||||
GRANT ALL ON *.* TO 'mydarkflameuser'@'localhost' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
# Then create a database for Darkflame Universe to use.
|
||||
@@ -324,13 +325,15 @@ While a character has a gmlevel of anything but `0`, some gameplay behavior will
|
||||
Some changes to the client `boot.cfg` file are needed to play on your server.
|
||||
|
||||
## Allowing a user to connect to your server
|
||||
**ALL OF THESE CHANGES ARE REQUIRED. PLEASE FULLY READ THIS SECTION**
|
||||
|
||||
To connect to a server follow these steps:
|
||||
* In the client directory, locate `boot.cfg`
|
||||
* Open it in a text editor and locate where it says `AUTHSERVERIP=0:`
|
||||
* Replace the contents after to `:` and the following `,` with what you configured as the server's public facing IP. For example `AUTHSERVERIP=0:localhost` for locally hosted servers
|
||||
* Next locate the line `UGCUSE3DSERVICES=7:`
|
||||
* Open `boot.cfg` in a text editor and locate the line `UGCUSE3DSERVICES=7:`
|
||||
* Ensure the number after the 7 is a `0`
|
||||
* Alternatively, remove the line with `UGCUSE3DSERVICES` altogether
|
||||
* Next locate where it says `AUTHSERVERIP=0:`
|
||||
* Replace the contents after to `:` and the following `,` with what you configured as the server's public facing IP. For example `AUTHSERVERIP=0:localhost` for locally hosted servers
|
||||
* Launch `legouniverse.exe`, through `wine` if on a Unix-like operating system
|
||||
* Note that if you are on WSL2, you will need to configure the public IP in the server and client to be the IP of the WSL2 instance and not localhost, which can be found by running `ifconfig` in the terminal. Windows defaults to WSL1, so this will not apply to most users.
|
||||
As an example, here is what the boot.cfg is required to contain for a server with the ip 12.34.56.78
|
||||
|
||||
@@ -6,6 +6,8 @@ FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG release-1.12.1
|
||||
GIT_PROGRESS TRUE
|
||||
GIT_SHALLOW 1
|
||||
)
|
||||
|
||||
# For Windows: Prevent overriding the parent project's compiler/linker settings
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
//Auth includes:
|
||||
#include "AuthPackets.h"
|
||||
#include "eConnectionType.h"
|
||||
#include "ServiceType.h"
|
||||
#include "MessageType/Server.h"
|
||||
#include "MessageType/Auth.h"
|
||||
|
||||
@@ -52,6 +52,7 @@ int main(int argc, char** argv) {
|
||||
//Create all the objects we need to run our service:
|
||||
Server::SetupLogger("AuthServer");
|
||||
if (!Game::logger) return EXIT_FAILURE;
|
||||
Game::config->LogSettings();
|
||||
|
||||
LOG("Starting Auth server...");
|
||||
LOG("Version: %s", PROJECT_VERSION);
|
||||
@@ -92,7 +93,7 @@ int main(int argc, char** argv) {
|
||||
const auto externalIPString = Game::config->GetValue("external_ip");
|
||||
if (!externalIPString.empty()) ourIP = externalIPString;
|
||||
|
||||
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Auth, Game::config, &Game::lastSignal, masterPassword);
|
||||
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServiceType::AUTH, Game::config, &Game::lastSignal, masterPassword);
|
||||
|
||||
//Run it until server gets a kill message from Master:
|
||||
auto t = std::chrono::high_resolution_clock::now();
|
||||
@@ -167,11 +168,11 @@ void HandlePacket(Packet* packet) {
|
||||
if (packet->length < 4) return;
|
||||
|
||||
if (packet->data[0] == ID_USER_PACKET_ENUM) {
|
||||
if (static_cast<eConnectionType>(packet->data[1]) == eConnectionType::SERVER) {
|
||||
if (static_cast<ServiceType>(packet->data[1]) == ServiceType::COMMON) {
|
||||
if (static_cast<MessageType::Server>(packet->data[3]) == MessageType::Server::VERSION_CONFIRM) {
|
||||
AuthPackets::HandleHandshake(Game::server, packet);
|
||||
}
|
||||
} else if (static_cast<eConnectionType>(packet->data[1]) == eConnectionType::AUTH) {
|
||||
} else if (static_cast<ServiceType>(packet->data[1]) == ServiceType::AUTH) {
|
||||
if (static_cast<MessageType::Auth>(packet->data[3]) == MessageType::Auth::LOGIN_REQUEST) {
|
||||
AuthPackets::HandleLoginRequest(Game::server, packet);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
set(DCHATFILTER_SOURCES "dChatFilter.cpp")
|
||||
|
||||
add_library(dChatFilter STATIC ${DCHATFILTER_SOURCES})
|
||||
target_link_libraries(dChatFilter dDatabase)
|
||||
target_link_libraries(dChatFilter dDatabase glm::glm)
|
||||
|
||||
@@ -105,7 +105,7 @@ void dChatFilter::ExportWordlistToDCF(const std::string& filepath, bool allowLis
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList) {
|
||||
std::set<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList) {
|
||||
if (gmLevel > eGameMasterLevel::FORUM_MODERATOR) return { }; //If anything but a forum mod, return true.
|
||||
if (message.empty()) return { };
|
||||
if (!allowList && m_DeniedWords.empty()) return { { 0, message.length() } };
|
||||
@@ -114,7 +114,7 @@ std::vector<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::
|
||||
std::string segment;
|
||||
std::regex reg("(!*|\\?*|\\;*|\\.*|\\,*)");
|
||||
|
||||
std::vector<std::pair<uint8_t, uint8_t>> listOfBadSegments = std::vector<std::pair<uint8_t, uint8_t>>();
|
||||
std::set<std::pair<uint8_t, uint8_t>> listOfBadSegments;
|
||||
|
||||
uint32_t position = 0;
|
||||
|
||||
@@ -127,17 +127,17 @@ std::vector<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::
|
||||
size_t hash = CalculateHash(segment);
|
||||
|
||||
if (std::find(m_UserUnapprovedWordCache.begin(), m_UserUnapprovedWordCache.end(), hash) != m_UserUnapprovedWordCache.end() && allowList) {
|
||||
listOfBadSegments.emplace_back(position, originalSegment.length());
|
||||
listOfBadSegments.emplace(position, originalSegment.length());
|
||||
}
|
||||
|
||||
if (std::find(m_ApprovedWords.begin(), m_ApprovedWords.end(), hash) == m_ApprovedWords.end() && allowList) {
|
||||
m_UserUnapprovedWordCache.push_back(hash);
|
||||
listOfBadSegments.emplace_back(position, originalSegment.length());
|
||||
listOfBadSegments.emplace(position, originalSegment.length());
|
||||
}
|
||||
|
||||
if (std::find(m_DeniedWords.begin(), m_DeniedWords.end(), hash) != m_DeniedWords.end() && !allowList) {
|
||||
m_UserUnapprovedWordCache.push_back(hash);
|
||||
listOfBadSegments.emplace_back(position, originalSegment.length());
|
||||
listOfBadSegments.emplace(position, originalSegment.length());
|
||||
}
|
||||
|
||||
position += originalSegment.length() + 1;
|
||||
|
||||
@@ -24,7 +24,7 @@ public:
|
||||
void ReadWordlistPlaintext(const std::string& filepath, bool allowList);
|
||||
bool ReadWordlistDCF(const std::string& filepath, bool allowList);
|
||||
void ExportWordlistToDCF(const std::string& filepath, bool allowList);
|
||||
std::vector<std::pair<uint8_t, uint8_t>> IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList = true);
|
||||
std::set<std::pair<uint8_t, uint8_t>> IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList = true);
|
||||
|
||||
private:
|
||||
bool m_DontGenerateDCF;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
set(DCHATSERVER_SOURCES
|
||||
"ChatIgnoreList.cpp"
|
||||
"ChatPacketHandler.cpp"
|
||||
"ChatJSONUtils.cpp"
|
||||
"ChatWeb.cpp"
|
||||
"PlayerContainer.cpp"
|
||||
"ChatWebAPI.cpp"
|
||||
"JSONUtils.cpp"
|
||||
"TeamContainer.cpp"
|
||||
)
|
||||
|
||||
add_executable(ChatServer "ChatServer.cpp")
|
||||
target_include_directories(ChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dChatFilter")
|
||||
target_include_directories(ChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dChatFilter" "${PROJECT_SOURCE_DIR}/dWeb")
|
||||
add_compile_definitions(ChatServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}\"")
|
||||
|
||||
add_library(dChatServer ${DCHATSERVER_SOURCES})
|
||||
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer")
|
||||
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer" "${PROJECT_SOURCE_DIR}/dChatFilter")
|
||||
|
||||
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter)
|
||||
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose)
|
||||
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter glm::glm)
|
||||
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose dWeb)
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
// not allowing teams, rejecting DMs, friends requets etc.
|
||||
// The only thing not auto-handled is instance activities force joining the team on the server.
|
||||
|
||||
void WriteOutgoingReplyHeader(RakNet::BitStream& bitStream, const LWOOBJID& receivingPlayer, const ChatIgnoreList::Response type) {
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
void WriteOutgoingReplyHeader(RakNet::BitStream& bitStream, const LWOOBJID& receivingPlayer, const MessageType::Client type) {
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receivingPlayer);
|
||||
|
||||
//portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, type);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, type);
|
||||
}
|
||||
|
||||
void ChatIgnoreList::GetIgnoreList(Packet* packet) {
|
||||
@@ -34,7 +34,7 @@ void ChatIgnoreList::GetIgnoreList(Packet* packet) {
|
||||
if (!receiver.ignoredPlayers.empty()) {
|
||||
LOG_DEBUG("Player %llu already has an ignore list, but is requesting it again.", playerId);
|
||||
} else {
|
||||
auto ignoreList = Database::Get()->GetIgnoreList(static_cast<uint32_t>(playerId));
|
||||
auto ignoreList = Database::Get()->GetIgnoreList(playerId);
|
||||
if (ignoreList.empty()) {
|
||||
LOG_DEBUG("Player %llu has no ignores", playerId);
|
||||
return;
|
||||
@@ -43,14 +43,13 @@ void ChatIgnoreList::GetIgnoreList(Packet* packet) {
|
||||
for (auto& ignoredPlayer : ignoreList) {
|
||||
receiver.ignoredPlayers.emplace_back(ignoredPlayer.name, ignoredPlayer.id);
|
||||
GeneralUtils::SetBit(receiver.ignoredPlayers.back().playerId, eObjectBits::CHARACTER);
|
||||
GeneralUtils::SetBit(receiver.ignoredPlayers.back().playerId, eObjectBits::PERSISTENT);
|
||||
}
|
||||
}
|
||||
|
||||
CBITSTREAM;
|
||||
WriteOutgoingReplyHeader(bitStream, receiver.playerID, ChatIgnoreList::Response::GET_IGNORE);
|
||||
WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::GET_IGNORE_LIST_RESPONSE);
|
||||
|
||||
bitStream.Write<uint8_t>(false); // Probably is Is Free Trial, but we don't care about that
|
||||
bitStream.Write<uint8_t>(false); // Is Free Trial, but we don't care about that
|
||||
bitStream.Write<uint16_t>(0); // literally spacing due to struct alignment
|
||||
|
||||
bitStream.Write<uint16_t>(receiver.ignoredPlayers.size());
|
||||
@@ -86,7 +85,7 @@ void ChatIgnoreList::AddIgnore(Packet* packet) {
|
||||
std::string toIgnoreStr = toIgnoreName.GetAsString();
|
||||
|
||||
CBITSTREAM;
|
||||
WriteOutgoingReplyHeader(bitStream, receiver.playerID, ChatIgnoreList::Response::ADD_IGNORE);
|
||||
WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::ADD_IGNORE_RESPONSE);
|
||||
|
||||
// Check if the player exists
|
||||
LWOOBJID ignoredPlayerId = LWOOBJID_EMPTY;
|
||||
@@ -114,9 +113,8 @@ void ChatIgnoreList::AddIgnore(Packet* packet) {
|
||||
}
|
||||
|
||||
if (ignoredPlayerId != LWOOBJID_EMPTY) {
|
||||
Database::Get()->AddIgnore(static_cast<uint32_t>(playerId), static_cast<uint32_t>(ignoredPlayerId));
|
||||
Database::Get()->AddIgnore(playerId, ignoredPlayerId);
|
||||
GeneralUtils::SetBit(ignoredPlayerId, eObjectBits::CHARACTER);
|
||||
GeneralUtils::SetBit(ignoredPlayerId, eObjectBits::PERSISTENT);
|
||||
|
||||
receiver.ignoredPlayers.emplace_back(toIgnoreStr, ignoredPlayerId);
|
||||
LOG_DEBUG("Player %llu is ignoring %s", playerId, toIgnoreStr.c_str());
|
||||
@@ -157,11 +155,11 @@ void ChatIgnoreList::RemoveIgnore(Packet* packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
Database::Get()->RemoveIgnore(static_cast<uint32_t>(playerId), static_cast<uint32_t>(toRemove->playerId));
|
||||
Database::Get()->RemoveIgnore(playerId, toRemove->playerId);
|
||||
receiver.ignoredPlayers.erase(toRemove, receiver.ignoredPlayers.end());
|
||||
|
||||
CBITSTREAM;
|
||||
WriteOutgoingReplyHeader(bitStream, receiver.playerID, ChatIgnoreList::Response::REMOVE_IGNORE);
|
||||
WriteOutgoingReplyHeader(bitStream, receiver.playerID, MessageType::Client::REMOVE_IGNORE_RESPONSE);
|
||||
|
||||
bitStream.Write<int8_t>(0);
|
||||
LUWString playerNameSend(removedIgnoreStr, 33);
|
||||
|
||||
@@ -5,17 +5,16 @@ struct Packet;
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* @brief The ignore list allows players to ignore someone silently. Requests will generally be blocked by the client, but they should also be checked
|
||||
* on the server as well so the sender can get a generic error code in response.
|
||||
*
|
||||
*/
|
||||
namespace ChatIgnoreList {
|
||||
void GetIgnoreList(Packet* packet);
|
||||
void AddIgnore(Packet* packet);
|
||||
void RemoveIgnore(Packet* packet);
|
||||
|
||||
enum class Response : uint8_t {
|
||||
ADD_IGNORE = 32,
|
||||
REMOVE_IGNORE = 33,
|
||||
GET_IGNORE = 34,
|
||||
};
|
||||
|
||||
enum class AddResponse : uint8_t {
|
||||
SUCCESS,
|
||||
ALREADY_IGNORED,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "JSONUtils.h"
|
||||
#include "ChatJSONUtils.h"
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
@@ -18,19 +18,12 @@ void to_json(json& data, const PlayerData& playerData) {
|
||||
|
||||
void to_json(json& data, const PlayerContainer& playerContainer) {
|
||||
data = json::array();
|
||||
for(auto& playerData : playerContainer.GetAllPlayers()) {
|
||||
for (auto& playerData : playerContainer.GetAllPlayers()) {
|
||||
if (playerData.first == LWOOBJID_EMPTY) continue;
|
||||
data.push_back(playerData.second);
|
||||
}
|
||||
}
|
||||
|
||||
void to_json(json& data, const TeamContainer& teamContainer) {
|
||||
for (auto& teamData : Game::playerContainer.GetTeams()) {
|
||||
if (!teamData) continue;
|
||||
data.push_back(*teamData);
|
||||
}
|
||||
}
|
||||
|
||||
void to_json(json& data, const TeamData& teamData) {
|
||||
data["id"] = teamData.teamID;
|
||||
data["loot_flag"] = teamData.lootFlag;
|
||||
@@ -48,15 +41,9 @@ void to_json(json& data, const TeamData& teamData) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string JSONUtils::CheckRequiredData(const json& data, const std::vector<std::string>& requiredData) {
|
||||
json check;
|
||||
check["error"] = json::array();
|
||||
for (const auto& required : requiredData) {
|
||||
if (!data.contains(required)) {
|
||||
check["error"].push_back("Missing Parameter: " + required);
|
||||
} else if (data[required] == "") {
|
||||
check["error"].push_back("Empty Parameter: " + required);
|
||||
}
|
||||
void TeamContainer::to_json(json& data, const TeamContainer::Data& teamContainer) {
|
||||
for (auto& teamData : TeamContainer::GetTeams()) {
|
||||
if (!teamData) continue;
|
||||
data.push_back(*teamData);
|
||||
}
|
||||
return check["error"].empty() ? "" : check.dump();
|
||||
}
|
||||
18
dChatServer/ChatJSONUtils.h
Normal file
18
dChatServer/ChatJSONUtils.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef __CHATJSONUTILS_H__
|
||||
#define __CHATJSONUTILS_H__
|
||||
|
||||
#include "json_fwd.hpp"
|
||||
#include "PlayerContainer.h"
|
||||
#include "TeamContainer.h"
|
||||
|
||||
/* Remember, to_json needs to be in the same namespace as the class its located in */
|
||||
|
||||
void to_json(nlohmann::json& data, const PlayerData& playerData);
|
||||
void to_json(nlohmann::json& data, const PlayerContainer& playerContainer);
|
||||
void to_json(nlohmann::json& data, const TeamData& teamData);
|
||||
|
||||
namespace TeamContainer {
|
||||
void to_json(nlohmann::json& data, const TeamContainer::Data& teamData);
|
||||
};
|
||||
|
||||
#endif // !__CHATJSONUTILS_H__
|
||||
@@ -12,13 +12,14 @@
|
||||
#include "RakString.h"
|
||||
#include "dConfig.h"
|
||||
#include "eObjectBits.h"
|
||||
#include "eConnectionType.h"
|
||||
#include "ServiceType.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "MessageType/Client.h"
|
||||
#include "MessageType/Game.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "ChatPackets.h"
|
||||
#include "TeamContainer.h"
|
||||
|
||||
void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
|
||||
//Get from the packet which player we want to do something with:
|
||||
@@ -34,7 +35,6 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
|
||||
FriendData fd;
|
||||
fd.isFTP = false; // not a thing in DLU
|
||||
fd.friendID = friendData.friendID;
|
||||
GeneralUtils::SetBit(fd.friendID, eObjectBits::PERSISTENT);
|
||||
GeneralUtils::SetBit(fd.friendID, eObjectBits::CHARACTER);
|
||||
|
||||
fd.isBestFriend = friendData.isBestFriend; //0 = friends, 1 = left_requested, 2 = right_requested, 3 = both_accepted - are now bffs
|
||||
@@ -60,11 +60,11 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
|
||||
|
||||
//Now, we need to send the friendlist to the server they came from:
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::GET_FRIENDS_LIST_RESPONSE);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::GET_FRIENDS_LIST_RESPONSE);
|
||||
bitStream.Write<uint8_t>(0);
|
||||
bitStream.Write<uint16_t>(1); //Length of packet -- just writing one as it doesn't matter, client skips it.
|
||||
bitStream.Write<uint16_t>(player.friends.size());
|
||||
@@ -73,7 +73,7 @@ void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) {
|
||||
data.Serialize(bitStream);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr = player.sysAddr;
|
||||
SystemAddress sysAddr = player.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
||||
requesteeFriendData.isOnline = false;
|
||||
requesteeFriendData.zoneID = requestor.zoneID;
|
||||
requestee.friends.push_back(requesteeFriendData);
|
||||
requestee.sysAddr = UNASSIGNED_SYSTEM_ADDRESS;
|
||||
requestee.worldServerSysAddr = UNASSIGNED_SYSTEM_ADDRESS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -160,9 +160,7 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
||||
|
||||
// Set the bits
|
||||
GeneralUtils::SetBit(queryPlayerID, eObjectBits::CHARACTER);
|
||||
GeneralUtils::SetBit(queryPlayerID, eObjectBits::PERSISTENT);
|
||||
GeneralUtils::SetBit(queryFriendID, eObjectBits::CHARACTER);
|
||||
GeneralUtils::SetBit(queryFriendID, eObjectBits::PERSISTENT);
|
||||
|
||||
// Since this player can either be the friend of someone else or be friends with someone else
|
||||
// their column in the database determines what bit gets set. When the value hits 3, they
|
||||
@@ -189,8 +187,8 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
||||
Database::Get()->SetBestFriendStatus(requestorPlayerID, requestee.playerID, bestFriendStatus);
|
||||
// Sent the best friend update here if the value is 3
|
||||
if (bestFriendStatus == 3U) {
|
||||
if (requestee.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestee, requestor, eAddFriendResponseType::ACCEPTED, false, true);
|
||||
if (requestor.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::ACCEPTED, false, true);
|
||||
if (requestee.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestee, requestor, eAddFriendResponseType::ACCEPTED, false, true);
|
||||
if (requestor.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::ACCEPTED, false, true);
|
||||
|
||||
for (auto& friendData : requestor.friends) {
|
||||
if (friendData.friendID == requestee.playerID) {
|
||||
@@ -211,7 +209,7 @@ void ChatPacketHandler::HandleFriendRequest(Packet* packet) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (requestor.sysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::WAITINGAPPROVAL, true, true);
|
||||
if (requestor.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) SendFriendResponse(requestor, requestee, eAddFriendResponseType::WAITINGAPPROVAL, true, true);
|
||||
}
|
||||
} else {
|
||||
auto maxFriends = Game::playerContainer.GetMaxNumberOfFriends();
|
||||
@@ -317,7 +315,6 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
|
||||
}
|
||||
|
||||
// Convert friendID to LWOOBJID
|
||||
GeneralUtils::SetBit(friendID, eObjectBits::PERSISTENT);
|
||||
GeneralUtils::SetBit(friendID, eObjectBits::CHARACTER);
|
||||
|
||||
Database::Get()->RemoveFriend(playerID, friendID);
|
||||
@@ -374,17 +371,17 @@ void ChatPacketHandler::HandleWho(Packet* packet) {
|
||||
bool online = player;
|
||||
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(request.requestor);
|
||||
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::WHO_RESPONSE);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::WHO_RESPONSE);
|
||||
bitStream.Write<uint8_t>(online);
|
||||
bitStream.Write(player.zoneID.GetMapID());
|
||||
bitStream.Write(player.zoneID.GetInstanceID());
|
||||
bitStream.Write(player.zoneID.GetCloneID());
|
||||
bitStream.Write(request.playerName);
|
||||
|
||||
SystemAddress sysAddr = sender.sysAddr;
|
||||
SystemAddress sysAddr = sender.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
@@ -397,10 +394,10 @@ void ChatPacketHandler::HandleShowAll(Packet* packet) {
|
||||
if (!sender) return;
|
||||
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(request.requestor);
|
||||
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::SHOW_ALL_RESPONSE);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::SHOW_ALL_RESPONSE);
|
||||
bitStream.Write<uint8_t>(!request.displayZoneData && !request.displayIndividualPlayers);
|
||||
bitStream.Write(Game::playerContainer.GetPlayerCount());
|
||||
bitStream.Write(Game::playerContainer.GetSimCount());
|
||||
@@ -418,7 +415,7 @@ void ChatPacketHandler::HandleShowAll(Packet* packet) {
|
||||
}
|
||||
}
|
||||
}
|
||||
SystemAddress sysAddr = sender.sysAddr;
|
||||
SystemAddress sysAddr = sender.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
@@ -447,7 +444,7 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) {
|
||||
|
||||
switch (channel) {
|
||||
case eChatChannel::TEAM: {
|
||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||
auto* team = TeamContainer::GetTeam(playerID);
|
||||
if (team == nullptr) return;
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
@@ -519,12 +516,34 @@ void ChatPacketHandler::HandlePrivateChatMessage(Packet* packet) {
|
||||
SendPrivateChatMessage(sender, receiver, sender, message, eChatChannel::GENERAL, eChatMessageResponseCode::NOTFRIENDS);
|
||||
}
|
||||
|
||||
void ChatPacketHandler::OnAchievementNotify(RakNet::BitStream& bitstream, const SystemAddress& sysAddr) {
|
||||
ChatPackets::AchievementNotify notify{};
|
||||
notify.Deserialize(bitstream);
|
||||
const auto& playerData = Game::playerContainer.GetPlayerData(notify.earnerName.GetAsString());
|
||||
if (!playerData) return;
|
||||
|
||||
for (const auto& myFriend : playerData.friends) {
|
||||
auto& friendData = Game::playerContainer.GetPlayerData(myFriend.friendID);
|
||||
if (friendData) {
|
||||
notify.targetPlayerName.string = GeneralUtils::ASCIIToUTF16(friendData.playerName);
|
||||
LOG_DEBUG("Sending achievement notify to %s", notify.targetPlayerName.GetAsString().c_str());
|
||||
|
||||
RakNet::BitStream worldStream;
|
||||
BitStreamUtils::WriteHeader(worldStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
worldStream.Write(friendData.playerID);
|
||||
notify.WriteHeader(worldStream);
|
||||
notify.Serialize(worldStream);
|
||||
Game::server->Send(worldStream, friendData.worldServerSysAddr, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(routeTo.playerID);
|
||||
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE);
|
||||
bitStream.Write(sender.playerID);
|
||||
bitStream.Write(channel);
|
||||
bitStream.Write<uint32_t>(0); // not used
|
||||
@@ -537,387 +556,7 @@ void ChatPacketHandler::SendPrivateChatMessage(const PlayerData& sender, const P
|
||||
bitStream.Write(responseCode);
|
||||
bitStream.Write(message);
|
||||
|
||||
SystemAddress sysAddr = routeTo.sysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
|
||||
void ChatPacketHandler::HandleTeamInvite(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
|
||||
LWOOBJID playerID;
|
||||
LUWString invitedPlayer;
|
||||
|
||||
inStream.Read(playerID);
|
||||
inStream.IgnoreBytes(4);
|
||||
inStream.Read(invitedPlayer);
|
||||
|
||||
const auto& player = Game::playerContainer.GetPlayerData(playerID);
|
||||
|
||||
if (!player) return;
|
||||
|
||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||
|
||||
if (team == nullptr) {
|
||||
team = Game::playerContainer.CreateTeam(playerID);
|
||||
}
|
||||
|
||||
const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer.GetAsString());
|
||||
|
||||
if (!other) return;
|
||||
|
||||
if (Game::playerContainer.GetTeam(other.playerID) != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (team->memberIDs.size() > 3) {
|
||||
// no more teams greater than 4
|
||||
|
||||
LOG("Someone tried to invite a 5th player to a team");
|
||||
return;
|
||||
}
|
||||
|
||||
SendTeamInvite(other, player);
|
||||
|
||||
LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.GetAsString().c_str());
|
||||
}
|
||||
|
||||
void ChatPacketHandler::HandleTeamInviteResponse(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
uint32_t size = 0;
|
||||
inStream.Read(size);
|
||||
char declined = 0;
|
||||
inStream.Read(declined);
|
||||
LWOOBJID leaderID = LWOOBJID_EMPTY;
|
||||
inStream.Read(leaderID);
|
||||
|
||||
LOG("Accepted invite: %llu -> %llu (%d)", playerID, leaderID, declined);
|
||||
|
||||
if (declined) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* team = Game::playerContainer.GetTeam(leaderID);
|
||||
|
||||
if (team == nullptr) {
|
||||
LOG("Failed to find team for leader (%llu)", leaderID);
|
||||
|
||||
team = Game::playerContainer.GetTeam(playerID);
|
||||
}
|
||||
|
||||
if (team == nullptr) {
|
||||
LOG("Failed to find team for player (%llu)", playerID);
|
||||
return;
|
||||
}
|
||||
|
||||
Game::playerContainer.AddMember(team, playerID);
|
||||
}
|
||||
|
||||
void ChatPacketHandler::HandleTeamLeave(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
uint32_t size = 0;
|
||||
inStream.Read(size);
|
||||
|
||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||
|
||||
LOG("(%llu) leaving team", playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
Game::playerContainer.RemoveMember(team, playerID, false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPacketHandler::HandleTeamKick(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
LUWString kickedPlayer;
|
||||
|
||||
inStream.Read(playerID);
|
||||
inStream.IgnoreBytes(4);
|
||||
inStream.Read(kickedPlayer);
|
||||
|
||||
|
||||
LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str());
|
||||
|
||||
const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer.GetAsString());
|
||||
|
||||
LWOOBJID kickedId = LWOOBJID_EMPTY;
|
||||
|
||||
if (kicked) {
|
||||
kickedId = kicked.playerID;
|
||||
} else {
|
||||
kickedId = Game::playerContainer.GetId(kickedPlayer.string);
|
||||
}
|
||||
|
||||
if (kickedId == LWOOBJID_EMPTY) return;
|
||||
|
||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
if (team->leaderID != playerID || team->leaderID == kickedId) return;
|
||||
|
||||
Game::playerContainer.RemoveMember(team, kickedId, false, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPacketHandler::HandleTeamPromote(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
LUWString promotedPlayer;
|
||||
|
||||
inStream.Read(playerID);
|
||||
inStream.IgnoreBytes(4);
|
||||
inStream.Read(promotedPlayer);
|
||||
|
||||
LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str());
|
||||
|
||||
const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer.GetAsString());
|
||||
|
||||
if (!promoted) return;
|
||||
|
||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
if (team->leaderID != playerID) return;
|
||||
|
||||
Game::playerContainer.PromoteMember(team, promoted.playerID);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPacketHandler::HandleTeamLootOption(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
uint32_t size = 0;
|
||||
inStream.Read(size);
|
||||
|
||||
char option;
|
||||
inStream.Read(option);
|
||||
|
||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
if (team->leaderID != playerID) return;
|
||||
|
||||
team->lootFlag = option;
|
||||
|
||||
Game::playerContainer.TeamStatusUpdate(team);
|
||||
|
||||
Game::playerContainer.UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPacketHandler::HandleTeamStatusRequest(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
|
||||
auto* team = Game::playerContainer.GetTeam(playerID);
|
||||
const auto& data = Game::playerContainer.GetPlayerData(playerID);
|
||||
|
||||
if (team != nullptr && data) {
|
||||
if (team->local && data.zoneID.GetMapID() != team->zoneId.GetMapID() && data.zoneID.GetCloneID() != team->zoneId.GetCloneID()) {
|
||||
Game::playerContainer.RemoveMember(team, playerID, false, false, true, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (team->memberIDs.size() <= 1 && !team->local) {
|
||||
Game::playerContainer.DisbandTeam(team);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!team->local) {
|
||||
ChatPacketHandler::SendTeamSetLeader(data, team->leaderID);
|
||||
} else {
|
||||
ChatPacketHandler::SendTeamSetLeader(data, LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
Game::playerContainer.TeamStatusUpdate(team);
|
||||
|
||||
const auto leaderName = GeneralUtils::UTF8ToUTF16(data.playerName);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||
|
||||
if (memberId == playerID) continue;
|
||||
|
||||
const auto memberName = Game::playerContainer.GetName(memberId);
|
||||
|
||||
if (otherMember) {
|
||||
ChatPacketHandler::SendTeamSetOffWorldFlag(otherMember, data.playerID, data.zoneID);
|
||||
}
|
||||
ChatPacketHandler::SendTeamAddPlayer(data, false, team->local, false, memberId, memberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0));
|
||||
}
|
||||
|
||||
Game::playerContainer.UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::TEAM_INVITE);
|
||||
|
||||
bitStream.Write(LUWString(sender.playerName.c_str()));
|
||||
bitStream.Write(sender.playerID);
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM);
|
||||
|
||||
bitStream.Write(bLeaderIsFreeTrial);
|
||||
bitStream.Write(i64LeaderID);
|
||||
bitStream.Write(i64LeaderZoneID);
|
||||
bitStream.Write<uint32_t>(0); // BinaryBuffe, no clue what's in here
|
||||
bitStream.Write(ucLootFlag);
|
||||
bitStream.Write(ucNumOfOtherPlayers);
|
||||
bitStream.Write(ucResponseCode);
|
||||
bitStream.Write<uint32_t>(wsLeaderName.size());
|
||||
for (const auto character : wsLeaderName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE);
|
||||
|
||||
bitStream.Write(i64LeaderID);
|
||||
bitStream.Write(i64LeaderZoneID);
|
||||
bitStream.Write<uint32_t>(0); // BinaryBuffe, no clue what's in here
|
||||
bitStream.Write(ucLootFlag);
|
||||
bitStream.Write(ucNumOfOtherPlayers);
|
||||
bitStream.Write<uint32_t>(wsLeaderName.size());
|
||||
for (const auto character : wsLeaderName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_SET_LEADER);
|
||||
|
||||
bitStream.Write(i64PlayerID);
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER);
|
||||
|
||||
bitStream.Write(bIsFreeTrial);
|
||||
bitStream.Write(bLocal);
|
||||
bitStream.Write(bNoLootOnDeath);
|
||||
bitStream.Write(i64PlayerID);
|
||||
bitStream.Write<uint32_t>(wsPlayerName.size());
|
||||
for (const auto character : wsPlayerName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
bitStream.Write1();
|
||||
if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) {
|
||||
zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0);
|
||||
}
|
||||
bitStream.Write(zoneID);
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER);
|
||||
|
||||
bitStream.Write(bDisband);
|
||||
bitStream.Write(bIsKicked);
|
||||
bitStream.Write(bIsLeaving);
|
||||
bitStream.Write(bLocal);
|
||||
bitStream.Write(i64LeaderID);
|
||||
bitStream.Write(i64PlayerID);
|
||||
bitStream.Write<uint32_t>(wsPlayerName.size());
|
||||
for (const auto character : wsPlayerName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG);
|
||||
|
||||
bitStream.Write(i64PlayerID);
|
||||
if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) {
|
||||
zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0);
|
||||
}
|
||||
bitStream.Write(zoneID);
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SystemAddress sysAddr = routeTo.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
@@ -936,11 +575,11 @@ void ChatPacketHandler::SendFriendUpdate(const PlayerData& friendData, const Pla
|
||||
[bool] - is FTP*/
|
||||
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(friendData.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::UPDATE_FRIEND_NOTIFY);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::UPDATE_FRIEND_NOTIFY);
|
||||
bitStream.Write<uint8_t>(notifyType);
|
||||
|
||||
std::string playerName = playerData.playerName.c_str();
|
||||
@@ -959,7 +598,7 @@ void ChatPacketHandler::SendFriendUpdate(const PlayerData& friendData, const Pla
|
||||
bitStream.Write<uint8_t>(isBestFriend); //isBFF
|
||||
bitStream.Write<uint8_t>(0); //isFTP
|
||||
|
||||
SystemAddress sysAddr = friendData.sysAddr;
|
||||
SystemAddress sysAddr = friendData.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
@@ -973,28 +612,28 @@ void ChatPacketHandler::SendFriendRequest(const PlayerData& receiver, const Play
|
||||
}
|
||||
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::ADD_FRIEND_REQUEST);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_REQUEST);
|
||||
bitStream.Write(LUWString(sender.playerName));
|
||||
bitStream.Write<uint8_t>(0); // This is a BFF flag however this is unused in live and does not have an implementation client side.
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendFriendResponse(const PlayerData& receiver, const PlayerData& sender, eAddFriendResponseType responseCode, uint8_t isBestFriendsAlready, uint8_t isBestFriendRequest) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
// Portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::ADD_FRIEND_RESPONSE);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::ADD_FRIEND_RESPONSE);
|
||||
bitStream.Write(responseCode);
|
||||
// For all requests besides accepted, write a flag that says whether or not we are already best friends with the receiver.
|
||||
bitStream.Write<uint8_t>(responseCode != eAddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender.sysAddr != UNASSIGNED_SYSTEM_ADDRESS);
|
||||
bitStream.Write<uint8_t>(responseCode != eAddFriendResponseType::ACCEPTED ? isBestFriendsAlready : sender.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS);
|
||||
// Then write the player name
|
||||
bitStream.Write(LUWString(sender.playerName));
|
||||
// Then if this is an acceptance code, write the following extra info.
|
||||
@@ -1004,20 +643,20 @@ void ChatPacketHandler::SendFriendResponse(const PlayerData& receiver, const Pla
|
||||
bitStream.Write(isBestFriendRequest); //isBFF
|
||||
bitStream.Write<uint8_t>(0); //isFTP
|
||||
}
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPacketHandler::SendRemoveFriend(const PlayerData& receiver, std::string& personToRemove, bool isSuccessful) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::REMOVE_FRIEND_RESPONSE);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::REMOVE_FRIEND_RESPONSE);
|
||||
bitStream.Write<uint8_t>(isSuccessful); //isOnline
|
||||
bitStream.Write(LUWString(personToRemove));
|
||||
|
||||
SystemAddress sysAddr = receiver.sysAddr;
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ enum class eChatChannel : uint8_t {
|
||||
|
||||
|
||||
enum class eChatMessageResponseCode : uint8_t {
|
||||
SENT = 0,
|
||||
NOTONLINE,
|
||||
GENERALERROR,
|
||||
RECEIVEDNEWWHISPER,
|
||||
NOTFRIENDS,
|
||||
SENDERFREETRIAL,
|
||||
RECEIVERFREETRIAL,
|
||||
SENT = 0,
|
||||
NOTONLINE,
|
||||
GENERALERROR,
|
||||
RECEIVEDNEWWHISPER,
|
||||
NOTFRIENDS,
|
||||
SENDERFREETRIAL,
|
||||
RECEIVERFREETRIAL,
|
||||
};
|
||||
|
||||
namespace ChatPacketHandler {
|
||||
@@ -52,30 +52,14 @@ namespace ChatPacketHandler {
|
||||
void HandleGMLevelUpdate(Packet* packet);
|
||||
void HandleWho(Packet* packet);
|
||||
void HandleShowAll(Packet* packet);
|
||||
|
||||
void HandleChatMessage(Packet* packet);
|
||||
void HandlePrivateChatMessage(Packet* packet);
|
||||
void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode);
|
||||
|
||||
void HandleTeamInvite(Packet* packet);
|
||||
void HandleTeamInviteResponse(Packet* packet);
|
||||
void HandleTeamLeave(Packet* packet);
|
||||
void HandleTeamKick(Packet* packet);
|
||||
void HandleTeamPromote(Packet* packet);
|
||||
void HandleTeamLootOption(Packet* packet);
|
||||
void HandleTeamStatusRequest(Packet* packet);
|
||||
|
||||
void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender);
|
||||
void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName);
|
||||
void SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName);
|
||||
void SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID);
|
||||
void SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID);
|
||||
void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName);
|
||||
void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID);
|
||||
void OnAchievementNotify(RakNet::BitStream& bitstream, const SystemAddress& sysAddr);
|
||||
|
||||
//FriendData is the player we're SENDING this stuff to. Player is the friend that changed state.
|
||||
void SendFriendUpdate(const PlayerData& friendData, const PlayerData& playerData, uint8_t notifyType, uint8_t isBestFriend);
|
||||
|
||||
void SendPrivateChatMessage(const PlayerData& sender, const PlayerData& receiver, const PlayerData& routeTo, const LUWString& message, const eChatChannel channel, const eChatMessageResponseCode responseCode);
|
||||
void SendFriendRequest(const PlayerData& receiver, const PlayerData& sender);
|
||||
void SendFriendResponse(const PlayerData& receiver, const PlayerData& sender, eAddFriendResponseType responseCode, uint8_t isBestFriendsAlready = 0U, uint8_t isBestFriendRequest = 0U);
|
||||
void SendRemoveFriend(const PlayerData& receiver, std::string& personToRemove, bool isSuccessful);
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
#include "Diagnostics.h"
|
||||
#include "AssetManager.h"
|
||||
#include "BinaryPathFinder.h"
|
||||
#include "eConnectionType.h"
|
||||
#include "ServiceType.h"
|
||||
#include "PlayerContainer.h"
|
||||
#include "ChatPacketHandler.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "MessageType/World.h"
|
||||
#include "ChatIgnoreList.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "TeamContainer.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Server.h"
|
||||
@@ -28,7 +29,7 @@
|
||||
#include "RakNetDefines.h"
|
||||
#include "MessageIdentifiers.h"
|
||||
|
||||
#include "ChatWebAPI.h"
|
||||
#include "ChatWeb.h"
|
||||
|
||||
namespace Game {
|
||||
Logger* logger = nullptr;
|
||||
@@ -58,6 +59,7 @@ int main(int argc, char** argv) {
|
||||
//Create all the objects we need to run our service:
|
||||
Server::SetupLogger("ChatServer");
|
||||
if (!Game::logger) return EXIT_FAILURE;
|
||||
Game::config->LogSettings();
|
||||
|
||||
//Read our config:
|
||||
|
||||
@@ -92,17 +94,18 @@ int main(int argc, char** argv) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// seyup the chat api web server
|
||||
bool web_server_enabled = Game::config->GetValue("web_server_enabled") == "1";
|
||||
ChatWebAPI chatwebapi;
|
||||
if (web_server_enabled && !chatwebapi.Startup()){
|
||||
// if we want the web api and it fails to start, exit
|
||||
// setup the chat api web server
|
||||
const uint32_t web_server_port = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("web_server_port")).value_or(2005);
|
||||
if (Game::config->GetValue("web_server_enabled") == "1" && !Game::web.Startup("localhost", web_server_port)) {
|
||||
// if we want the web server and it fails to start, exit
|
||||
LOG("Failed to start web server, shutting down.");
|
||||
Database::Destroy("ChatServer");
|
||||
delete Game::logger;
|
||||
delete Game::config;
|
||||
return EXIT_FAILURE;
|
||||
};
|
||||
}
|
||||
|
||||
if (Game::web.IsEnabled()) ChatWeb::RegisterRoutes();
|
||||
|
||||
//Find out the master's IP:
|
||||
std::string masterIP;
|
||||
@@ -121,7 +124,7 @@ int main(int argc, char** argv) {
|
||||
const auto externalIPString = Game::config->GetValue("external_ip");
|
||||
if (!externalIPString.empty()) ourIP = externalIPString;
|
||||
|
||||
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::Chat, Game::config, &Game::lastSignal, masterPassword);
|
||||
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServiceType::CHAT, Game::config, &Game::lastSignal, masterPassword);
|
||||
|
||||
const bool dontGenerateDCF = GeneralUtils::TryParse<bool>(Game::config->GetValue("dont_generate_dcf")).value_or(false);
|
||||
Game::chatFilter = new dChatFilter(Game::assetManager->GetResPath().string() + "/chatplus_en_us", dontGenerateDCF);
|
||||
@@ -166,10 +169,8 @@ int main(int argc, char** argv) {
|
||||
packet = nullptr;
|
||||
}
|
||||
|
||||
//Check and handle web requests:
|
||||
if (web_server_enabled) {
|
||||
chatwebapi.ReceiveRequests();
|
||||
}
|
||||
// Check and handle web requests:
|
||||
if (Game::web.IsEnabled()) Game::web.ReceiveRequests();
|
||||
|
||||
//Push our log every 30s:
|
||||
if (framesSinceLastFlush >= logFlushTime) {
|
||||
@@ -197,6 +198,7 @@ int main(int argc, char** argv) {
|
||||
std::this_thread::sleep_until(t);
|
||||
}
|
||||
Game::playerContainer.Shutdown();
|
||||
TeamContainer::Shutdown();
|
||||
//Delete our objects here:
|
||||
Database::Destroy("ChatServer");
|
||||
delete Game::server;
|
||||
@@ -217,20 +219,24 @@ void HandlePacket(Packet* packet) {
|
||||
CINSTREAM;
|
||||
inStream.SetReadOffset(BYTES_TO_BITS(1));
|
||||
|
||||
eConnectionType connection;
|
||||
MessageType::Chat chatMessageID;
|
||||
|
||||
ServiceType connection;
|
||||
inStream.Read(connection);
|
||||
if (connection != eConnectionType::CHAT) return;
|
||||
if (connection != ServiceType::CHAT) return;
|
||||
|
||||
MessageType::Chat chatMessageID;
|
||||
inStream.Read(chatMessageID);
|
||||
|
||||
// Our packing byte wasnt there? Probably a false packet
|
||||
if (inStream.GetNumberOfUnreadBits() < 8) return;
|
||||
inStream.IgnoreBytes(1);
|
||||
|
||||
switch (chatMessageID) {
|
||||
case MessageType::Chat::GM_MUTE:
|
||||
Game::playerContainer.MuteUpdate(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::CREATE_TEAM:
|
||||
Game::playerContainer.CreateTeamServer(packet);
|
||||
TeamContainer::CreateTeamServer(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::GET_FRIENDS_LIST:
|
||||
@@ -250,7 +256,7 @@ void HandlePacket(Packet* packet) {
|
||||
break;
|
||||
|
||||
case MessageType::Chat::TEAM_GET_STATUS:
|
||||
ChatPacketHandler::HandleTeamStatusRequest(packet);
|
||||
TeamContainer::HandleTeamStatusRequest(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::ADD_FRIEND_REQUEST:
|
||||
@@ -280,27 +286,27 @@ void HandlePacket(Packet* packet) {
|
||||
break;
|
||||
|
||||
case MessageType::Chat::TEAM_INVITE:
|
||||
ChatPacketHandler::HandleTeamInvite(packet);
|
||||
TeamContainer::HandleTeamInvite(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::TEAM_INVITE_RESPONSE:
|
||||
ChatPacketHandler::HandleTeamInviteResponse(packet);
|
||||
TeamContainer::HandleTeamInviteResponse(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::TEAM_LEAVE:
|
||||
ChatPacketHandler::HandleTeamLeave(packet);
|
||||
TeamContainer::HandleTeamLeave(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::TEAM_SET_LEADER:
|
||||
ChatPacketHandler::HandleTeamPromote(packet);
|
||||
TeamContainer::HandleTeamPromote(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::TEAM_KICK:
|
||||
ChatPacketHandler::HandleTeamKick(packet);
|
||||
TeamContainer::HandleTeamKick(packet);
|
||||
break;
|
||||
|
||||
case MessageType::Chat::TEAM_SET_LOOT:
|
||||
ChatPacketHandler::HandleTeamLootOption(packet);
|
||||
TeamContainer::HandleTeamLootOption(packet);
|
||||
break;
|
||||
case MessageType::Chat::GMLEVEL_UPDATE:
|
||||
ChatPacketHandler::HandleGMLevelUpdate(packet);
|
||||
@@ -322,6 +328,9 @@ void HandlePacket(Packet* packet) {
|
||||
case MessageType::Chat::SHOW_ALL:
|
||||
ChatPacketHandler::HandleShowAll(packet);
|
||||
break;
|
||||
case MessageType::Chat::ACHIEVEMENT_NOTIFY:
|
||||
ChatPacketHandler::OnAchievementNotify(inStream, packet->systemAddress);
|
||||
break;
|
||||
case MessageType::Chat::USER_CHANNEL_CHAT_MESSAGE:
|
||||
case MessageType::Chat::WORLD_DISCONNECT_REQUEST:
|
||||
case MessageType::Chat::WORLD_PROXIMITY_RESPONSE:
|
||||
@@ -357,7 +366,6 @@ void HandlePacket(Packet* packet) {
|
||||
case MessageType::Chat::UGCMANIFEST_REPORT_DONE_BLUEPRINT:
|
||||
case MessageType::Chat::UGCC_REQUEST:
|
||||
case MessageType::Chat::WORLD_PLAYERS_PET_MODERATED_ACKNOWLEDGE:
|
||||
case MessageType::Chat::ACHIEVEMENT_NOTIFY:
|
||||
case MessageType::Chat::GM_CLOSE_PRIVATE_CHAT_WINDOW:
|
||||
case MessageType::Chat::PLAYER_READY:
|
||||
case MessageType::Chat::GET_DONATION_TOTAL:
|
||||
|
||||
138
dChatServer/ChatWeb.cpp
Normal file
138
dChatServer/ChatWeb.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "ChatWeb.h"
|
||||
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "json.hpp"
|
||||
#include "dCommonVars.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "dServer.h"
|
||||
#include "dConfig.h"
|
||||
#include "PlayerContainer.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "ChatPackets.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "Database.h"
|
||||
#include "ChatJSONUtils.h"
|
||||
#include "JSONUtils.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "dChatFilter.h"
|
||||
#include "TeamContainer.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = Game::playerContainer;
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
|
||||
reply.contentType = ContentType::JSON;
|
||||
}
|
||||
|
||||
void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = TeamContainer::GetTeamContainer();
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
|
||||
reply.contentType = ContentType::JSON;
|
||||
}
|
||||
|
||||
void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
|
||||
auto data = GeneralUtils::TryParse<json>(body);
|
||||
if (!data) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
reply.contentType = ContentType::JSON;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& good_data = data.value();
|
||||
auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" });
|
||||
if (!check.empty()) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = check;
|
||||
reply.contentType = ContentType::JSON;
|
||||
} else {
|
||||
|
||||
ChatPackets::Announcement announcement;
|
||||
announcement.title = good_data["title"];
|
||||
announcement.message = good_data["message"];
|
||||
announcement.Broadcast();
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = "{\"status\":\"Announcement Sent\"}";
|
||||
reply.contentType = ContentType::JSON;
|
||||
}
|
||||
}
|
||||
|
||||
void HandleWSChat(mg_connection* connection, json data) {
|
||||
auto check = JSONUtils::CheckRequiredData(data, { "user", "message", "gmlevel", "zone" });
|
||||
if (!check.empty()) {
|
||||
LOG_DEBUG("Received invalid websocket message: %s", check.c_str());
|
||||
} else {
|
||||
const auto user = data["user"].get<std::string>();
|
||||
const auto message = data["message"].get<std::string>();
|
||||
const auto gmlevel = GeneralUtils::TryParse<eGameMasterLevel>(data["gmlevel"].get<std::string>()).value_or(eGameMasterLevel::CIVILIAN);
|
||||
const auto zone = data["zone"].get<uint32_t>();
|
||||
|
||||
const auto filter_check = Game::chatFilter->IsSentenceOkay(message, gmlevel);
|
||||
if (!filter_check.empty()) {
|
||||
LOG_DEBUG("Chat message \"%s\" from %s was not allowed", message.c_str(), user.c_str());
|
||||
data["error"] = "Chat message blocked by filter";
|
||||
data["filtered"] = json::array();
|
||||
for (const auto& [start, len] : filter_check) {
|
||||
data["filtered"].push_back(message.substr(start, len));
|
||||
}
|
||||
mg_ws_send(connection, data.dump().c_str(), data.dump().size(), WEBSOCKET_OP_TEXT);
|
||||
return;
|
||||
}
|
||||
LOG("%s: %s", user.c_str(), message.c_str());
|
||||
|
||||
// TODO: Implement chat message handling from websocket message
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
namespace ChatWeb {
|
||||
void RegisterRoutes() {
|
||||
|
||||
// REST API v1 routes
|
||||
|
||||
std::string v1_route = "/api/v1/";
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandleHTTPPlayersRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "teams",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandleHTTPTeamsRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "announce",
|
||||
.method = eHTTPMethod::POST,
|
||||
.handle = HandleHTTPAnnounceRequest
|
||||
});
|
||||
|
||||
// WebSocket Events Handlers
|
||||
|
||||
// Game::web.RegisterWSEvent({
|
||||
// .name = "chat",
|
||||
// .handle = HandleWSChat
|
||||
// });
|
||||
|
||||
// WebSocket subscriptions
|
||||
|
||||
Game::web.RegisterWSSubscription("player");
|
||||
}
|
||||
|
||||
void SendWSPlayerUpdate(const PlayerData& player, eActivityType activityType) {
|
||||
json data;
|
||||
data["player_data"] = player;
|
||||
data["update_type"] = magic_enum::enum_name(activityType);
|
||||
Game::web.SendWSMessage("player", data);
|
||||
}
|
||||
}
|
||||
|
||||
19
dChatServer/ChatWeb.h
Normal file
19
dChatServer/ChatWeb.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef __CHATWEB_H__
|
||||
#define __CHATWEB_H__
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
#include "Web.h"
|
||||
#include "PlayerContainer.h"
|
||||
#include "IActivityLog.h"
|
||||
#include "ChatPacketHandler.h"
|
||||
|
||||
namespace ChatWeb {
|
||||
void RegisterRoutes();
|
||||
void SendWSPlayerUpdate(const PlayerData& player, eActivityType activityType);
|
||||
};
|
||||
|
||||
|
||||
#endif // __CHATWEB_H__
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
#include "ChatWebAPI.h"
|
||||
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "json.hpp"
|
||||
#include "dCommonVars.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "dServer.h"
|
||||
#include "dConfig.h"
|
||||
#include "PlayerContainer.h"
|
||||
#include "JSONUtils.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "ChatPackets.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "Database.h"
|
||||
|
||||
#ifdef DARKFLAME_PLATFORM_WIN32
|
||||
#pragma push_macro("DELETE")
|
||||
#undef DELETE
|
||||
#endif
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
typedef struct mg_connection mg_connection;
|
||||
typedef struct mg_http_message mg_http_message;
|
||||
|
||||
namespace {
|
||||
const char* json_content_type = "Content-Type: application/json\r\n";
|
||||
std::map<std::pair<eHTTPMethod, std::string>, WebAPIHTTPRoute> Routes {};
|
||||
}
|
||||
|
||||
bool ValidateAuthentication(const mg_http_message* http_msg) {
|
||||
// TO DO: This is just a placeholder for now
|
||||
// use tokens or something at a later point if we want to implement authentication
|
||||
// bit using the listen bind address to limit external access is good enough to start with
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ValidateJSON(std::optional<json> data, HTTPReply& reply) {
|
||||
if (!data) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HandlePlayersRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = Game::playerContainer;
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleTeamsRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = Game::playerContainer.GetTeamContainer();
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleAnnounceRequest(HTTPReply& reply, std::string body) {
|
||||
auto data = GeneralUtils::TryParse<json>(body);
|
||||
if (!ValidateJSON(data, reply)) return;
|
||||
|
||||
const auto& good_data = data.value();
|
||||
auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" });
|
||||
if (!check.empty()) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = check;
|
||||
} else {
|
||||
|
||||
ChatPackets::Announcement announcement;
|
||||
announcement.title = good_data["title"];
|
||||
announcement.message = good_data["message"];
|
||||
announcement.Send();
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = "{\"status\":\"Announcement Sent\"}";
|
||||
}
|
||||
}
|
||||
|
||||
void HandleInvalidRoute(HTTPReply& reply) {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "{\"error\":\"Invalid Route\"}";
|
||||
}
|
||||
|
||||
void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) {
|
||||
HTTPReply reply;
|
||||
|
||||
if (!http_msg) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid Request\"}";
|
||||
} else if (ValidateAuthentication(http_msg)) {
|
||||
|
||||
// convert method from cstring to std string
|
||||
std::string method_string(http_msg->method.buf, http_msg->method.len);
|
||||
// get mehtod from mg to enum
|
||||
const eHTTPMethod method = magic_enum::enum_cast<eHTTPMethod>(method_string).value_or(eHTTPMethod::INVALID);
|
||||
|
||||
// convert uri from cstring to std string
|
||||
std::string uri(http_msg->uri.buf, http_msg->uri.len);
|
||||
std::transform(uri.begin(), uri.end(), uri.begin(), ::tolower);
|
||||
|
||||
// convert body from cstring to std string
|
||||
std::string body(http_msg->body.buf, http_msg->body.len);
|
||||
|
||||
|
||||
const auto routeItr = Routes.find({method, uri});
|
||||
|
||||
if (routeItr != Routes.end()) {
|
||||
const auto& [_, route] = *routeItr;
|
||||
route.handle(reply, body);
|
||||
} else HandleInvalidRoute(reply);
|
||||
} else {
|
||||
reply.status = eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.message = "{\"error\":\"Unauthorized\"}";
|
||||
}
|
||||
mg_http_reply(connection, static_cast<int>(reply.status), json_content_type, reply.message.c_str());
|
||||
}
|
||||
|
||||
|
||||
void HandleRequests(mg_connection* connection, int request, void* request_data) {
|
||||
switch (request) {
|
||||
case MG_EV_HTTP_MSG:
|
||||
HandleHTTPMessage(connection, static_cast<mg_http_message*>(request_data));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatWebAPI::RegisterHTTPRoutes(WebAPIHTTPRoute route) {
|
||||
auto [_, success] = Routes.try_emplace({ route.method, route.path }, route);
|
||||
if (!success) {
|
||||
LOG_DEBUG("Failed to register route %s", route.path.c_str());
|
||||
} else {
|
||||
LOG_DEBUG("Registered route %s", route.path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ChatWebAPI::ChatWebAPI() {
|
||||
mg_log_set(MG_LL_NONE);
|
||||
mg_mgr_init(&mgr); // Initialize event manager
|
||||
}
|
||||
|
||||
ChatWebAPI::~ChatWebAPI() {
|
||||
mg_mgr_free(&mgr);
|
||||
}
|
||||
|
||||
bool ChatWebAPI::Startup() {
|
||||
// Make listen address
|
||||
std::string listen_ip = Game::config->GetValue("web_server_listen_ip");
|
||||
if (listen_ip == "localhost") listen_ip = "127.0.0.1";
|
||||
|
||||
const std::string& listen_port = Game::config->GetValue("web_server_listen_port");
|
||||
const std::string& listen_address = "http://" + listen_ip + ":" + listen_port;
|
||||
LOG("Starting web server on %s", listen_address.c_str());
|
||||
|
||||
// Create HTTP listener
|
||||
if (!mg_http_listen(&mgr, listen_address.c_str(), HandleRequests, NULL)) {
|
||||
LOG("Failed to create web server listener on %s", listen_port.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Register routes
|
||||
|
||||
// API v1 routes
|
||||
std::string v1_route = "/api/v1/";
|
||||
RegisterHTTPRoutes({
|
||||
.path = v1_route + "players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandlePlayersRequest
|
||||
});
|
||||
|
||||
RegisterHTTPRoutes({
|
||||
.path = v1_route + "teams",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandleTeamsRequest
|
||||
});
|
||||
|
||||
RegisterHTTPRoutes({
|
||||
.path = v1_route + "announce",
|
||||
.method = eHTTPMethod::POST,
|
||||
.handle = HandleAnnounceRequest
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatWebAPI::ReceiveRequests() {
|
||||
mg_mgr_poll(&mgr, 15);
|
||||
}
|
||||
|
||||
#ifdef DARKFLAME_PLATFORM_WIN32
|
||||
#pragma pop_macro("DELETE")
|
||||
#endif
|
||||
@@ -1,36 +0,0 @@
|
||||
#ifndef __CHATWEBAPI_H__
|
||||
#define __CHATWEBAPI_H__
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
#include "mongoose.h"
|
||||
#include "eHTTPStatusCode.h"
|
||||
|
||||
enum class eHTTPMethod;
|
||||
|
||||
typedef struct mg_mgr mg_mgr;
|
||||
|
||||
struct HTTPReply {
|
||||
eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND;
|
||||
std::string message = "{\"error\":\"Not Found\"}";
|
||||
};
|
||||
|
||||
struct WebAPIHTTPRoute {
|
||||
std::string path;
|
||||
eHTTPMethod method;
|
||||
std::function<void(HTTPReply&, const std::string&)> handle;
|
||||
};
|
||||
|
||||
class ChatWebAPI {
|
||||
public:
|
||||
ChatWebAPI();
|
||||
~ChatWebAPI();
|
||||
void ReceiveRequests();
|
||||
void RegisterHTTPRoutes(WebAPIHTTPRoute route);
|
||||
bool Startup();
|
||||
private:
|
||||
mg_mgr mgr;
|
||||
|
||||
};
|
||||
|
||||
#endif // __CHATWEBAPI_H__
|
||||
@@ -1,17 +0,0 @@
|
||||
#ifndef __JSONUTILS_H__
|
||||
#define __JSONUTILS_H__
|
||||
|
||||
#include "json_fwd.hpp"
|
||||
#include "PlayerContainer.h"
|
||||
|
||||
void to_json(nlohmann::json& data, const PlayerData& playerData);
|
||||
void to_json(nlohmann::json& data, const PlayerContainer& playerContainer);
|
||||
void to_json(nlohmann::json& data, const TeamContainer& teamData);
|
||||
void to_json(nlohmann::json& data, const TeamData& teamData);
|
||||
|
||||
namespace JSONUtils {
|
||||
// check required data for reqeust
|
||||
std::string CheckRequiredData(const nlohmann::json& data, const std::vector<std::string>& requiredData);
|
||||
}
|
||||
|
||||
#endif // __JSONUTILS_H__
|
||||
@@ -8,10 +8,12 @@
|
||||
#include "GeneralUtils.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "Database.h"
|
||||
#include "eConnectionType.h"
|
||||
#include "ServiceType.h"
|
||||
#include "ChatPackets.h"
|
||||
#include "dConfig.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "ChatWeb.h"
|
||||
#include "TeamContainer.h"
|
||||
|
||||
void PlayerContainer::Initialize() {
|
||||
m_MaxNumberOfBestFriends =
|
||||
@@ -52,14 +54,15 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
|
||||
if (!inStream.Read(data.zoneID)) return;
|
||||
if (!inStream.Read(data.muteExpire)) return;
|
||||
if (!inStream.Read(data.gmLevel)) return;
|
||||
data.sysAddr = packet->systemAddress;
|
||||
data.worldServerSysAddr = packet->systemAddress;
|
||||
|
||||
m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName);
|
||||
m_PlayerCount++;
|
||||
|
||||
LOG("Added user: %s (%llu), zone: %i", data.playerName.c_str(), data.playerID, data.zoneID.GetMapID());
|
||||
ChatWeb::SendWSPlayerUpdate(data, isLogin ? eActivityType::PlayerLoggedIn : eActivityType::PlayerChangedZone);
|
||||
|
||||
Database::Get()->UpdateActivityLog(data.playerID, eActivityType::PlayerLoggedIn, data.zoneID.GetMapID());
|
||||
Database::Get()->UpdateActivityLog(data.playerID, isLogin ? eActivityType::PlayerLoggedIn : eActivityType::PlayerChangedZone, data.zoneID.GetMapID());
|
||||
m_PlayersToRemove.erase(playerId);
|
||||
}
|
||||
|
||||
@@ -99,7 +102,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
|
||||
if (fd) ChatPacketHandler::SendFriendUpdate(fd, player, 0, fr.isBestFriend);
|
||||
}
|
||||
|
||||
auto* team = GetTeam(playerID);
|
||||
auto* team = TeamContainer::GetTeam(playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
const auto memberName = GeneralUtils::UTF8ToUTF16(player.playerName);
|
||||
@@ -109,10 +112,12 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
ChatPacketHandler::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 });
|
||||
TeamContainer::SendTeamSetOffWorldFlag(otherMember, playerID, { 0, 0, 0 });
|
||||
}
|
||||
}
|
||||
|
||||
ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut);
|
||||
|
||||
m_PlayerCount--;
|
||||
LOG("Removed user: %llu", playerID);
|
||||
m_Players.erase(playerID);
|
||||
@@ -140,43 +145,9 @@ void PlayerContainer::MuteUpdate(Packet* packet) {
|
||||
BroadcastMuteUpdate(playerID, expire);
|
||||
}
|
||||
|
||||
void PlayerContainer::CreateTeamServer(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID;
|
||||
inStream.Read(playerID);
|
||||
size_t membersSize = 0;
|
||||
inStream.Read(membersSize);
|
||||
|
||||
if (membersSize >= 4) {
|
||||
LOG("Tried to create a team with more than 4 players");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<LWOOBJID> members;
|
||||
|
||||
members.reserve(membersSize);
|
||||
|
||||
for (size_t i = 0; i < membersSize; i++) {
|
||||
LWOOBJID member;
|
||||
inStream.Read(member);
|
||||
members.push_back(member);
|
||||
}
|
||||
|
||||
LWOZONEID zoneId;
|
||||
|
||||
inStream.Read(zoneId);
|
||||
|
||||
auto* team = CreateLocalTeam(members);
|
||||
|
||||
if (team != nullptr) {
|
||||
team->zoneId = zoneId;
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_MUTE);
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_MUTE);
|
||||
|
||||
bitStream.Write(player);
|
||||
bitStream.Write(time);
|
||||
@@ -184,221 +155,6 @@ void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) {
|
||||
Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true);
|
||||
}
|
||||
|
||||
TeamData* PlayerContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
|
||||
if (members.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TeamData* newTeam = nullptr;
|
||||
|
||||
for (const auto member : members) {
|
||||
auto* team = GetTeam(member);
|
||||
|
||||
if (team != nullptr) {
|
||||
RemoveMember(team, member, false, false, true);
|
||||
}
|
||||
|
||||
if (newTeam == nullptr) {
|
||||
newTeam = CreateTeam(member, true);
|
||||
} else {
|
||||
AddMember(newTeam, member);
|
||||
}
|
||||
}
|
||||
|
||||
newTeam->lootFlag = 1;
|
||||
|
||||
TeamStatusUpdate(newTeam);
|
||||
|
||||
return newTeam;
|
||||
}
|
||||
|
||||
TeamData* PlayerContainer::CreateTeam(LWOOBJID leader, bool local) {
|
||||
auto* team = new TeamData();
|
||||
|
||||
team->teamID = ++m_TeamIDCounter;
|
||||
team->leaderID = leader;
|
||||
team->local = local;
|
||||
|
||||
GetTeamsMut().push_back(team);
|
||||
|
||||
AddMember(team, leader);
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
TeamData* PlayerContainer::GetTeam(LWOOBJID playerID) {
|
||||
for (auto* team : GetTeams()) {
|
||||
if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue;
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PlayerContainer::AddMember(TeamData* team, LWOOBJID playerID) {
|
||||
if (team->memberIDs.size() >= 4) {
|
||||
LOG("Tried to add player to team that already had 4 players");
|
||||
const auto& player = GetPlayerData(playerID);
|
||||
if (!player) return;
|
||||
ChatPackets::SendSystemMessage(player.sysAddr, u"The teams is full! You have not been added to a team!");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID);
|
||||
|
||||
if (index != team->memberIDs.end()) return;
|
||||
|
||||
team->memberIDs.push_back(playerID);
|
||||
|
||||
const auto& leader = GetPlayerData(team->leaderID);
|
||||
const auto& member = GetPlayerData(playerID);
|
||||
|
||||
if (!leader || !member) return;
|
||||
|
||||
const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName);
|
||||
const auto memberName = GeneralUtils::UTF8ToUTF16(member.playerName);
|
||||
|
||||
ChatPacketHandler::SendTeamInviteConfirm(member, false, leader.playerID, leader.zoneID, team->lootFlag, 0, 0, leaderName);
|
||||
|
||||
if (!team->local) {
|
||||
ChatPacketHandler::SendTeamSetLeader(member, leader.playerID);
|
||||
} else {
|
||||
ChatPacketHandler::SendTeamSetLeader(member, LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = GetPlayerData(memberId);
|
||||
|
||||
if (otherMember == member) continue;
|
||||
|
||||
const auto otherMemberName = GetName(memberId);
|
||||
|
||||
ChatPacketHandler::SendTeamAddPlayer(member, false, team->local, false, memberId, otherMemberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0));
|
||||
|
||||
if (otherMember) {
|
||||
ChatPacketHandler::SendTeamAddPlayer(otherMember, false, team->local, false, member.playerID, memberName, member.zoneID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerContainer::RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent) {
|
||||
const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID);
|
||||
|
||||
if (index == team->memberIDs.end()) return;
|
||||
|
||||
const auto& member = GetPlayerData(playerID);
|
||||
|
||||
if (member && !silent) {
|
||||
ChatPacketHandler::SendTeamSetLeader(member, LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
const auto memberName = GetName(playerID);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
if (silent && memberId == playerID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& otherMember = GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
ChatPacketHandler::SendTeamRemovePlayer(otherMember, disband, kicked, leaving, false, team->leaderID, playerID, memberName);
|
||||
}
|
||||
|
||||
team->memberIDs.erase(index);
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
|
||||
if (team->memberIDs.size() <= 1) {
|
||||
DisbandTeam(team);
|
||||
} else {
|
||||
if (playerID == team->leaderID) {
|
||||
PromoteMember(team, team->memberIDs[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) {
|
||||
team->leaderID = newLeader;
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
ChatPacketHandler::SendTeamSetLeader(otherMember, newLeader);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerContainer::DisbandTeam(TeamData* team) {
|
||||
const auto index = std::find(GetTeams().begin(), GetTeams().end(), team);
|
||||
|
||||
if (index == GetTeams().end()) return;
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
const auto memberName = GeneralUtils::UTF8ToUTF16(otherMember.playerName);
|
||||
|
||||
ChatPacketHandler::SendTeamSetLeader(otherMember, LWOOBJID_EMPTY);
|
||||
ChatPacketHandler::SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, otherMember.playerID, memberName);
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, true);
|
||||
|
||||
GetTeamsMut().erase(index);
|
||||
|
||||
delete team;
|
||||
}
|
||||
|
||||
void PlayerContainer::TeamStatusUpdate(TeamData* team) {
|
||||
const auto index = std::find(GetTeams().begin(), GetTeams().end(), team);
|
||||
|
||||
if (index == GetTeams().end()) return;
|
||||
|
||||
const auto& leader = GetPlayerData(team->leaderID);
|
||||
|
||||
if (!leader) return;
|
||||
|
||||
const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
if (!team->local) {
|
||||
ChatPacketHandler::SendTeamStatus(otherMember, team->leaderID, leader.zoneID, team->lootFlag, 0, leaderName);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
|
||||
void PlayerContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::TEAM_GET_STATUS);
|
||||
|
||||
bitStream.Write(team->teamID);
|
||||
bitStream.Write(deleteTeam);
|
||||
|
||||
if (!deleteTeam) {
|
||||
bitStream.Write(team->lootFlag);
|
||||
bitStream.Write<char>(team->memberIDs.size());
|
||||
for (const auto memberID : team->memberIDs) {
|
||||
bitStream.Write(memberID);
|
||||
}
|
||||
}
|
||||
|
||||
Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true);
|
||||
}
|
||||
|
||||
std::u16string PlayerContainer::GetName(LWOOBJID playerID) {
|
||||
const auto iter = m_Names.find(playerID);
|
||||
|
||||
@@ -447,5 +203,4 @@ void PlayerContainer::Shutdown() {
|
||||
Database::Get()->UpdateActivityLog(id, eActivityType::PlayerLoggedOut, playerData.zoneID.GetMapID());
|
||||
m_Players.erase(m_Players.begin());
|
||||
}
|
||||
for (auto* team : GetTeams()) if (team) delete team;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ enum class eGameMasterLevel : uint8_t;
|
||||
|
||||
struct TeamData;
|
||||
|
||||
struct TeamContainer {
|
||||
std::vector<TeamData*> mTeams;
|
||||
};
|
||||
|
||||
struct IgnoreData {
|
||||
IgnoreData(const std::string& name, const LWOOBJID& id) : playerName{ name }, playerId{ id } {}
|
||||
inline bool operator==(const std::string& other) const noexcept {
|
||||
@@ -42,7 +38,7 @@ struct PlayerData {
|
||||
return muteExpire == 1 || muteExpire > time(NULL);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr{};
|
||||
SystemAddress worldServerSysAddr{};
|
||||
LWOZONEID zoneID{};
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
time_t muteExpire = 0;
|
||||
@@ -73,7 +69,6 @@ public:
|
||||
void ScheduleRemovePlayer(Packet* packet);
|
||||
void RemovePlayer(const LWOOBJID playerID);
|
||||
void MuteUpdate(Packet* packet);
|
||||
void CreateTeamServer(Packet* packet);
|
||||
void BroadcastMuteUpdate(LWOOBJID player, time_t time);
|
||||
void Shutdown();
|
||||
|
||||
@@ -81,34 +76,19 @@ public:
|
||||
const PlayerData& GetPlayerData(const std::string& playerName);
|
||||
PlayerData& GetPlayerDataMutable(const LWOOBJID& playerID);
|
||||
PlayerData& GetPlayerDataMutable(const std::string& playerName);
|
||||
std::u16string GetName(LWOOBJID playerID);
|
||||
LWOOBJID GetId(const std::u16string& playerName);
|
||||
void Update(const float deltaTime);
|
||||
|
||||
uint32_t GetPlayerCount() { return m_PlayerCount; };
|
||||
uint32_t GetSimCount() { return m_SimCount; };
|
||||
const std::map<LWOOBJID, PlayerData>& GetAllPlayers() const { return m_Players; };
|
||||
|
||||
TeamData* CreateLocalTeam(std::vector<LWOOBJID> members);
|
||||
TeamData* CreateTeam(LWOOBJID leader, bool local = false);
|
||||
TeamData* GetTeam(LWOOBJID playerID);
|
||||
void AddMember(TeamData* team, LWOOBJID playerID);
|
||||
void RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent = false);
|
||||
void PromoteMember(TeamData* team, LWOOBJID newLeader);
|
||||
void DisbandTeam(TeamData* team);
|
||||
void TeamStatusUpdate(TeamData* team);
|
||||
void UpdateTeamsOnWorld(TeamData* team, bool deleteTeam);
|
||||
std::u16string GetName(LWOOBJID playerID);
|
||||
LWOOBJID GetId(const std::u16string& playerName);
|
||||
uint32_t GetMaxNumberOfBestFriends() { return m_MaxNumberOfBestFriends; }
|
||||
uint32_t GetMaxNumberOfFriends() { return m_MaxNumberOfFriends; }
|
||||
const TeamContainer& GetTeamContainer() { return m_TeamContainer; }
|
||||
std::vector<TeamData*>& GetTeamsMut() { return m_TeamContainer.mTeams; };
|
||||
const std::vector<TeamData*>& GetTeams() { return GetTeamsMut(); };
|
||||
|
||||
void Update(const float deltaTime);
|
||||
bool PlayerBeingRemoved(const LWOOBJID playerID) { return m_PlayersToRemove.contains(playerID); }
|
||||
|
||||
private:
|
||||
LWOOBJID m_TeamIDCounter = 0;
|
||||
std::map<LWOOBJID, PlayerData> m_Players;
|
||||
TeamContainer m_TeamContainer{};
|
||||
std::unordered_map<LWOOBJID, std::u16string> m_Names;
|
||||
std::map<LWOOBJID, float> m_PlayersToRemove;
|
||||
uint32_t m_MaxNumberOfBestFriends = 5;
|
||||
|
||||
669
dChatServer/TeamContainer.cpp
Normal file
669
dChatServer/TeamContainer.cpp
Normal file
@@ -0,0 +1,669 @@
|
||||
#include "TeamContainer.h"
|
||||
|
||||
#include "ChatPackets.h"
|
||||
|
||||
#include "MessageType/Chat.h"
|
||||
#include "MessageType/Game.h"
|
||||
|
||||
#include "ChatPacketHandler.h"
|
||||
#include "PlayerContainer.h"
|
||||
|
||||
namespace {
|
||||
TeamContainer::Data g_TeamContainer{};
|
||||
LWOOBJID g_TeamIDCounter = 0;
|
||||
}
|
||||
|
||||
const TeamContainer::Data& TeamContainer::GetTeamContainer() {
|
||||
return g_TeamContainer;
|
||||
}
|
||||
|
||||
std::vector<TeamData*>& TeamContainer::GetTeamsMut() {
|
||||
return g_TeamContainer.mTeams;
|
||||
}
|
||||
|
||||
const std::vector<TeamData*>& TeamContainer::GetTeams() {
|
||||
return GetTeamsMut();
|
||||
}
|
||||
|
||||
void TeamContainer::Shutdown() {
|
||||
for (auto* team : g_TeamContainer.mTeams) if (team) delete team;
|
||||
}
|
||||
|
||||
void TeamContainer::HandleTeamInvite(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
|
||||
LWOOBJID playerID;
|
||||
LUWString invitedPlayer;
|
||||
|
||||
inStream.Read(playerID);
|
||||
inStream.IgnoreBytes(4);
|
||||
inStream.Read(invitedPlayer);
|
||||
|
||||
const auto& player = Game::playerContainer.GetPlayerData(playerID);
|
||||
|
||||
if (!player) return;
|
||||
|
||||
auto* team = GetTeam(playerID);
|
||||
|
||||
if (team == nullptr) {
|
||||
team = CreateTeam(playerID);
|
||||
}
|
||||
|
||||
const auto& other = Game::playerContainer.GetPlayerData(invitedPlayer.GetAsString());
|
||||
|
||||
if (!other) return;
|
||||
|
||||
if (GetTeam(other.playerID) != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (team->memberIDs.size() > 3) {
|
||||
// no more teams greater than 4
|
||||
|
||||
LOG("Someone tried to invite a 5th player to a team");
|
||||
return;
|
||||
}
|
||||
|
||||
SendTeamInvite(other, player);
|
||||
|
||||
LOG("Got team invite: %llu -> %s", playerID, invitedPlayer.GetAsString().c_str());
|
||||
|
||||
bool failed = false;
|
||||
for (const auto& ignore : other.ignoredPlayers) {
|
||||
if (ignore.playerId == player.playerID) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ChatPackets::TeamInviteInitialResponse response{};
|
||||
response.inviteFailedToSend = failed;
|
||||
response.playerName = invitedPlayer.string;
|
||||
ChatPackets::SendRoutedMsg(response, playerID, player.worldServerSysAddr);
|
||||
}
|
||||
|
||||
void TeamContainer::HandleTeamInviteResponse(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
uint32_t size = 0;
|
||||
inStream.Read(size);
|
||||
char declined = 0;
|
||||
inStream.Read(declined);
|
||||
LWOOBJID leaderID = LWOOBJID_EMPTY;
|
||||
inStream.Read(leaderID);
|
||||
|
||||
LOG("Invite reponse received: %llu -> %llu (%d)", playerID, leaderID, declined);
|
||||
|
||||
if (declined) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* team = GetTeam(leaderID);
|
||||
|
||||
if (team == nullptr) {
|
||||
LOG("Failed to find team for leader (%llu)", leaderID);
|
||||
|
||||
team = GetTeam(playerID);
|
||||
}
|
||||
|
||||
if (team == nullptr) {
|
||||
LOG("Failed to find team for player (%llu)", playerID);
|
||||
return;
|
||||
}
|
||||
|
||||
AddMember(team, playerID);
|
||||
}
|
||||
|
||||
void TeamContainer::HandleTeamLeave(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
uint32_t size = 0;
|
||||
inStream.Read(size);
|
||||
|
||||
auto* team = GetTeam(playerID);
|
||||
|
||||
LOG("(%llu) leaving team", playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
RemoveMember(team, playerID, false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::HandleTeamKick(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
LUWString kickedPlayer;
|
||||
|
||||
inStream.Read(playerID);
|
||||
inStream.IgnoreBytes(4);
|
||||
inStream.Read(kickedPlayer);
|
||||
|
||||
|
||||
LOG("(%llu) kicking (%s) from team", playerID, kickedPlayer.GetAsString().c_str());
|
||||
|
||||
const auto& kicked = Game::playerContainer.GetPlayerData(kickedPlayer.GetAsString());
|
||||
|
||||
LWOOBJID kickedId = LWOOBJID_EMPTY;
|
||||
|
||||
if (kicked) {
|
||||
kickedId = kicked.playerID;
|
||||
} else {
|
||||
kickedId = Game::playerContainer.GetId(kickedPlayer.string);
|
||||
}
|
||||
|
||||
if (kickedId == LWOOBJID_EMPTY) return;
|
||||
|
||||
auto* team = GetTeam(playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
if (team->leaderID != playerID || team->leaderID == kickedId) return;
|
||||
|
||||
RemoveMember(team, kickedId, false, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::HandleTeamPromote(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
LUWString promotedPlayer;
|
||||
|
||||
inStream.Read(playerID);
|
||||
inStream.IgnoreBytes(4);
|
||||
inStream.Read(promotedPlayer);
|
||||
|
||||
LOG("(%llu) promoting (%s) to team leader", playerID, promotedPlayer.GetAsString().c_str());
|
||||
|
||||
const auto& promoted = Game::playerContainer.GetPlayerData(promotedPlayer.GetAsString());
|
||||
|
||||
if (!promoted) return;
|
||||
|
||||
auto* team = GetTeam(playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
if (team->leaderID != playerID) return;
|
||||
|
||||
PromoteMember(team, promoted.playerID);
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::HandleTeamLootOption(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
uint32_t size = 0;
|
||||
inStream.Read(size);
|
||||
|
||||
char option;
|
||||
inStream.Read(option);
|
||||
|
||||
auto* team = GetTeam(playerID);
|
||||
|
||||
if (team != nullptr) {
|
||||
if (team->leaderID != playerID) return;
|
||||
|
||||
team->lootFlag = option;
|
||||
|
||||
TeamStatusUpdate(team);
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::HandleTeamStatusRequest(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID = LWOOBJID_EMPTY;
|
||||
inStream.Read(playerID);
|
||||
|
||||
auto* team = GetTeam(playerID);
|
||||
const auto& data = Game::playerContainer.GetPlayerData(playerID);
|
||||
|
||||
if (team != nullptr && data) {
|
||||
LOG_DEBUG("Player %llu is requesting team status", playerID);
|
||||
if (team->local && data.zoneID.GetMapID() != team->zoneId.GetMapID() && data.zoneID.GetCloneID() != team->zoneId.GetCloneID()) {
|
||||
RemoveMember(team, playerID, false, false, false, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (team->memberIDs.size() <= 1 && !team->local) {
|
||||
DisbandTeam(team, LWOOBJID_EMPTY, u"");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!team->local) {
|
||||
SendTeamSetLeader(data, team->leaderID);
|
||||
} else {
|
||||
SendTeamSetLeader(data, LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
TeamStatusUpdate(team);
|
||||
|
||||
const auto leaderName = GeneralUtils::UTF8ToUTF16(data.playerName);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||
|
||||
if (memberId == playerID) continue;
|
||||
|
||||
const auto memberName = Game::playerContainer.GetName(memberId);
|
||||
|
||||
if (otherMember) {
|
||||
SendTeamSetOffWorldFlag(otherMember, data.playerID, data.zoneID);
|
||||
}
|
||||
SendTeamAddPlayer(data, false, team->local, false, memberId, memberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0));
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TEAM_INVITE);
|
||||
|
||||
bitStream.Write(LUWString(sender.playerName.c_str()));
|
||||
bitStream.Write(sender.playerID);
|
||||
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void TeamContainer::SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_INVITE_CONFIRM);
|
||||
|
||||
bitStream.Write(bLeaderIsFreeTrial);
|
||||
bitStream.Write(i64LeaderID);
|
||||
bitStream.Write(i64LeaderZoneID);
|
||||
bitStream.Write<uint32_t>(0); // BinaryBuffe, no clue what's in here
|
||||
bitStream.Write(ucLootFlag);
|
||||
bitStream.Write(ucNumOfOtherPlayers);
|
||||
bitStream.Write(ucResponseCode);
|
||||
bitStream.Write<uint32_t>(wsLeaderName.size());
|
||||
for (const auto character : wsLeaderName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void TeamContainer::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_GET_STATUS_RESPONSE);
|
||||
|
||||
bitStream.Write(i64LeaderID);
|
||||
bitStream.Write(i64LeaderZoneID);
|
||||
bitStream.Write<uint32_t>(0); // BinaryBuffe, no clue what's in here
|
||||
bitStream.Write(ucLootFlag);
|
||||
bitStream.Write(ucNumOfOtherPlayers);
|
||||
bitStream.Write<uint32_t>(wsLeaderName.size());
|
||||
for (const auto character : wsLeaderName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void TeamContainer::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_SET_LEADER);
|
||||
|
||||
bitStream.Write(i64PlayerID);
|
||||
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void TeamContainer::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_ADD_PLAYER);
|
||||
|
||||
bitStream.Write(bIsFreeTrial);
|
||||
bitStream.Write(bLocal);
|
||||
bitStream.Write(bNoLootOnDeath);
|
||||
bitStream.Write(i64PlayerID);
|
||||
bitStream.Write<uint32_t>(wsPlayerName.size());
|
||||
for (const auto character : wsPlayerName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
bitStream.Write1();
|
||||
if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) {
|
||||
zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0);
|
||||
}
|
||||
bitStream.Write(zoneID);
|
||||
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void TeamContainer::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_REMOVE_PLAYER);
|
||||
|
||||
bitStream.Write(bDisband);
|
||||
bitStream.Write(bIsKicked);
|
||||
bitStream.Write(bIsLeaving);
|
||||
bitStream.Write(bLocal);
|
||||
bitStream.Write(i64LeaderID);
|
||||
bitStream.Write(i64PlayerID);
|
||||
bitStream.Write<uint32_t>(wsPlayerName.size());
|
||||
for (const auto character : wsPlayerName) {
|
||||
bitStream.Write(character);
|
||||
}
|
||||
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void TeamContainer::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
|
||||
bitStream.Write(receiver.playerID);
|
||||
|
||||
//portion that will get routed:
|
||||
CMSGHEADER;
|
||||
|
||||
bitStream.Write(receiver.playerID);
|
||||
bitStream.Write(MessageType::Game::TEAM_SET_OFF_WORLD_FLAG);
|
||||
|
||||
bitStream.Write(i64PlayerID);
|
||||
if (receiver.zoneID.GetCloneID() == zoneID.GetCloneID()) {
|
||||
zoneID = LWOZONEID(zoneID.GetMapID(), zoneID.GetInstanceID(), 0);
|
||||
}
|
||||
bitStream.Write(zoneID);
|
||||
|
||||
SystemAddress sysAddr = receiver.worldServerSysAddr;
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void TeamContainer::CreateTeamServer(Packet* packet) {
|
||||
CINSTREAM_SKIP_HEADER;
|
||||
LWOOBJID playerID;
|
||||
inStream.Read(playerID);
|
||||
size_t membersSize = 0;
|
||||
inStream.Read(membersSize);
|
||||
|
||||
if (membersSize >= 4) {
|
||||
LOG("Tried to create a team with more than 4 players");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<LWOOBJID> members;
|
||||
|
||||
members.reserve(membersSize);
|
||||
|
||||
for (size_t i = 0; i < membersSize; i++) {
|
||||
LWOOBJID member;
|
||||
inStream.Read(member);
|
||||
members.push_back(member);
|
||||
}
|
||||
|
||||
LWOZONEID zoneId;
|
||||
|
||||
inStream.Read(zoneId);
|
||||
|
||||
auto* team = CreateLocalTeam(members);
|
||||
|
||||
if (team != nullptr) {
|
||||
team->zoneId = zoneId;
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
}
|
||||
|
||||
TeamData* TeamContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
|
||||
if (members.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TeamData* newTeam = nullptr;
|
||||
|
||||
for (const auto member : members) {
|
||||
auto* team = GetTeam(member);
|
||||
|
||||
if (team != nullptr) {
|
||||
RemoveMember(team, member, false, false, true);
|
||||
}
|
||||
|
||||
if (newTeam == nullptr) {
|
||||
newTeam = CreateTeam(member, true);
|
||||
} else {
|
||||
AddMember(newTeam, member);
|
||||
}
|
||||
}
|
||||
|
||||
newTeam->lootFlag = 1;
|
||||
|
||||
TeamStatusUpdate(newTeam);
|
||||
|
||||
return newTeam;
|
||||
}
|
||||
|
||||
TeamData* TeamContainer::CreateTeam(LWOOBJID leader, bool local) {
|
||||
auto* team = new TeamData();
|
||||
|
||||
team->teamID = ++g_TeamIDCounter;
|
||||
team->leaderID = leader;
|
||||
team->local = local;
|
||||
|
||||
GetTeamsMut().push_back(team);
|
||||
|
||||
AddMember(team, leader);
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
TeamData* TeamContainer::GetTeam(LWOOBJID playerID) {
|
||||
for (auto* team : GetTeams()) {
|
||||
if (std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID) == team->memberIDs.end()) continue;
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TeamContainer::AddMember(TeamData* team, LWOOBJID playerID) {
|
||||
if (team->memberIDs.size() >= 4) {
|
||||
LOG("Tried to add player to team that already had 4 players");
|
||||
const auto& player = Game::playerContainer.GetPlayerData(playerID);
|
||||
if (!player) return;
|
||||
ChatPackets::SendSystemMessage(player.worldServerSysAddr, u"The teams is full! You have not been added to a team!");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = std::find(team->memberIDs.begin(), team->memberIDs.end(), playerID);
|
||||
|
||||
if (index != team->memberIDs.end()) return;
|
||||
|
||||
team->memberIDs.push_back(playerID);
|
||||
|
||||
const auto& leader = Game::playerContainer.GetPlayerData(team->leaderID);
|
||||
const auto& member = Game::playerContainer.GetPlayerData(playerID);
|
||||
|
||||
if (!leader || !member) return;
|
||||
|
||||
const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName);
|
||||
const auto memberName = GeneralUtils::UTF8ToUTF16(member.playerName);
|
||||
|
||||
SendTeamInviteConfirm(member, false, leader.playerID, leader.zoneID, team->lootFlag, 0, 0, leaderName);
|
||||
|
||||
if (!team->local) {
|
||||
SendTeamSetLeader(member, leader.playerID);
|
||||
} else {
|
||||
SendTeamSetLeader(member, LWOOBJID_EMPTY);
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||
|
||||
if (otherMember == member) continue;
|
||||
|
||||
const auto otherMemberName = Game::playerContainer.GetName(memberId);
|
||||
|
||||
SendTeamAddPlayer(member, false, team->local, false, memberId, otherMemberName, otherMember ? otherMember.zoneID : LWOZONEID(0, 0, 0));
|
||||
|
||||
if (otherMember) {
|
||||
SendTeamAddPlayer(otherMember, false, team->local, false, member.playerID, memberName, member.zoneID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::RemoveMember(TeamData* team, LWOOBJID causingPlayerID, bool disband, bool kicked, bool leaving, bool silent) {
|
||||
LOG_DEBUG("Player %llu is leaving team %i", causingPlayerID, team->teamID);
|
||||
const auto index = std::ranges::find(team->memberIDs, causingPlayerID);
|
||||
|
||||
if (index == team->memberIDs.end()) return;
|
||||
|
||||
team->memberIDs.erase(index);
|
||||
|
||||
const auto& member = Game::playerContainer.GetPlayerData(causingPlayerID);
|
||||
|
||||
const auto causingMemberName = Game::playerContainer.GetName(causingPlayerID);
|
||||
|
||||
if (member && !silent) {
|
||||
SendTeamRemovePlayer(member, disband, kicked, leaving, team->local, LWOOBJID_EMPTY, causingPlayerID, causingMemberName);
|
||||
}
|
||||
|
||||
if (team->memberIDs.size() <= 1) {
|
||||
DisbandTeam(team, causingPlayerID, causingMemberName);
|
||||
} else /* team has enough members to be a team still */ {
|
||||
team->leaderID = (causingPlayerID == team->leaderID) ? team->memberIDs[0] : team->leaderID;
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
if (silent && memberId == causingPlayerID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
SendTeamRemovePlayer(otherMember, disband, kicked, leaving, team->local, team->leaderID, causingPlayerID, causingMemberName);
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::PromoteMember(TeamData* team, LWOOBJID newLeader) {
|
||||
team->leaderID = newLeader;
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
SendTeamSetLeader(otherMember, newLeader);
|
||||
}
|
||||
}
|
||||
|
||||
void TeamContainer::DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName) {
|
||||
const auto index = std::ranges::find(GetTeams(), team);
|
||||
|
||||
if (index == GetTeams().end()) return;
|
||||
LOG_DEBUG("Disbanding team %i", (*index)->teamID);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
SendTeamSetLeader(otherMember, LWOOBJID_EMPTY);
|
||||
SendTeamRemovePlayer(otherMember, true, false, false, team->local, team->leaderID, causingPlayerID, causingPlayerName);
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, true);
|
||||
|
||||
GetTeamsMut().erase(index);
|
||||
|
||||
delete team;
|
||||
}
|
||||
|
||||
void TeamContainer::TeamStatusUpdate(TeamData* team) {
|
||||
const auto index = std::find(GetTeams().begin(), GetTeams().end(), team);
|
||||
|
||||
if (index == GetTeams().end()) return;
|
||||
|
||||
const auto& leader = Game::playerContainer.GetPlayerData(team->leaderID);
|
||||
|
||||
if (!leader) return;
|
||||
|
||||
const auto leaderName = GeneralUtils::UTF8ToUTF16(leader.playerName);
|
||||
|
||||
for (const auto memberId : team->memberIDs) {
|
||||
const auto& otherMember = Game::playerContainer.GetPlayerData(memberId);
|
||||
|
||||
if (!otherMember) continue;
|
||||
|
||||
if (!team->local) {
|
||||
SendTeamStatus(otherMember, team->leaderID, leader.zoneID, team->lootFlag, 0, leaderName);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTeamsOnWorld(team, false);
|
||||
}
|
||||
|
||||
void TeamContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::TEAM_GET_STATUS);
|
||||
|
||||
bitStream.Write(team->teamID);
|
||||
bitStream.Write(deleteTeam);
|
||||
|
||||
if (!deleteTeam) {
|
||||
bitStream.Write(team->lootFlag);
|
||||
bitStream.Write<char>(team->memberIDs.size());
|
||||
for (const auto memberID : team->memberIDs) {
|
||||
bitStream.Write(memberID);
|
||||
}
|
||||
}
|
||||
|
||||
Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true);
|
||||
}
|
||||
59
dChatServer/TeamContainer.h
Normal file
59
dChatServer/TeamContainer.h
Normal file
@@ -0,0 +1,59 @@
|
||||
// Darkflame Universe
|
||||
// Copyright 2025
|
||||
|
||||
#ifndef TEAMCONTAINER_H
|
||||
#define TEAMCONTAINER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
|
||||
struct Packet;
|
||||
struct PlayerData;
|
||||
struct TeamData;
|
||||
|
||||
namespace TeamContainer {
|
||||
struct Data {
|
||||
std::vector<TeamData*> mTeams;
|
||||
};
|
||||
|
||||
void Shutdown();
|
||||
|
||||
void HandleTeamInvite(Packet* packet);
|
||||
void HandleTeamInviteResponse(Packet* packet);
|
||||
void HandleTeamLeave(Packet* packet);
|
||||
void HandleTeamKick(Packet* packet);
|
||||
void HandleTeamPromote(Packet* packet);
|
||||
void HandleTeamLootOption(Packet* packet);
|
||||
void HandleTeamStatusRequest(Packet* packet);
|
||||
|
||||
void SendTeamInvite(const PlayerData& receiver, const PlayerData& sender);
|
||||
void SendTeamInviteConfirm(const PlayerData& receiver, bool bLeaderIsFreeTrial, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, uint8_t ucResponseCode, std::u16string wsLeaderName);
|
||||
void SendTeamStatus(const PlayerData& receiver, LWOOBJID i64LeaderID, LWOZONEID i64LeaderZoneID, uint8_t ucLootFlag, uint8_t ucNumOfOtherPlayers, std::u16string wsLeaderName);
|
||||
void SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID);
|
||||
void SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTrial, bool bLocal, bool bNoLootOnDeath, LWOOBJID i64PlayerID, std::u16string wsPlayerName, LWOZONEID zoneID);
|
||||
|
||||
/* Sends a message to the provided `receiver` with information about the updated team. If `i64LeaderID` is not LWOOBJID_EMPTY, the client will update the leader to that new playerID. */
|
||||
void SendTeamRemovePlayer(const PlayerData& receiver, bool bDisband, bool bIsKicked, bool bIsLeaving, bool bLocal, LWOOBJID i64LeaderID, LWOOBJID i64PlayerID, std::u16string wsPlayerName);
|
||||
void SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID);
|
||||
|
||||
void CreateTeamServer(Packet* packet);
|
||||
|
||||
TeamData* CreateLocalTeam(std::vector<LWOOBJID> members);
|
||||
TeamData* CreateTeam(LWOOBJID leader, bool local = false);
|
||||
TeamData* GetTeam(LWOOBJID playerID);
|
||||
void AddMember(TeamData* team, LWOOBJID playerID);
|
||||
void RemoveMember(TeamData* team, LWOOBJID playerID, bool disband, bool kicked, bool leaving, bool silent = false);
|
||||
void PromoteMember(TeamData* team, LWOOBJID newLeader);
|
||||
void DisbandTeam(TeamData* team, const LWOOBJID causingPlayerID, const std::u16string& causingPlayerName);
|
||||
void TeamStatusUpdate(TeamData* team);
|
||||
void UpdateTeamsOnWorld(TeamData* team, bool deleteTeam);
|
||||
|
||||
const TeamContainer::Data& GetTeamContainer();
|
||||
std::vector<TeamData*>& GetTeamsMut();
|
||||
const std::vector<TeamData*>& GetTeams();
|
||||
};
|
||||
|
||||
#endif //!TEAMCONTAINER_H
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
// AMFValue template class instantiations
|
||||
template <typename ValueType>
|
||||
class AMFValue : public AMFBaseValue {
|
||||
static_assert(!std::is_same_v<ValueType, std::string_view>, "AMFValue cannot be instantiated with std::string_view");
|
||||
public:
|
||||
AMFValue() = default;
|
||||
AMFValue(const ValueType value) : m_Data{ value } {}
|
||||
@@ -52,6 +53,15 @@ public:
|
||||
|
||||
void SetValue(const ValueType value) { m_Data = value; }
|
||||
|
||||
AMFValue<ValueType>& operator=(const AMFValue<ValueType>& other) {
|
||||
return operator=(other.m_Data);
|
||||
}
|
||||
|
||||
AMFValue<ValueType>& operator=(const ValueType& other) {
|
||||
m_Data = other;
|
||||
return *this;
|
||||
}
|
||||
|
||||
protected:
|
||||
ValueType m_Data;
|
||||
};
|
||||
@@ -211,13 +221,17 @@ public:
|
||||
* @param key The key to associate with the value
|
||||
* @param value The value to insert
|
||||
*/
|
||||
void Insert(const std::string_view key, std::unique_ptr<AMFBaseValue> value) {
|
||||
template<typename AmfType>
|
||||
AmfType& Insert(const std::string_view key, std::unique_ptr<AmfType> value) {
|
||||
const auto element = m_Associative.find(key);
|
||||
auto& toReturn = *value;
|
||||
if (element != m_Associative.cend() && element->second) {
|
||||
element->second = std::move(value);
|
||||
} else {
|
||||
m_Associative.emplace(key, std::move(value));
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,11 +243,15 @@ public:
|
||||
* @param key The key to associate with the value
|
||||
* @param value The value to insert
|
||||
*/
|
||||
void Insert(const size_t index, std::unique_ptr<AMFBaseValue> value) {
|
||||
template<typename AmfType>
|
||||
AmfType& Insert(const size_t index, std::unique_ptr<AmfType> value) {
|
||||
auto& toReturn = *value;
|
||||
if (index >= m_Dense.size()) {
|
||||
m_Dense.resize(index + 1);
|
||||
}
|
||||
|
||||
m_Dense.at(index) = std::move(value);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -349,6 +367,13 @@ public:
|
||||
m_Dense.clear();
|
||||
}
|
||||
|
||||
template<typename AmfType = AMFArrayValue>
|
||||
AmfType& PushDebug(const std::string_view name) {
|
||||
auto* value = PushArray();
|
||||
value->Insert("name", name.data());
|
||||
return value->Insert<AmfType>("value", std::make_unique<AmfType>());
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* The associative portion. These values are key'd with strings to an AMFValue.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "Database.h"
|
||||
#include "Game.h"
|
||||
#include "Sd0.h"
|
||||
#include "ZCompression.h"
|
||||
#include "Logger.h"
|
||||
|
||||
@@ -44,10 +45,10 @@ uint32_t BrickByBrickFix::TruncateBrokenBrickByBrickXml() {
|
||||
}
|
||||
|
||||
// Ignore the valgrind warning about uninitialized values. These are discarded later when we know the actual uncompressed size.
|
||||
std::unique_ptr<uint8_t[]> uncompressedChunk(new uint8_t[ZCompression::MAX_SD0_CHUNK_SIZE]);
|
||||
std::unique_ptr<uint8_t[]> uncompressedChunk(new uint8_t[Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE]);
|
||||
int32_t err{};
|
||||
int32_t actualUncompressedSize = ZCompression::Decompress(
|
||||
compressedChunk.get(), chunkSize, uncompressedChunk.get(), ZCompression::MAX_SD0_CHUNK_SIZE, err);
|
||||
compressedChunk.get(), chunkSize, uncompressedChunk.get(), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err);
|
||||
|
||||
if (actualUncompressedSize != -1) {
|
||||
uint32_t previousSize = completeUncompressedModel.size();
|
||||
@@ -117,7 +118,7 @@ uint32_t BrickByBrickFix::UpdateBrickByBrickModelsToSd0() {
|
||||
}
|
||||
|
||||
std::string outputString(sd0ConvertedModel.get(), oldLxfmlSizeWithHeader);
|
||||
std::istringstream outputStringStream(outputString);
|
||||
std::stringstream outputStringStream(outputString);
|
||||
|
||||
try {
|
||||
Database::Get()->UpdateUgcModelData(model.id, outputStringStream);
|
||||
|
||||
@@ -16,6 +16,11 @@ set(DCOMMON_SOURCES
|
||||
"BrickByBrickFix.cpp"
|
||||
"BinaryPathFinder.cpp"
|
||||
"FdbToSqlite.cpp"
|
||||
"JSONUtils.cpp"
|
||||
"TinyXmlUtils.cpp"
|
||||
"Sd0.cpp"
|
||||
"Lxfml.cpp"
|
||||
"LxfmlBugged.cpp"
|
||||
)
|
||||
|
||||
# Workaround for compiler bug where the optimized code could result in a memcpy of 0 bytes, even though that isnt possible.
|
||||
@@ -49,6 +54,8 @@ elseif (WIN32)
|
||||
zlib
|
||||
URL https://github.com/madler/zlib/archive/refs/tags/v1.2.11.zip
|
||||
URL_HASH MD5=9d6a627693163bbbf3f26403a3a0b0b1
|
||||
GIT_PROGRESS TRUE
|
||||
GIT_SHALLOW 1
|
||||
)
|
||||
|
||||
# Disable warning about no project version.
|
||||
@@ -69,5 +76,6 @@ else ()
|
||||
endif ()
|
||||
|
||||
target_link_libraries(dCommon
|
||||
PUBLIC glm::glm
|
||||
PRIVATE ZLIB::ZLIB bcrypt tinyxml2
|
||||
INTERFACE dDatabase)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <assert.h>
|
||||
|
||||
#ifdef _DEBUG
|
||||
# define DluAssert(expression) assert(expression)
|
||||
# define DluAssert(expression) do { assert(expression); } while(0)
|
||||
#else
|
||||
# define DluAssert(expression)
|
||||
#endif
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// C++
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -18,6 +19,9 @@
|
||||
#include "dPlatforms.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "DluAssert.h"
|
||||
|
||||
#include <glm/ext/vector_float3.hpp>
|
||||
|
||||
enum eInventoryType : uint32_t;
|
||||
enum class eObjectBits : size_t;
|
||||
@@ -145,7 +149,7 @@ namespace GeneralUtils {
|
||||
template <typename... Bases>
|
||||
struct overload : Bases... {
|
||||
using is_transparent = void;
|
||||
using Bases::operator() ... ;
|
||||
using Bases::operator() ...;
|
||||
};
|
||||
|
||||
struct char_pointer_hash {
|
||||
@@ -202,7 +206,7 @@ namespace GeneralUtils {
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(!Numeric<T>)
|
||||
requires(!Numeric<T>)
|
||||
[[nodiscard]] std::optional<T> TryParse(std::string_view str);
|
||||
|
||||
#if !(__GNUC__ >= 11 || _MSC_VER >= 1924)
|
||||
@@ -221,7 +225,7 @@ namespace GeneralUtils {
|
||||
*/
|
||||
template <std::floating_point T>
|
||||
[[nodiscard]] std::optional<T> TryParse(std::string_view str) noexcept
|
||||
try {
|
||||
try {
|
||||
while (!str.empty() && std::isspace(str.front())) str.remove_prefix(1);
|
||||
|
||||
size_t parseNum;
|
||||
@@ -243,7 +247,7 @@ namespace GeneralUtils {
|
||||
* @returns An std::optional containing the desired NiPoint3 if it can be constructed from the string parameters
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] std::optional<NiPoint3> TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ) {
|
||||
[[nodiscard]] std::optional<T> TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ) {
|
||||
const auto x = TryParse<float>(strX);
|
||||
if (!x) return std::nullopt;
|
||||
|
||||
@@ -251,7 +255,7 @@ namespace GeneralUtils {
|
||||
if (!y) return std::nullopt;
|
||||
|
||||
const auto z = TryParse<float>(strZ);
|
||||
return z ? std::make_optional<NiPoint3>(x.value(), y.value(), z.value()) : std::nullopt;
|
||||
return z ? std::make_optional<T>(x.value(), y.value(), z.value()) : std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,8 +264,8 @@ namespace GeneralUtils {
|
||||
* @returns An std::optional containing the desired NiPoint3 if it can be constructed from the string parameters
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] std::optional<NiPoint3> TryParse(const std::span<const std::string> str) {
|
||||
return (str.size() == 3) ? TryParse<NiPoint3>(str[0], str[1], str[2]) : std::nullopt;
|
||||
[[nodiscard]] std::optional<T> TryParse(const std::span<const std::string> str) {
|
||||
return (str.size() == 3) ? TryParse<T>(str[0], str[1], str[2]) : std::nullopt;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
@@ -299,6 +303,12 @@ namespace GeneralUtils {
|
||||
return T();
|
||||
}
|
||||
|
||||
template<typename Container>
|
||||
inline Container::value_type GetRandomElement(const Container& container) {
|
||||
DluAssert(!container.empty());
|
||||
return container[GenerateRandomNumber<typename Container::size_type>(0, container.size() - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the value of an enum entry to its underlying type
|
||||
* @param entry Enum entry to cast
|
||||
@@ -323,4 +333,28 @@ namespace GeneralUtils {
|
||||
|
||||
return GenerateRandomNumber<T>(std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
|
||||
}
|
||||
|
||||
// https://www.quora.com/How-do-you-round-to-specific-increments-like-0-5-in-C
|
||||
// Rounds to the nearest floating point value specified.
|
||||
template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
|
||||
T RountToNearestEven(const T value, const T modulus) {
|
||||
const auto modulo = std::fmod(value, modulus);
|
||||
const auto abs_modulo_2 = std::abs(modulo * 2);
|
||||
const auto abs_modulus = std::abs(modulus);
|
||||
|
||||
bool round_away_from_zero = false;
|
||||
if (abs_modulo_2 > abs_modulus) {
|
||||
round_away_from_zero = true;
|
||||
} else if (abs_modulo_2 == abs_modulus) {
|
||||
const auto trunc_quot = std::floor(std::abs(value / modulus));
|
||||
const auto odd = std::fmod(trunc_quot, T{ 2 }) != 0;
|
||||
round_away_from_zero = odd;
|
||||
}
|
||||
|
||||
if (round_away_from_zero) {
|
||||
return value + (std::copysign(modulus, value) - modulo);
|
||||
} else {
|
||||
return value - modulo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
dCommon/JSONUtils.cpp
Normal file
17
dCommon/JSONUtils.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "JSONUtils.h"
|
||||
#include "json.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
std::string JSONUtils::CheckRequiredData(const json& data, const std::vector<std::string>& requiredData) {
|
||||
json check;
|
||||
check["error"] = json::array();
|
||||
for (const auto& required : requiredData) {
|
||||
if (!data.contains(required)) {
|
||||
check["error"].push_back("Missing Parameter: " + required);
|
||||
} else if (data[required] == "") {
|
||||
check["error"].push_back("Empty Parameter: " + required);
|
||||
}
|
||||
}
|
||||
return check["error"].empty() ? "" : check.dump();
|
||||
}
|
||||
11
dCommon/JSONUtils.h
Normal file
11
dCommon/JSONUtils.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef _JSONUTILS_H_
|
||||
#define _JSONUTILS_H_
|
||||
|
||||
#include "json_fwd.hpp"
|
||||
|
||||
namespace JSONUtils {
|
||||
// check required fields in json data
|
||||
std::string CheckRequiredData(const nlohmann::json& data, const std::vector<std::string>& requiredData);
|
||||
}
|
||||
|
||||
#endif // _JSONUTILS_H_
|
||||
@@ -83,6 +83,12 @@ public:
|
||||
this->value = value;
|
||||
}
|
||||
|
||||
//! Initializer
|
||||
LDFData(const std::string& key, const T& value) {
|
||||
this->key = GeneralUtils::ASCIIToUTF16(key);
|
||||
this->value = value;
|
||||
}
|
||||
|
||||
//! Destructor
|
||||
~LDFData(void) override {}
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* path) {
|
||||
// they will not be valid constexpr and will be evaluated at runtime instead of compile time!
|
||||
// The full string is still stored in the binary, however the offset of the filename in the absolute paths
|
||||
// is used in the instruction instead of the start of the absolute path.
|
||||
#define LOG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->Log(str, message, ##__VA_ARGS__); } while(0)
|
||||
#define LOG_DEBUG(message, ...) do { auto str = FILENAME_AND_LINE; Game::logger->LogDebug(str, message, ##__VA_ARGS__); } while(0)
|
||||
#define LOG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->Log(str_, message, ##__VA_ARGS__); } while(0)
|
||||
#define LOG_DEBUG(message, ...) do { auto str_ = FILENAME_AND_LINE; Game::logger->LogDebug(str_, message, ##__VA_ARGS__); } while(0)
|
||||
|
||||
// Writer class for writing data to files.
|
||||
class Writer {
|
||||
|
||||
130
dCommon/Lxfml.cpp
Normal file
130
dCommon/Lxfml.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "Lxfml.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "TinyXmlUtils.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
|
||||
Result toReturn;
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto err = doc.Parse(data.data());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
TinyXmlUtils::DocumentReader reader(doc);
|
||||
std::map<std::string/* refID */, std::string> transformations;
|
||||
|
||||
auto lxfml = reader["LXFML"];
|
||||
if (!lxfml) {
|
||||
LOG("Failed to find LXFML element.");
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
// First get all the positions of bricks
|
||||
for (const auto& brick : lxfml["Bricks"]) {
|
||||
const auto* part = brick.FirstChildElement("Part");
|
||||
while (part) {
|
||||
const auto* bone = part->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
auto* transformation = bone->Attribute("transformation");
|
||||
if (transformation) {
|
||||
auto* refID = bone->Attribute("refID");
|
||||
if (refID) transformations[refID] = transformation;
|
||||
}
|
||||
}
|
||||
part = part->NextSiblingElement("Part");
|
||||
}
|
||||
}
|
||||
|
||||
// These points are well out of bounds for an actual player
|
||||
NiPoint3 lowest{ 10'000.0f, 10'000.0f, 10'000.0f };
|
||||
NiPoint3 highest{ -10'000.0f, -10'000.0f, -10'000.0f };
|
||||
|
||||
NiPoint3 delta = NiPoint3Constant::ZERO;
|
||||
if (curPosition == NiPoint3Constant::ZERO) {
|
||||
// Calculate the lowest and highest points on the entire model
|
||||
for (const auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (z < lowest.z) lowest.z = z;
|
||||
|
||||
if (highest.x < x) highest.x = x;
|
||||
if (highest.y < y) highest.y = y;
|
||||
if (highest.z < z) highest.z = z;
|
||||
}
|
||||
|
||||
delta = (highest - lowest) / 2.0f;
|
||||
} else {
|
||||
lowest = curPosition;
|
||||
highest = curPosition;
|
||||
delta = NiPoint3Constant::ZERO;
|
||||
}
|
||||
|
||||
auto newRootPos = lowest + delta;
|
||||
|
||||
// Need to snap this chosen position to the nearest valid spot
|
||||
// on the LEGO grid
|
||||
newRootPos.x = GeneralUtils::RountToNearestEven(newRootPos.x, 0.8f);
|
||||
newRootPos.z = GeneralUtils::RountToNearestEven(newRootPos.z, 0.8f);
|
||||
|
||||
// Clamp the Y to the lowest point on the model
|
||||
newRootPos.y = lowest.y;
|
||||
|
||||
// Adjust all positions to account for the new origin
|
||||
for (auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x;
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
|
||||
std::stringstream stream;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
stream << split[i];
|
||||
stream << ',';
|
||||
}
|
||||
stream << x << ',' << y << ',' << z;
|
||||
transformation = stream.str();
|
||||
}
|
||||
|
||||
// Finally write the new transformation back into the lxfml
|
||||
for (auto& brick : lxfml["Bricks"]) {
|
||||
auto* part = brick.FirstChildElement("Part");
|
||||
while (part) {
|
||||
auto* bone = part->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
auto* transformation = bone->Attribute("transformation");
|
||||
if (transformation) {
|
||||
auto* refID = bone->Attribute("refID");
|
||||
if (refID) {
|
||||
bone->SetAttribute("transformation", transformations[refID].c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
part = part->NextSiblingElement("Part");
|
||||
}
|
||||
}
|
||||
|
||||
tinyxml2::XMLPrinter printer;
|
||||
doc.Print(&printer);
|
||||
|
||||
toReturn.lxfml = printer.CStr();
|
||||
toReturn.center = newRootPos;
|
||||
return toReturn;
|
||||
}
|
||||
27
dCommon/Lxfml.h
Normal file
27
dCommon/Lxfml.h
Normal file
@@ -0,0 +1,27 @@
|
||||
// Darkflame Universe
|
||||
// Copyright 2025
|
||||
|
||||
#ifndef LXFML_H
|
||||
#define LXFML_H
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "NiPoint3.h"
|
||||
|
||||
namespace Lxfml {
|
||||
struct Result {
|
||||
std::string lxfml;
|
||||
NiPoint3 center;
|
||||
};
|
||||
|
||||
// Normalizes a LXFML model to be positioned relative to its local 0, 0, 0 rather than a game worlds 0, 0, 0.
|
||||
// Returns a struct of its new center and the updated LXFML containing these edits.
|
||||
[[nodiscard]] Result NormalizePosition(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
|
||||
|
||||
// these are only for the migrations due to a bug in one of the implementations.
|
||||
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);
|
||||
[[nodiscard]] Result NormalizePositionAfterFirstPart(const std::string_view data, const NiPoint3& position);
|
||||
};
|
||||
|
||||
#endif //!LXFML_H
|
||||
210
dCommon/LxfmlBugged.cpp
Normal file
210
dCommon/LxfmlBugged.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include "Lxfml.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "TinyXmlUtils.h"
|
||||
|
||||
#include <ranges>
|
||||
|
||||
// this file should not be touched
|
||||
|
||||
Lxfml::Result Lxfml::NormalizePositionOnlyFirstPart(const std::string_view data) {
|
||||
Result toReturn;
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto err = doc.Parse(data.data());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
TinyXmlUtils::DocumentReader reader(doc);
|
||||
std::map<std::string/* refID */, std::string> transformations;
|
||||
|
||||
auto lxfml = reader["LXFML"];
|
||||
if (!lxfml) {
|
||||
LOG("Failed to find LXFML element.");
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
// First get all the positions of bricks
|
||||
for (const auto& brick : lxfml["Bricks"]) {
|
||||
const auto* part = brick.FirstChildElement("Part");
|
||||
if (part) {
|
||||
const auto* bone = part->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
auto* transformation = bone->Attribute("transformation");
|
||||
if (transformation) {
|
||||
auto* refID = bone->Attribute("refID");
|
||||
if (refID) transformations[refID] = transformation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These points are well out of bounds for an actual player
|
||||
NiPoint3 lowest{ 10'000.0f, 10'000.0f, 10'000.0f };
|
||||
NiPoint3 highest{ -10'000.0f, -10'000.0f, -10'000.0f };
|
||||
|
||||
// Calculate the lowest and highest points on the entire model
|
||||
for (const auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (z < lowest.z) lowest.z = z;
|
||||
|
||||
if (highest.x < x) highest.x = x;
|
||||
if (highest.y < y) highest.y = y;
|
||||
if (highest.z < z) highest.z = z;
|
||||
}
|
||||
|
||||
auto delta = (highest - lowest) / 2.0f;
|
||||
auto newRootPos = lowest + delta;
|
||||
|
||||
// Clamp the Y to the lowest point on the model
|
||||
newRootPos.y = lowest.y;
|
||||
|
||||
// Adjust all positions to account for the new origin
|
||||
for (auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x;
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y;
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z;
|
||||
std::stringstream stream;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
stream << split[i];
|
||||
stream << ',';
|
||||
}
|
||||
stream << x << ',' << y << ',' << z;
|
||||
transformation = stream.str();
|
||||
}
|
||||
|
||||
// Finally write the new transformation back into the lxfml
|
||||
for (auto& brick : lxfml["Bricks"]) {
|
||||
auto* part = brick.FirstChildElement("Part");
|
||||
if (part) {
|
||||
auto* bone = part->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
auto* transformation = bone->Attribute("transformation");
|
||||
if (transformation) {
|
||||
auto* refID = bone->Attribute("refID");
|
||||
if (refID) {
|
||||
bone->SetAttribute("transformation", transformations[refID].c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tinyxml2::XMLPrinter printer;
|
||||
doc.Print(&printer);
|
||||
|
||||
toReturn.lxfml = printer.CStr();
|
||||
toReturn.center = newRootPos;
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
Lxfml::Result Lxfml::NormalizePositionAfterFirstPart(const std::string_view data, const NiPoint3& position) {
|
||||
Result toReturn;
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto err = doc.Parse(data.data());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
TinyXmlUtils::DocumentReader reader(doc);
|
||||
std::map<std::string/* refID */, std::string> transformations;
|
||||
|
||||
auto lxfml = reader["LXFML"];
|
||||
if (!lxfml) {
|
||||
LOG("Failed to find LXFML element.");
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
// First get all the positions of bricks
|
||||
for (const auto& brick : lxfml["Bricks"]) {
|
||||
const auto* part = brick.FirstChildElement("Part");
|
||||
bool firstPart = true;
|
||||
while (part) {
|
||||
if (firstPart) {
|
||||
firstPart = false;
|
||||
} else {
|
||||
LOG("Found extra bricks");
|
||||
const auto* bone = part->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
auto* transformation = bone->Attribute("transformation");
|
||||
if (transformation) {
|
||||
auto* refID = bone->Attribute("refID");
|
||||
if (refID) transformations[refID] = transformation;
|
||||
}
|
||||
}
|
||||
}
|
||||
part = part->NextSiblingElement("Part");
|
||||
}
|
||||
}
|
||||
|
||||
auto newRootPos = position;
|
||||
|
||||
// Adjust all positions to account for the new origin
|
||||
for (auto& transformation : transformations | std::views::values) {
|
||||
auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x;
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y;
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z;
|
||||
std::stringstream stream;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
stream << split[i];
|
||||
stream << ',';
|
||||
}
|
||||
stream << x << ',' << y << ',' << z;
|
||||
transformation = stream.str();
|
||||
}
|
||||
|
||||
// Finally write the new transformation back into the lxfml
|
||||
for (auto& brick : lxfml["Bricks"]) {
|
||||
auto* part = brick.FirstChildElement("Part");
|
||||
bool firstPart = true;
|
||||
while (part) {
|
||||
if (firstPart) {
|
||||
firstPart = false;
|
||||
} else {
|
||||
auto* bone = part->FirstChildElement("Bone");
|
||||
if (bone) {
|
||||
auto* transformation = bone->Attribute("transformation");
|
||||
if (transformation) {
|
||||
auto* refID = bone->Attribute("refID");
|
||||
if (refID) {
|
||||
bone->SetAttribute("transformation", transformations[refID].c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
part = part->NextSiblingElement("Part");
|
||||
}
|
||||
}
|
||||
|
||||
tinyxml2::XMLPrinter printer;
|
||||
doc.Print(&printer);
|
||||
|
||||
toReturn.lxfml = printer.CStr();
|
||||
toReturn.center = newRootPos;
|
||||
return toReturn;
|
||||
}
|
||||
@@ -6,10 +6,14 @@
|
||||
\brief Defines a point in space in XYZ coordinates
|
||||
*/
|
||||
|
||||
|
||||
class NiPoint3;
|
||||
class NiQuaternion;
|
||||
typedef NiPoint3 Vector3; //!< The Vector3 class is technically the NiPoint3 class, but typedef'd for clarity in some cases
|
||||
|
||||
#include <glm/ext/vector_float3.hpp>
|
||||
|
||||
#include "NiQuaternion.h"
|
||||
|
||||
//! A custom class the defines a point in space
|
||||
class NiPoint3 {
|
||||
public:
|
||||
@@ -21,6 +25,12 @@ public:
|
||||
//! Initializer
|
||||
constexpr NiPoint3() = default;
|
||||
|
||||
constexpr NiPoint3(const glm::vec3& vec) noexcept
|
||||
: x{ vec.x }
|
||||
, y{ vec.y }
|
||||
, z{ vec.z } {
|
||||
}
|
||||
|
||||
//! Initializer
|
||||
/*!
|
||||
\param x The x coordinate
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#endif
|
||||
|
||||
#include "NiQuaternion.h"
|
||||
#include <glm/ext/quaternion_float.hpp>
|
||||
|
||||
// MARK: Getters / Setters
|
||||
|
||||
|
||||
@@ -3,37 +3,18 @@
|
||||
// C++
|
||||
#include <cmath>
|
||||
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
// MARK: Member Functions
|
||||
|
||||
Vector3 NiQuaternion::GetEulerAngles() const {
|
||||
Vector3 angles;
|
||||
|
||||
// roll (x-axis rotation)
|
||||
const float sinr_cosp = 2 * (w * x + y * z);
|
||||
const float cosr_cosp = 1 - 2 * (x * x + y * y);
|
||||
angles.x = std::atan2(sinr_cosp, cosr_cosp);
|
||||
|
||||
// pitch (y-axis rotation)
|
||||
const float sinp = 2 * (w * y - z * x);
|
||||
|
||||
if (std::abs(sinp) >= 1) {
|
||||
angles.y = std::copysign(3.14 / 2, sinp); // use 90 degrees if out of range
|
||||
} else {
|
||||
angles.y = std::asin(sinp);
|
||||
}
|
||||
|
||||
// yaw (z-axis rotation)
|
||||
const float siny_cosp = 2 * (w * z + x * y);
|
||||
const float cosy_cosp = 1 - 2 * (y * y + z * z);
|
||||
angles.z = std::atan2(siny_cosp, cosy_cosp);
|
||||
|
||||
return angles;
|
||||
Vector3 QuatUtils::Euler(const NiQuaternion& quat) {
|
||||
return glm::eulerAngles(quat);
|
||||
}
|
||||
|
||||
// MARK: Helper Functions
|
||||
|
||||
//! Look from a specific point in space to another point in space (Y-locked)
|
||||
NiQuaternion NiQuaternion::LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
NiQuaternion QuatUtils::LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
//To make sure we don't orient around the X/Z axis:
|
||||
NiPoint3 source = sourcePoint;
|
||||
NiPoint3 dest = destPoint;
|
||||
@@ -51,11 +32,11 @@ NiQuaternion NiQuaternion::LookAt(const NiPoint3& sourcePoint, const NiPoint3& d
|
||||
NiPoint3 vecB = vecA.CrossProduct(posZ);
|
||||
|
||||
if (vecB.DotProduct(forwardVector) < 0) rotAngle = -rotAngle;
|
||||
return NiQuaternion::CreateFromAxisAngle(vecA, rotAngle);
|
||||
return glm::angleAxis(rotAngle, glm::vec3{vecA.x, vecA.y, vecA.z});
|
||||
}
|
||||
|
||||
//! Look from a specific point in space to another point in space
|
||||
NiQuaternion NiQuaternion::LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
NiQuaternion QuatUtils::LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
NiPoint3 forwardVector = NiPoint3(destPoint - sourcePoint).Unitize();
|
||||
|
||||
NiPoint3 posZ = NiPoint3Constant::UNIT_Z;
|
||||
@@ -67,37 +48,26 @@ NiQuaternion NiQuaternion::LookAtUnlocked(const NiPoint3& sourcePoint, const NiP
|
||||
NiPoint3 vecB = vecA.CrossProduct(posZ);
|
||||
|
||||
if (vecB.DotProduct(forwardVector) < 0) rotAngle = -rotAngle;
|
||||
return NiQuaternion::CreateFromAxisAngle(vecA, rotAngle);
|
||||
return glm::angleAxis(rotAngle, glm::vec3{vecA.x, vecA.y, vecA.z});
|
||||
}
|
||||
|
||||
//! Creates a Quaternion from a specific axis and angle relative to that axis
|
||||
NiQuaternion NiQuaternion::CreateFromAxisAngle(const Vector3& axis, float angle) {
|
||||
float halfAngle = angle * 0.5f;
|
||||
float s = static_cast<float>(sin(halfAngle));
|
||||
|
||||
NiQuaternion q;
|
||||
q.x = axis.GetX() * s;
|
||||
q.y = axis.GetY() * s;
|
||||
q.z = axis.GetZ() * s;
|
||||
q.w = static_cast<float>(cos(halfAngle));
|
||||
|
||||
return q;
|
||||
NiQuaternion QuatUtils::AxisAngle(const Vector3& axis, float angle) {
|
||||
return glm::angleAxis(angle, glm::vec3(axis.x, axis.y, axis.z));
|
||||
}
|
||||
|
||||
NiQuaternion NiQuaternion::FromEulerAngles(const NiPoint3& eulerAngles) {
|
||||
// Abbreviations for the various angular functions
|
||||
float cy = cos(eulerAngles.z * 0.5);
|
||||
float sy = sin(eulerAngles.z * 0.5);
|
||||
float cp = cos(eulerAngles.y * 0.5);
|
||||
float sp = sin(eulerAngles.y * 0.5);
|
||||
float cr = cos(eulerAngles.x * 0.5);
|
||||
float sr = sin(eulerAngles.x * 0.5);
|
||||
|
||||
NiQuaternion q;
|
||||
q.w = cr * cp * cy + sr * sp * sy;
|
||||
q.x = sr * cp * cy - cr * sp * sy;
|
||||
q.y = cr * sp * cy + sr * cp * sy;
|
||||
q.z = cr * cp * sy - sr * sp * cy;
|
||||
|
||||
return q;
|
||||
NiQuaternion QuatUtils::FromEuler(const NiPoint3& eulerAngles) {
|
||||
return glm::quat(glm::vec3(eulerAngles.x, eulerAngles.y, eulerAngles.z));
|
||||
}
|
||||
|
||||
Vector3 QuatUtils::Forward(const NiQuaternion& quat) {
|
||||
return quat * glm::vec3(0, 0, 1);
|
||||
}
|
||||
|
||||
Vector3 QuatUtils::Up(const NiQuaternion& quat) {
|
||||
return quat * glm::vec3(0, 1, 0);
|
||||
}
|
||||
|
||||
Vector3 QuatUtils::Right(const NiQuaternion& quat) {
|
||||
return quat * glm::vec3(1, 0, 0);
|
||||
}
|
||||
|
||||
@@ -1,158 +1,27 @@
|
||||
#ifndef __NIQUATERNION_H__
|
||||
#define __NIQUATERNION_H__
|
||||
#ifndef NIQUATERNION_H
|
||||
#define NIQUATERNION_H
|
||||
|
||||
// Custom Classes
|
||||
#include "NiPoint3.h"
|
||||
|
||||
/*!
|
||||
\file NiQuaternion.hpp
|
||||
\brief Defines a quaternion in space in WXYZ coordinates
|
||||
*/
|
||||
#define GLM_FORCE_QUAT_DATA_WXYZ
|
||||
|
||||
class NiQuaternion;
|
||||
typedef NiQuaternion Quaternion; //!< A typedef for a shorthand version of NiQuaternion
|
||||
#include <glm/ext/quaternion_float.hpp>
|
||||
|
||||
//! A class that defines a rotation in space
|
||||
class NiQuaternion {
|
||||
public:
|
||||
float w{ 1 }; //!< The w coordinate
|
||||
float x{ 0 }; //!< The x coordinate
|
||||
float y{ 0 }; //!< The y coordinate
|
||||
float z{ 0 }; //!< The z coordinate
|
||||
using Quaternion = glm::quat;
|
||||
using NiQuaternion = Quaternion;
|
||||
|
||||
|
||||
//! The initializer
|
||||
constexpr NiQuaternion() = default;
|
||||
|
||||
//! The initializer
|
||||
/*!
|
||||
\param w The w coordinate
|
||||
\param x The x coordinate
|
||||
\param y The y coordinate
|
||||
\param z The z coordinate
|
||||
*/
|
||||
constexpr NiQuaternion(const float w, const float x, const float y, const float z) noexcept
|
||||
: w{ w }
|
||||
, x{ x }
|
||||
, y{ y }
|
||||
, z{ z } {
|
||||
}
|
||||
|
||||
// MARK: Setters / Getters
|
||||
|
||||
//! Gets the W coordinate
|
||||
/*!
|
||||
\return The w coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetW() const noexcept;
|
||||
|
||||
//! Sets the W coordinate
|
||||
/*!
|
||||
\param w The w coordinate
|
||||
*/
|
||||
constexpr void SetW(const float w) noexcept;
|
||||
|
||||
//! Gets the X coordinate
|
||||
/*!
|
||||
\return The x coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetX() const noexcept;
|
||||
|
||||
//! Sets the X coordinate
|
||||
/*!
|
||||
\param x The x coordinate
|
||||
*/
|
||||
constexpr void SetX(const float x) noexcept;
|
||||
|
||||
//! Gets the Y coordinate
|
||||
/*!
|
||||
\return The y coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetY() const noexcept;
|
||||
|
||||
//! Sets the Y coordinate
|
||||
/*!
|
||||
\param y The y coordinate
|
||||
*/
|
||||
constexpr void SetY(const float y) noexcept;
|
||||
|
||||
//! Gets the Z coordinate
|
||||
/*!
|
||||
\return The z coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetZ() const noexcept;
|
||||
|
||||
//! Sets the Z coordinate
|
||||
/*!
|
||||
\param z The z coordinate
|
||||
*/
|
||||
constexpr void SetZ(const float z) noexcept;
|
||||
|
||||
// MARK: Member Functions
|
||||
|
||||
//! Returns the forward vector from the quaternion
|
||||
/*!
|
||||
\return The forward vector of the quaternion
|
||||
*/
|
||||
[[nodiscard]] constexpr Vector3 GetForwardVector() const noexcept;
|
||||
|
||||
//! Returns the up vector from the quaternion
|
||||
/*!
|
||||
\return The up vector fo the quaternion
|
||||
*/
|
||||
[[nodiscard]] constexpr Vector3 GetUpVector() const noexcept;
|
||||
|
||||
//! Returns the right vector from the quaternion
|
||||
/*!
|
||||
\return The right vector of the quaternion
|
||||
*/
|
||||
[[nodiscard]] constexpr Vector3 GetRightVector() const noexcept;
|
||||
|
||||
[[nodiscard]] Vector3 GetEulerAngles() const;
|
||||
|
||||
// MARK: Operators
|
||||
|
||||
//! Operator to check for equality
|
||||
constexpr bool operator==(const NiQuaternion& rot) const noexcept;
|
||||
|
||||
//! Operator to check for inequality
|
||||
constexpr bool operator!=(const NiQuaternion& rot) const noexcept;
|
||||
|
||||
// MARK: Helper Functions
|
||||
|
||||
//! Look from a specific point in space to another point in space (Y-locked)
|
||||
/*!
|
||||
\param sourcePoint The source location
|
||||
\param destPoint The destination location
|
||||
\return The Quaternion with the rotation towards the destination
|
||||
*/
|
||||
[[nodiscard]] static NiQuaternion LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint);
|
||||
|
||||
//! Look from a specific point in space to another point in space
|
||||
/*!
|
||||
\param sourcePoint The source location
|
||||
\param destPoint The destination location
|
||||
\return The Quaternion with the rotation towards the destination
|
||||
*/
|
||||
[[nodiscard]] static NiQuaternion LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint);
|
||||
|
||||
//! Creates a Quaternion from a specific axis and angle relative to that axis
|
||||
/*!
|
||||
\param axis The axis that is used
|
||||
\param angle The angle relative to this axis
|
||||
\return A quaternion created from the axis and angle
|
||||
*/
|
||||
[[nodiscard]] static NiQuaternion CreateFromAxisAngle(const Vector3& axis, float angle);
|
||||
|
||||
[[nodiscard]] static NiQuaternion FromEulerAngles(const NiPoint3& eulerAngles);
|
||||
namespace QuatUtils {
|
||||
constexpr NiQuaternion IDENTITY = glm::identity<NiQuaternion>();
|
||||
Vector3 Forward(const NiQuaternion& quat);
|
||||
Vector3 Up(const NiQuaternion& quat);
|
||||
Vector3 Right(const NiQuaternion& quat);
|
||||
NiQuaternion LookAt(const NiPoint3& from, const NiPoint3& to);
|
||||
NiQuaternion LookAtUnlocked(const NiPoint3& from, const NiPoint3& to);
|
||||
Vector3 Euler(const NiQuaternion& quat);
|
||||
NiQuaternion AxisAngle(const Vector3& axis, float angle);
|
||||
NiQuaternion FromEuler(const NiPoint3& eulerAngles);
|
||||
constexpr float PI_OVER_180 = glm::pi<float>() / 180.0f;
|
||||
};
|
||||
|
||||
// Static Variables
|
||||
namespace NiQuaternionConstant {
|
||||
constexpr NiQuaternion IDENTITY(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Include constexpr and inline function definitions in a seperate file for readability
|
||||
#include "NiQuaternion.inl"
|
||||
|
||||
#endif // !__NIQUATERNION_H__
|
||||
#endif // !NIQUATERNION_H
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#pragma once
|
||||
#ifndef __NIQUATERNION_H__
|
||||
#error "This should only be included inline in NiQuaternion.h: Do not include directly!"
|
||||
#endif
|
||||
|
||||
// MARK: Setters / Getters
|
||||
|
||||
//! Gets the W coordinate
|
||||
constexpr float NiQuaternion::GetW() const noexcept {
|
||||
return this->w;
|
||||
}
|
||||
|
||||
//! Sets the W coordinate
|
||||
constexpr void NiQuaternion::SetW(const float w) noexcept {
|
||||
this->w = w;
|
||||
}
|
||||
|
||||
//! Gets the X coordinate
|
||||
constexpr float NiQuaternion::GetX() const noexcept {
|
||||
return this->x;
|
||||
}
|
||||
|
||||
//! Sets the X coordinate
|
||||
constexpr void NiQuaternion::SetX(const float x) noexcept {
|
||||
this->x = x;
|
||||
}
|
||||
|
||||
//! Gets the Y coordinate
|
||||
constexpr float NiQuaternion::GetY() const noexcept {
|
||||
return this->y;
|
||||
}
|
||||
|
||||
//! Sets the Y coordinate
|
||||
constexpr void NiQuaternion::SetY(const float y) noexcept {
|
||||
this->y = y;
|
||||
}
|
||||
|
||||
//! Gets the Z coordinate
|
||||
constexpr float NiQuaternion::GetZ() const noexcept {
|
||||
return this->z;
|
||||
}
|
||||
|
||||
//! Sets the Z coordinate
|
||||
constexpr void NiQuaternion::SetZ(const float z) noexcept {
|
||||
this->z = z;
|
||||
}
|
||||
|
||||
// MARK: Member Functions
|
||||
|
||||
//! Returns the forward vector from the quaternion
|
||||
constexpr Vector3 NiQuaternion::GetForwardVector() const noexcept {
|
||||
return Vector3(2 * (x * z + w * y), 2 * (y * z - w * x), 1 - 2 * (x * x + y * y));
|
||||
}
|
||||
|
||||
//! Returns the up vector from the quaternion
|
||||
constexpr Vector3 NiQuaternion::GetUpVector() const noexcept {
|
||||
return Vector3(2 * (x * y - w * z), 1 - 2 * (x * x + z * z), 2 * (y * z + w * x));
|
||||
}
|
||||
|
||||
//! Returns the right vector from the quaternion
|
||||
constexpr Vector3 NiQuaternion::GetRightVector() const noexcept {
|
||||
return Vector3(1 - 2 * (y * y + z * z), 2 * (x * y + w * z), 2 * (x * z - w * y));
|
||||
}
|
||||
|
||||
// MARK: Operators
|
||||
|
||||
//! Operator to check for equality
|
||||
constexpr bool NiQuaternion::operator==(const NiQuaternion& rot) const noexcept {
|
||||
return rot.x == this->x && rot.y == this->y && rot.z == this->z && rot.w == this->w;
|
||||
}
|
||||
|
||||
//! Operator to check for inequality
|
||||
constexpr bool NiQuaternion::operator!=(const NiQuaternion& rot) const noexcept {
|
||||
return !(*this == rot);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ struct LocalSpaceInfo {
|
||||
|
||||
struct PositionUpdate {
|
||||
NiPoint3 position = NiPoint3Constant::ZERO;
|
||||
NiQuaternion rotation = NiQuaternionConstant::IDENTITY;
|
||||
NiQuaternion rotation = QuatUtils::IDENTITY;
|
||||
bool onGround = false;
|
||||
bool onRail = false;
|
||||
NiPoint3 velocity = NiPoint3Constant::ZERO;
|
||||
|
||||
150
dCommon/Sd0.cpp
Normal file
150
dCommon/Sd0.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
#include "Sd0.h"
|
||||
|
||||
#include <array>
|
||||
#include <ranges>
|
||||
|
||||
#include "BinaryIO.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
|
||||
#include "ZCompression.h"
|
||||
|
||||
// Insert header if on first buffer
|
||||
void WriteHeader(Sd0::BinaryBuffer& chunk) {
|
||||
chunk.push_back(Sd0::SD0_HEADER[0]);
|
||||
chunk.push_back(Sd0::SD0_HEADER[1]);
|
||||
chunk.push_back(Sd0::SD0_HEADER[2]);
|
||||
chunk.push_back(Sd0::SD0_HEADER[3]);
|
||||
chunk.push_back(Sd0::SD0_HEADER[4]);
|
||||
}
|
||||
|
||||
// Write the size of the buffer to a chunk
|
||||
void WriteSize(Sd0::BinaryBuffer& chunk, uint32_t chunkSize) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
char toPush = chunkSize & 0xff;
|
||||
chunkSize = chunkSize >> 8;
|
||||
chunk.push_back(toPush);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t GetDataOffset(bool firstBuffer) {
|
||||
return firstBuffer ? 9 : 4;
|
||||
}
|
||||
|
||||
Sd0::Sd0(std::istream& buffer) {
|
||||
char header[5]{};
|
||||
|
||||
// Check if this is an sd0 buffer. It's possible we may be handed a zlib buffer directly due to old code so check for that too.
|
||||
if (!BinaryIO::BinaryRead(buffer, header) || memcmp(header, SD0_HEADER, sizeof(header)) != 0) {
|
||||
LOG("Failed to read SD0 header %i %i %i %i %i %i %i", buffer.good(), buffer.tellg(), header[0], header[1], header[2], header[3], header[4]);
|
||||
LOG_DEBUG("This may be a zlib buffer directly? Trying again assuming its a zlib buffer.");
|
||||
auto& firstChunk = m_Chunks.emplace_back();
|
||||
WriteHeader(firstChunk);
|
||||
buffer.seekg(0, std::ios::end);
|
||||
uint32_t bufferSize = buffer.tellg();
|
||||
buffer.seekg(0, std::ios::beg);
|
||||
WriteSize(firstChunk, bufferSize);
|
||||
firstChunk.resize(firstChunk.size() + bufferSize);
|
||||
auto* dataStart = reinterpret_cast<char*>(firstChunk.data() + GetDataOffset(true));
|
||||
if (!buffer.read(dataStart, bufferSize)) {
|
||||
m_Chunks.pop_back();
|
||||
LOG("Failed to read %u bytes from chunk %i", bufferSize, m_Chunks.size() - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
while (buffer && buffer.peek() != std::istream::traits_type::eof()) {
|
||||
uint32_t chunkSize{};
|
||||
if (!BinaryIO::BinaryRead(buffer, chunkSize)) {
|
||||
LOG("Failed to read chunk size from stream %lld %zu", buffer.tellg(), m_Chunks.size());
|
||||
break;
|
||||
}
|
||||
auto& chunk = m_Chunks.emplace_back();
|
||||
bool firstBuffer = m_Chunks.size() == 1;
|
||||
auto dataOffset = GetDataOffset(firstBuffer);
|
||||
|
||||
// Insert header if on first buffer
|
||||
if (firstBuffer) {
|
||||
WriteHeader(chunk);
|
||||
}
|
||||
|
||||
WriteSize(chunk, chunkSize);
|
||||
|
||||
chunk.resize(chunkSize + dataOffset);
|
||||
auto* dataStart = reinterpret_cast<char*>(chunk.data() + dataOffset);
|
||||
if (!buffer.read(dataStart, chunkSize)) {
|
||||
m_Chunks.pop_back();
|
||||
LOG("Failed to read %u bytes from chunk %i", chunkSize, m_Chunks.size() - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sd0::FromData(const uint8_t* data, size_t bufferSize) {
|
||||
const auto originalBufferSize = bufferSize;
|
||||
if (bufferSize == 0) return;
|
||||
|
||||
m_Chunks.clear();
|
||||
while (bufferSize > 0) {
|
||||
const auto numToCopy = std::min(MAX_UNCOMPRESSED_CHUNK_SIZE, bufferSize);
|
||||
const auto* startOffset = data + originalBufferSize - bufferSize;
|
||||
bufferSize -= numToCopy;
|
||||
std::array<uint8_t, MAX_UNCOMPRESSED_CHUNK_SIZE> compressedChunk;
|
||||
const auto compressedSize = ZCompression::Compress(
|
||||
startOffset, numToCopy,
|
||||
compressedChunk.data(), compressedChunk.size());
|
||||
|
||||
auto& chunk = m_Chunks.emplace_back();
|
||||
bool firstBuffer = m_Chunks.size() == 1;
|
||||
auto dataOffset = GetDataOffset(firstBuffer);
|
||||
|
||||
if (firstBuffer) {
|
||||
WriteHeader(chunk);
|
||||
}
|
||||
|
||||
WriteSize(chunk, compressedSize);
|
||||
|
||||
chunk.resize(compressedSize + dataOffset);
|
||||
memcpy(chunk.data() + dataOffset, compressedChunk.data(), compressedSize);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::string Sd0::GetAsStringUncompressed() const {
|
||||
std::string toReturn;
|
||||
bool first = true;
|
||||
uint32_t totalSize{};
|
||||
for (const auto& chunk : m_Chunks) {
|
||||
auto dataOffset = GetDataOffset(first);
|
||||
first = false;
|
||||
const auto chunkSize = chunk.size();
|
||||
|
||||
auto oldSize = toReturn.size();
|
||||
toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE);
|
||||
int32_t error{};
|
||||
const auto uncompressedSize = ZCompression::Decompress(
|
||||
chunk.data() + dataOffset, chunkSize - dataOffset,
|
||||
reinterpret_cast<uint8_t*>(toReturn.data()) + oldSize, MAX_UNCOMPRESSED_CHUNK_SIZE,
|
||||
error);
|
||||
|
||||
totalSize += uncompressedSize;
|
||||
}
|
||||
|
||||
toReturn.resize(totalSize);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
std::stringstream Sd0::GetAsStream() const {
|
||||
std::stringstream toReturn;
|
||||
|
||||
for (const auto& chunk : m_Chunks) {
|
||||
toReturn.write(reinterpret_cast<const char*>(chunk.data()), chunk.size());
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
const std::vector<Sd0::BinaryBuffer>& Sd0::GetAsVector() const {
|
||||
return m_Chunks;
|
||||
}
|
||||
42
dCommon/Sd0.h
Normal file
42
dCommon/Sd0.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// Darkflame Universe
|
||||
// Copyright 2025
|
||||
|
||||
#ifndef SD0_H
|
||||
#define SD0_H
|
||||
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
// Sd0 is comprised of multiple zlib compressed buffers stored in a row.
|
||||
// The format starts with a SD0 header (see SD0_HEADER) followed by the size of a zlib buffer, and then the zlib buffer itself.
|
||||
// This repeats until end of file
|
||||
class Sd0 {
|
||||
public:
|
||||
using BinaryBuffer = std::vector<uint8_t>;
|
||||
|
||||
static inline const char* SD0_HEADER = "sd0\x01\xff";
|
||||
|
||||
/**
|
||||
* @brief Max size of an inflated sd0 zlib chunk
|
||||
*/
|
||||
static constexpr inline size_t MAX_UNCOMPRESSED_CHUNK_SIZE = 1024 * 256;
|
||||
|
||||
// Read the input buffer into an internal chunk stream to be used later
|
||||
Sd0(std::istream& buffer);
|
||||
|
||||
// Uncompresses the entire Sd0 buffer and returns it as a string
|
||||
[[nodiscard]] std::string GetAsStringUncompressed() const;
|
||||
|
||||
// Gets the Sd0 buffer as a stream in its raw compressed form
|
||||
[[nodiscard]] std::stringstream GetAsStream() const;
|
||||
|
||||
// Gets the Sd0 buffer as a vector in its raw compressed form
|
||||
[[nodiscard]] const std::vector<BinaryBuffer>& GetAsVector() const;
|
||||
|
||||
// Compress data into a Sd0 buffer
|
||||
void FromData(const uint8_t* data, size_t bufferSize);
|
||||
private:
|
||||
std::vector<BinaryBuffer> m_Chunks{};
|
||||
};
|
||||
|
||||
#endif //!SD0_H
|
||||
37
dCommon/TinyXmlUtils.cpp
Normal file
37
dCommon/TinyXmlUtils.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "TinyXmlUtils.h"
|
||||
|
||||
#include <tinyxml2.h>
|
||||
|
||||
using namespace TinyXmlUtils;
|
||||
|
||||
Element DocumentReader::operator[](const std::string_view elem) const {
|
||||
return Element(m_Doc.FirstChildElement(elem.empty() ? nullptr : elem.data()), elem);
|
||||
}
|
||||
|
||||
Element::Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem) :
|
||||
m_IteratedName{ elem },
|
||||
m_Elem{ xmlElem } {
|
||||
}
|
||||
|
||||
Element Element::operator[](const std::string_view elem) const {
|
||||
const auto* usedElem = elem.empty() ? nullptr : elem.data();
|
||||
auto* toReturn = m_Elem ? m_Elem->FirstChildElement(usedElem) : nullptr;
|
||||
return Element(toReturn, m_IteratedName);
|
||||
}
|
||||
|
||||
ElementIterator Element::begin() {
|
||||
return ElementIterator(m_Elem ? m_Elem->FirstChildElement() : nullptr);
|
||||
}
|
||||
|
||||
ElementIterator Element::end() {
|
||||
return ElementIterator(nullptr);
|
||||
}
|
||||
|
||||
ElementIterator::ElementIterator(tinyxml2::XMLElement* elem) :
|
||||
m_CurElem{ elem } {
|
||||
}
|
||||
|
||||
ElementIterator& ElementIterator::operator++() {
|
||||
if (m_CurElem) m_CurElem = m_CurElem->NextSiblingElement();
|
||||
return *this;
|
||||
}
|
||||
66
dCommon/TinyXmlUtils.h
Normal file
66
dCommon/TinyXmlUtils.h
Normal file
@@ -0,0 +1,66 @@
|
||||
// Darkflame Universe
|
||||
// Copyright 2025
|
||||
|
||||
#ifndef TINYXMLUTILS_H
|
||||
#define TINYXMLUTILS_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "DluAssert.h"
|
||||
|
||||
#include <tinyxml2.h>
|
||||
|
||||
namespace TinyXmlUtils {
|
||||
// See cstdlib for iterator technicalities
|
||||
struct ElementIterator {
|
||||
ElementIterator(tinyxml2::XMLElement* elem);
|
||||
|
||||
ElementIterator& operator++();
|
||||
[[nodiscard]] tinyxml2::XMLElement* operator->() { DluAssert(m_CurElem); return m_CurElem; }
|
||||
[[nodiscard]] tinyxml2::XMLElement& operator*() { DluAssert(m_CurElem); return *m_CurElem; }
|
||||
|
||||
bool operator==(const ElementIterator& other) const { return other.m_CurElem == m_CurElem; }
|
||||
|
||||
private:
|
||||
tinyxml2::XMLElement* m_CurElem{ nullptr };
|
||||
};
|
||||
|
||||
// Wrapper class to act as an iterator over xml elements.
|
||||
// All the normal rules that apply to Iterators in the std library apply here.
|
||||
class Element {
|
||||
public:
|
||||
Element(tinyxml2::XMLElement* xmlElem, const std::string_view elem);
|
||||
|
||||
// The first child element of this element.
|
||||
[[nodiscard]] ElementIterator begin();
|
||||
|
||||
// Always returns an ElementIterator which points to nullptr.
|
||||
// TinyXml2 return NULL when you've reached the last child element so
|
||||
// you can't do any funny one past end logic here.
|
||||
[[nodiscard]] ElementIterator end();
|
||||
|
||||
// Get a child element
|
||||
[[nodiscard]] Element operator[](const std::string_view elem) const;
|
||||
[[nodiscard]] Element operator[](const char* elem) const { return operator[](std::string_view(elem)); };
|
||||
|
||||
// Whether or not data exists for this element
|
||||
operator bool() const { return m_Elem != nullptr; }
|
||||
|
||||
[[nodiscard]] const tinyxml2::XMLElement* operator->() const { return m_Elem; }
|
||||
private:
|
||||
const char* GetElementName() const { return m_IteratedName.empty() ? nullptr : m_IteratedName.c_str(); }
|
||||
const std::string m_IteratedName;
|
||||
tinyxml2::XMLElement* m_Elem;
|
||||
};
|
||||
|
||||
class DocumentReader {
|
||||
public:
|
||||
DocumentReader(tinyxml2::XMLDocument& doc) : m_Doc{ doc } {}
|
||||
|
||||
[[nodiscard]] Element operator[](const std::string_view elem) const;
|
||||
private:
|
||||
tinyxml2::XMLDocument& m_Doc;
|
||||
};
|
||||
};
|
||||
|
||||
#endif //!TINYXMLUTILS_H
|
||||
@@ -8,11 +8,5 @@ namespace ZCompression {
|
||||
int32_t Compress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst);
|
||||
|
||||
int32_t Decompress(const uint8_t* abSrc, int32_t nLenSrc, uint8_t* abDst, int32_t nLenDst, int32_t& nErr);
|
||||
|
||||
/**
|
||||
* @brief Max size of an inflated sd0 zlib chunk
|
||||
*
|
||||
*/
|
||||
constexpr uint32_t MAX_SD0_CHUNK_SIZE = 1024 * 256;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
#include "zlib.h"
|
||||
|
||||
constexpr uint32_t CRC32_INIT = 0xFFFFFFFF;
|
||||
constexpr auto NULL_TERMINATOR = std::string_view{"\0\0\0", 4};
|
||||
|
||||
AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
if (!std::filesystem::is_directory(path)) {
|
||||
throw std::runtime_error("Attempted to load asset bundle (" + path.string() + ") however it is not a valid directory.");
|
||||
@@ -18,12 +21,20 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
|
||||
m_RootPath = m_Path;
|
||||
m_ResPath = (m_Path / "client" / "res");
|
||||
} else if (std::filesystem::exists(m_Path / ".." / "versions") && std::filesystem::exists(m_Path / "res")) {
|
||||
} else if (std::filesystem::exists(m_Path / "res" / "pack")) {
|
||||
if (!std::filesystem::exists(m_Path / ".." / "versions")) {
|
||||
throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded.");
|
||||
}
|
||||
|
||||
m_AssetBundleType = eAssetBundleType::Packed;
|
||||
|
||||
m_RootPath = (m_Path / "..");
|
||||
m_ResPath = (m_Path / "res");
|
||||
} else if (std::filesystem::exists(m_Path / "pack") && std::filesystem::exists(m_Path / ".." / ".." / "versions")) {
|
||||
} else if (std::filesystem::exists(m_Path / "pack")) {
|
||||
if (!std::filesystem::exists(m_Path / ".." / ".." / "versions")) {
|
||||
throw std::runtime_error("No \"versions\" directory found in the parent directories of \"res\" - packed asset bundle cannot be loaded.");
|
||||
}
|
||||
|
||||
m_AssetBundleType = eAssetBundleType::Packed;
|
||||
|
||||
m_RootPath = (m_Path / ".." / "..");
|
||||
@@ -48,6 +59,7 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
break;
|
||||
}
|
||||
case eAssetBundleType::None:
|
||||
[[fallthrough]];
|
||||
case eAssetBundleType::Unpacked: {
|
||||
break;
|
||||
}
|
||||
@@ -55,19 +67,10 @@ AssetManager::AssetManager(const std::filesystem::path& path) {
|
||||
}
|
||||
|
||||
void AssetManager::LoadPackIndex() {
|
||||
m_PackIndex = new PackIndex(m_RootPath);
|
||||
m_PackIndex = PackIndex(m_RootPath);
|
||||
}
|
||||
|
||||
std::filesystem::path AssetManager::GetResPath() {
|
||||
return m_ResPath;
|
||||
}
|
||||
|
||||
eAssetBundleType AssetManager::GetAssetBundleType() {
|
||||
return m_AssetBundleType;
|
||||
}
|
||||
|
||||
bool AssetManager::HasFile(const char* name) {
|
||||
auto fixedName = std::string(name);
|
||||
bool AssetManager::HasFile(std::string fixedName) const {
|
||||
std::transform(fixedName.begin(), fixedName.end(), fixedName.begin(), [](uint8_t c) { return std::tolower(c); });
|
||||
|
||||
// Special case for unpacked client have BrickModels in upper case
|
||||
@@ -81,8 +84,7 @@ bool AssetManager::HasFile(const char* name) {
|
||||
std::replace(fixedName.begin(), fixedName.end(), '/', '\\');
|
||||
if (fixedName.rfind("client\\res\\", 0) != 0) fixedName = "client\\res\\" + fixedName;
|
||||
|
||||
uint32_t crc = crc32b(0xFFFFFFFF, reinterpret_cast<uint8_t*>(const_cast<char*>(fixedName.c_str())), fixedName.size());
|
||||
crc = crc32b(crc, reinterpret_cast<Bytef*>(const_cast<char*>("\0\0\0\0")), 4);
|
||||
const auto crc = crc32b(crc32b(CRC32_INIT, fixedName), NULL_TERMINATOR);
|
||||
|
||||
for (const auto& item : this->m_PackIndex->GetPackFileIndices()) {
|
||||
if (item.m_Crc == crc) {
|
||||
@@ -93,8 +95,7 @@ bool AssetManager::HasFile(const char* name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AssetManager::GetFile(const char* name, char** data, uint32_t* len) {
|
||||
auto fixedName = std::string(name);
|
||||
bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) const {
|
||||
std::transform(fixedName.begin(), fixedName.end(), fixedName.begin(), [](uint8_t c) { return std::tolower(c); });
|
||||
std::replace(fixedName.begin(), fixedName.end(), '\\', '/'); // On the off chance someone has the wrong slashes, force forward slashes
|
||||
|
||||
@@ -129,8 +130,7 @@ bool AssetManager::GetFile(const char* name, char** data, uint32_t* len) {
|
||||
fixedName = "client\\res\\" + fixedName;
|
||||
}
|
||||
int32_t packIndex = -1;
|
||||
uint32_t crc = crc32b(0xFFFFFFFF, reinterpret_cast<uint8_t*>(const_cast<char*>(fixedName.c_str())), fixedName.size());
|
||||
crc = crc32b(crc, reinterpret_cast<Bytef*>(const_cast<char*>("\0\0\0\0")), 4);
|
||||
auto crc = crc32b(crc32b(CRC32_INIT, fixedName), NULL_TERMINATOR);
|
||||
|
||||
for (const auto& item : this->m_PackIndex->GetPackFileIndices()) {
|
||||
if (item.m_Crc == crc) {
|
||||
@@ -144,15 +144,13 @@ bool AssetManager::GetFile(const char* name, char** data, uint32_t* len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto packs = this->m_PackIndex->GetPacks();
|
||||
auto* pack = packs.at(packIndex);
|
||||
|
||||
bool success = pack->ReadFileFromPack(crc, data, len);
|
||||
const auto& pack = this->m_PackIndex->GetPacks().at(packIndex);
|
||||
const bool success = pack.ReadFileFromPack(crc, data, len);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
AssetStream AssetManager::GetFile(const char* name) {
|
||||
AssetStream AssetManager::GetFile(const char* name) const {
|
||||
char* buf; uint32_t len;
|
||||
|
||||
bool success = this->GetFile(name, &buf, &len);
|
||||
@@ -160,23 +158,15 @@ AssetStream AssetManager::GetFile(const char* name) {
|
||||
return AssetStream(buf, len, success);
|
||||
}
|
||||
|
||||
uint32_t AssetManager::crc32b(uint32_t base, uint8_t* message, size_t l) {
|
||||
size_t i, j;
|
||||
uint32_t crc, msb;
|
||||
|
||||
crc = base;
|
||||
for (i = 0; i < l; i++) {
|
||||
uint32_t AssetManager::crc32b(uint32_t crc, const std::string_view message) {
|
||||
for (const auto byte : message) {
|
||||
// xor next byte to upper bits of crc
|
||||
crc ^= (static_cast<unsigned int>(message[i]) << 24);
|
||||
for (j = 0; j < 8; j++) { // Do eight times.
|
||||
msb = crc >> 31;
|
||||
crc ^= (static_cast<uint32_t>(std::bit_cast<uint8_t>(byte)) << 24);
|
||||
for (size_t _ = 0; _ < 8; _++) { // Do eight times.
|
||||
const uint32_t msb = crc >> 31;
|
||||
crc <<= 1;
|
||||
crc ^= (0 - msb) & 0x04C11DB7;
|
||||
}
|
||||
}
|
||||
return crc; // don't complement crc on output
|
||||
}
|
||||
|
||||
AssetManager::~AssetManager() {
|
||||
delete m_PackIndex;
|
||||
}
|
||||
|
||||
@@ -61,23 +61,32 @@ struct AssetStream : std::istream {
|
||||
class AssetManager {
|
||||
public:
|
||||
AssetManager(const std::filesystem::path& path);
|
||||
~AssetManager();
|
||||
|
||||
std::filesystem::path GetResPath();
|
||||
eAssetBundleType GetAssetBundleType();
|
||||
[[nodiscard]]
|
||||
const std::filesystem::path& GetResPath() const {
|
||||
return m_ResPath;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
eAssetBundleType GetAssetBundleType() const {
|
||||
return m_AssetBundleType;
|
||||
}
|
||||
|
||||
bool HasFile(const char* name);
|
||||
bool GetFile(const char* name, char** data, uint32_t* len);
|
||||
AssetStream GetFile(const char* name);
|
||||
[[nodiscard]]
|
||||
bool HasFile(std::string name) const;
|
||||
|
||||
[[nodiscard]]
|
||||
bool GetFile(std::string name, char** data, uint32_t* len) const;
|
||||
|
||||
[[nodiscard]]
|
||||
AssetStream GetFile(const char* name) const;
|
||||
|
||||
private:
|
||||
void LoadPackIndex();
|
||||
|
||||
// Modified crc algorithm (mpeg2)
|
||||
// Reference: https://stackoverflow.com/questions/54339800/how-to-modify-crc-32-to-crc-32-mpeg-2
|
||||
inline uint32_t crc32b(uint32_t base, uint8_t* message, size_t l);
|
||||
|
||||
bool m_SuccessfullyLoaded;
|
||||
static inline uint32_t crc32b(uint32_t crc, std::string_view message);
|
||||
|
||||
std::filesystem::path m_Path;
|
||||
std::filesystem::path m_RootPath;
|
||||
@@ -85,5 +94,5 @@ private:
|
||||
|
||||
eAssetBundleType m_AssetBundleType = eAssetBundleType::None;
|
||||
|
||||
PackIndex* m_PackIndex;
|
||||
std::optional<PackIndex> m_PackIndex;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "Pack.h"
|
||||
|
||||
#include "BinaryIO.h"
|
||||
#include "Sd0.h"
|
||||
#include "ZCompression.h"
|
||||
|
||||
Pack::Pack(const std::filesystem::path& filePath) {
|
||||
@@ -21,19 +22,20 @@ Pack::Pack(const std::filesystem::path& filePath) {
|
||||
|
||||
m_FileStream.seekg(recordCountPos, std::ios::beg);
|
||||
|
||||
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_RecordCount);
|
||||
uint32_t recordCount = 0;
|
||||
BinaryIO::BinaryRead<uint32_t>(m_FileStream, recordCount);
|
||||
|
||||
for (int i = 0; i < m_RecordCount; i++) {
|
||||
m_Records.reserve(recordCount);
|
||||
std::generate_n(std::back_inserter(m_Records), recordCount, [&] {
|
||||
PackRecord record;
|
||||
BinaryIO::BinaryRead<PackRecord>(m_FileStream, record);
|
||||
|
||||
m_Records.push_back(record);
|
||||
}
|
||||
return record;
|
||||
});
|
||||
|
||||
m_FileStream.close();
|
||||
}
|
||||
|
||||
bool Pack::HasFile(uint32_t crc) {
|
||||
bool Pack::HasFile(const uint32_t crc) const {
|
||||
for (const auto& record : m_Records) {
|
||||
if (record.m_Crc == crc) {
|
||||
return true;
|
||||
@@ -43,7 +45,7 @@ bool Pack::HasFile(uint32_t crc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pack::ReadFileFromPack(uint32_t crc, char** data, uint32_t* len) {
|
||||
bool Pack::ReadFileFromPack(const uint32_t crc, char** data, uint32_t* len) const {
|
||||
// Time for some wacky C file reading for speed reasons
|
||||
|
||||
PackRecord pkRecord{};
|
||||
@@ -105,7 +107,7 @@ bool Pack::ReadFileFromPack(uint32_t crc, char** data, uint32_t* len) {
|
||||
pos += size; // Move pointer position the amount of bytes read to the right
|
||||
|
||||
int32_t err;
|
||||
currentReadPos += ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), ZCompression::MAX_SD0_CHUNK_SIZE, err);
|
||||
currentReadPos += ZCompression::Decompress(reinterpret_cast<uint8_t*>(chunk), size, reinterpret_cast<uint8_t*>(decompressedData + currentReadPos), Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, err);
|
||||
|
||||
free(chunk);
|
||||
}
|
||||
|
||||
@@ -24,16 +24,17 @@ struct PackRecord {
|
||||
class Pack {
|
||||
public:
|
||||
Pack(const std::filesystem::path& filePath);
|
||||
~Pack() = default;
|
||||
|
||||
bool HasFile(uint32_t crc);
|
||||
bool ReadFileFromPack(uint32_t crc, char** data, uint32_t* len);
|
||||
[[nodiscard]]
|
||||
bool HasFile(uint32_t crc) const;
|
||||
|
||||
[[nodiscard]]
|
||||
bool ReadFileFromPack(uint32_t crc, char** data, uint32_t* len) const;
|
||||
private:
|
||||
std::ifstream m_FileStream;
|
||||
std::filesystem::path m_FilePath;
|
||||
|
||||
char m_Version[7];
|
||||
|
||||
uint32_t m_RecordCount;
|
||||
std::vector<PackRecord> m_Records;
|
||||
};
|
||||
|
||||
@@ -6,38 +6,32 @@
|
||||
PackIndex::PackIndex(const std::filesystem::path& filePath) {
|
||||
m_FileStream = std::ifstream(filePath / "versions" / "primary.pki", std::ios::in | std::ios::binary);
|
||||
|
||||
uint32_t packPathCount = 0;
|
||||
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_Version);
|
||||
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_PackPathCount);
|
||||
BinaryIO::BinaryRead<uint32_t>(m_FileStream, packPathCount);
|
||||
|
||||
m_PackPaths.resize(m_PackPathCount);
|
||||
m_PackPaths.resize(packPathCount);
|
||||
for (auto& item : m_PackPaths) {
|
||||
BinaryIO::ReadString<uint32_t>(m_FileStream, item, BinaryIO::ReadType::String);
|
||||
}
|
||||
|
||||
BinaryIO::BinaryRead<uint32_t>(m_FileStream, m_PackFileIndexCount);
|
||||
uint32_t packFileIndexCount = 0;
|
||||
BinaryIO::BinaryRead<uint32_t>(m_FileStream, packFileIndexCount);
|
||||
|
||||
for (int i = 0; i < m_PackFileIndexCount; i++) {
|
||||
m_PackFileIndices.reserve(packFileIndexCount);
|
||||
std::generate_n(std::back_inserter(m_PackFileIndices), packFileIndexCount, [&] {
|
||||
PackFileIndex packFileIndex;
|
||||
BinaryIO::BinaryRead<PackFileIndex>(m_FileStream, packFileIndex);
|
||||
|
||||
m_PackFileIndices.push_back(packFileIndex);
|
||||
}
|
||||
return packFileIndex;
|
||||
});
|
||||
|
||||
LOG("Loaded pack catalog with %i pack files and %i files", m_PackPaths.size(), m_PackFileIndices.size());
|
||||
|
||||
m_Packs.reserve(m_PackPaths.size());
|
||||
for (auto& item : m_PackPaths) {
|
||||
std::replace(item.begin(), item.end(), '\\', '/');
|
||||
|
||||
auto* pack = new Pack(filePath / item);
|
||||
|
||||
m_Packs.push_back(pack);
|
||||
m_Packs.emplace_back(filePath / item);
|
||||
}
|
||||
|
||||
m_FileStream.close();
|
||||
}
|
||||
|
||||
PackIndex::~PackIndex() {
|
||||
for (const auto* item : m_Packs) {
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,20 +21,23 @@ struct PackFileIndex {
|
||||
class PackIndex {
|
||||
public:
|
||||
PackIndex(const std::filesystem::path& filePath);
|
||||
~PackIndex();
|
||||
|
||||
const std::vector<std::string>& GetPackPaths() { return m_PackPaths; }
|
||||
const std::vector<PackFileIndex>& GetPackFileIndices() { return m_PackFileIndices; }
|
||||
const std::vector<Pack*>& GetPacks() { return m_Packs; }
|
||||
[[nodiscard]]
|
||||
const std::vector<std::string>& GetPackPaths() const { return m_PackPaths; }
|
||||
|
||||
[[nodiscard]]
|
||||
const std::vector<PackFileIndex>& GetPackFileIndices() const { return m_PackFileIndices; }
|
||||
|
||||
[[nodiscard]]
|
||||
const std::vector<Pack>& GetPacks() const { return m_Packs; }
|
||||
private:
|
||||
std::ifstream m_FileStream;
|
||||
|
||||
uint32_t m_Version;
|
||||
|
||||
uint32_t m_PackPathCount;
|
||||
std::vector<std::string> m_PackPaths;
|
||||
uint32_t m_PackFileIndexCount;
|
||||
|
||||
std::vector<PackFileIndex> m_PackFileIndices;
|
||||
|
||||
std::vector<Pack*> m_Packs;
|
||||
std::vector<Pack> m_Packs;
|
||||
};
|
||||
|
||||
@@ -47,6 +47,8 @@ void dConfig::LoadConfig() {
|
||||
void dConfig::ReloadConfig() {
|
||||
this->m_ConfigValues.clear();
|
||||
LoadConfig();
|
||||
for (const auto& handler : m_ConfigHandlers) handler();
|
||||
LogSettings();
|
||||
}
|
||||
|
||||
const std::string& dConfig::GetValue(std::string key) {
|
||||
@@ -58,6 +60,18 @@ const std::string& dConfig::GetValue(std::string key) {
|
||||
return this->m_ConfigValues[key];
|
||||
}
|
||||
|
||||
void dConfig::AddConfigHandler(std::function<void()> handler) {
|
||||
m_ConfigHandlers.push_back(handler);
|
||||
}
|
||||
|
||||
void dConfig::LogSettings() const {
|
||||
LOG("Configuration settings:");
|
||||
for (const auto& [key, value] : m_ConfigValues) {
|
||||
const auto& valueLog = key.find("password") != std::string::npos ? "<HIDDEN>" : value;
|
||||
LOG(" %s = %s", key.c_str(), valueLog.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void dConfig::ProcessLine(const std::string& line) {
|
||||
auto splitLoc = line.find('=');
|
||||
auto key = line.substr(0, splitLoc);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
@@ -29,10 +31,15 @@ public:
|
||||
* Reloads the config file to reset values
|
||||
*/
|
||||
void ReloadConfig();
|
||||
|
||||
// Adds a function to be called when the config is (re)loaded
|
||||
void AddConfigHandler(std::function<void()> handler);
|
||||
void LogSettings() const;
|
||||
|
||||
private:
|
||||
void ProcessLine(const std::string& line);
|
||||
|
||||
private:
|
||||
std::map<std::string, std::string> m_ConfigValues;
|
||||
std::vector<std::function<void()>> m_ConfigHandlers;
|
||||
std::string m_ConfigFilePath;
|
||||
};
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
namespace MessageType {
|
||||
enum class Master : uint32_t {
|
||||
REQUEST_PERSISTENT_ID = 1,
|
||||
REQUEST_PERSISTENT_ID_RESPONSE,
|
||||
REQUEST_ZONE_TRANSFER,
|
||||
REQUEST_ZONE_TRANSFER = 1,
|
||||
REQUEST_ZONE_TRANSFER_RESPONSE,
|
||||
SERVER_INFO,
|
||||
REQUEST_SESSION_KEY,
|
||||
|
||||
14
dCommon/dEnums/ServiceType.h
Normal file
14
dCommon/dEnums/ServiceType.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef __SERVICETYPE__H__
|
||||
#define __SERVICETYPE__H__
|
||||
|
||||
enum class ServiceType : uint16_t {
|
||||
COMMON = 0,
|
||||
AUTH,
|
||||
CHAT,
|
||||
WORLD = 4,
|
||||
CLIENT,
|
||||
MASTER,
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
#endif //!__SERVICETYPE__H__
|
||||
@@ -3,13 +3,14 @@
|
||||
#ifndef __DCOMMONVARS__H__
|
||||
#define __DCOMMONVARS__H__
|
||||
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include "BitStream.h"
|
||||
#include "eConnectionType.h"
|
||||
#include "MessageType/Client.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "MessageType/Client.h"
|
||||
#include "ServiceType.h"
|
||||
|
||||
#pragma warning (disable:4251) //Disables SQL warnings
|
||||
|
||||
@@ -33,7 +34,7 @@ constexpr uint32_t lowFrameDelta = FRAMES_TO_MS(lowFramerate);
|
||||
#define CBITSTREAM RakNet::BitStream bitStream;
|
||||
#define CINSTREAM RakNet::BitStream inStream(packet->data, packet->length, false);
|
||||
#define CINSTREAM_SKIP_HEADER CINSTREAM if (inStream.GetNumberOfUnreadBits() >= BYTES_TO_BITS(HEADER_SIZE)) inStream.IgnoreBytes(HEADER_SIZE); else inStream.IgnoreBits(inStream.GetNumberOfUnreadBits());
|
||||
#define CMSGHEADER BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::GAME_MSG);
|
||||
#define CMSGHEADER BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::GAME_MSG);
|
||||
#define SEND_PACKET Game::server->Send(bitStream, sysAddr, false);
|
||||
#define SEND_PACKET_BROADCAST Game::server->Send(bitStream, UNASSIGNED_SYSTEM_ADDRESS, true);
|
||||
|
||||
@@ -98,6 +99,8 @@ public:
|
||||
constexpr LWOZONEID() noexcept = default;
|
||||
constexpr LWOZONEID(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLONEID& cloneID) noexcept { m_MapID = mapID; m_InstanceID = instanceID; m_CloneID = cloneID; }
|
||||
constexpr LWOZONEID(const LWOZONEID& replacement) noexcept { *this = replacement; }
|
||||
constexpr bool operator==(const LWOZONEID&) const = default;
|
||||
constexpr auto operator<=>(const LWOZONEID&) const = default;
|
||||
|
||||
private:
|
||||
LWOMAPID m_MapID = LWOMAPID_INVALID; //1000 for VE, 1100 for AG, etc...
|
||||
|
||||
@@ -16,7 +16,11 @@ enum class eCharacterVersion : uint32_t {
|
||||
VAULT_SIZE,
|
||||
// Fixes speed base value in level component
|
||||
SPEED_BASE,
|
||||
UP_TO_DATE, // will become NJ_JAYMISSIONS
|
||||
// Fixes nexus force explorer missions
|
||||
NJ_JAYMISSIONS,
|
||||
NEXUS_FORCE_EXPLORER, // Fixes pet ids in player inventories
|
||||
PET_IDS, // Fixes pet ids in player inventories
|
||||
UP_TO_DATE, // will become INVENTORY_PERSISTENT_IDS
|
||||
};
|
||||
|
||||
#endif //!__ECHARACTERVERSION__H__
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#ifndef __ECONNECTIONTYPE__H__
|
||||
#define __ECONNECTIONTYPE__H__
|
||||
|
||||
enum class eConnectionType : uint16_t {
|
||||
SERVER = 0,
|
||||
AUTH,
|
||||
CHAT,
|
||||
WORLD = 4,
|
||||
CLIENT,
|
||||
MASTER
|
||||
};
|
||||
|
||||
#endif //!__ECONNECTIONTYPE__H__
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef __EHTTPMETHODS__H__
|
||||
#define __EHTTPMETHODS__H__
|
||||
|
||||
#include "dPlatforms.h"
|
||||
|
||||
#ifdef DARKFLAME_PLATFORM_WIN32
|
||||
#pragma push_macro("DELETE")
|
||||
#undef DELETE
|
||||
|
||||
@@ -28,7 +28,8 @@ enum eInventoryType : uint32_t {
|
||||
DONATION,
|
||||
VAULT_MODELS,
|
||||
ITEM_SETS, //internal, technically this is BankBehaviors.
|
||||
INVALID // made up, for internal use!!!, Technically this called the ALL inventory.
|
||||
INVALID, // made up, for internal use!!!, Technically this called the ALL inventory.
|
||||
ALL, // Use this to search all inventories instead of a specific one.
|
||||
};
|
||||
|
||||
class InventoryType {
|
||||
|
||||
@@ -50,7 +50,10 @@ enum class eMissionState : int {
|
||||
/**
|
||||
* The mission has been completed before and has now been completed again. Used for daily missions.
|
||||
*/
|
||||
COMPLETE_READY_TO_COMPLETE = 12
|
||||
COMPLETE_READY_TO_COMPLETE = 12,
|
||||
|
||||
// The mission is failed (don't know where this is used)
|
||||
FAILED = 16,
|
||||
};
|
||||
|
||||
#endif //!__MISSIONSTATE__H__
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#ifndef __EOBJECTBITS__H__
|
||||
#define __EOBJECTBITS__H__
|
||||
#ifndef EOBJECTBITS_H
|
||||
#define EOBJECTBITS_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
enum class eObjectBits : size_t {
|
||||
PERSISTENT = 32,
|
||||
CLIENT = 46,
|
||||
SPAWNED = 58,
|
||||
CHARACTER = 60
|
||||
};
|
||||
|
||||
#endif //!__EOBJECTBITS__H__
|
||||
#endif //!EOBJECTBITS_H
|
||||
|
||||
55
dDashboardServer/CMakeLists.txt
Normal file
55
dDashboardServer/CMakeLists.txt
Normal file
@@ -0,0 +1,55 @@
|
||||
add_subdirectory(blueprints)
|
||||
|
||||
set(DDASHBOARDSERVER_SOURCES
|
||||
"DashboardWeb.cpp"
|
||||
# Explicitly include blueprint sources to ensure they are compiled into the library
|
||||
"blueprints/AuthBlueprint.cpp"
|
||||
"blueprints/ApiBlueprint.cpp"
|
||||
"blueprints/PageBlueprint.cpp"
|
||||
"blueprints/PlayKeysBlueprint.cpp"
|
||||
"blueprints/CharactersBlueprint.cpp"
|
||||
"blueprints/MailBlueprint.cpp"
|
||||
"blueprints/BugReportsBlueprint.cpp"
|
||||
"blueprints/ModerationBlueprint.cpp"
|
||||
)
|
||||
|
||||
# Create dDashboardServer library
|
||||
add_library(dDashboardServer ${DDASHBOARDSERVER_SOURCES})
|
||||
target_include_directories(dDashboardServer PRIVATE ${PROJECT_SOURCE_DIR}/dServer)
|
||||
find_package(CURL)
|
||||
if (CURL_FOUND)
|
||||
target_link_libraries(dDashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt CURL::libcurl)
|
||||
else()
|
||||
message(WARNING "libcurl not found; building dDashboardServer without CURL::libcurl. Some features may be disabled.")
|
||||
target_link_libraries(dDashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt)
|
||||
endif()
|
||||
|
||||
add_executable(DashboardServer "DashboardServer.cpp")
|
||||
if (CURL_FOUND)
|
||||
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt CURL::libcurl dDashboardServer)
|
||||
else()
|
||||
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dServer Crow::Crow bcrypt dDashboardServer)
|
||||
endif()
|
||||
target_include_directories(DashboardServer PRIVATE ${PROJECT_SOURCE_DIR}/dServer)
|
||||
add_compile_definitions(DashboardServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}\"")
|
||||
|
||||
# Define Windows version for ASIO/Crow compatibility (Windows 10)
|
||||
if(WIN32)
|
||||
target_compile_definitions(DashboardServer PRIVATE _WIN32_WINNT=0x0A00)
|
||||
target_compile_definitions(dDashboardServer PRIVATE _WIN32_WINNT=0x0A00)
|
||||
endif()
|
||||
|
||||
# Copy static files and templates to build directory
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/static
|
||||
$<TARGET_FILE_DIR:DashboardServer>/static
|
||||
COMMENT "Copying static files to build directory"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET DashboardServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/templates
|
||||
$<TARGET_FILE_DIR:DashboardServer>/templates
|
||||
COMMENT "Copying templates to build directory"
|
||||
)
|
||||
33
dDashboardServer/DashboardHelpers.cpp
Normal file
33
dDashboardServer/DashboardHelpers.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "DashboardHelpers.h"
|
||||
|
||||
namespace DashboardHelpers {
|
||||
|
||||
DataTablesParams ParseDataTablesParams(const crow::request& req) {
|
||||
DataTablesParams p;
|
||||
try {
|
||||
if (req.url_params.get("draw")) p.draw = std::stoi(req.url_params.get("draw"));
|
||||
if (req.url_params.get("start")) p.start = std::stoi(req.url_params.get("start"));
|
||||
if (req.url_params.get("length")) p.length = std::stoi(req.url_params.get("length"));
|
||||
if (req.url_params.get("order[0][column]")) p.orderColumn = std::stoi(req.url_params.get("order[0][column]"));
|
||||
if (req.url_params.get("order[0][dir]")) p.orderDir = req.url_params.get("order[0][dir]");
|
||||
} catch (...) {
|
||||
// ignore parse errors, return defaults
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
crow::json::wvalue CreateDataTablesResponse(int draw, uint32_t recordsTotal, uint32_t recordsFiltered, const crow::json::wvalue::list& data) {
|
||||
crow::json::wvalue resp;
|
||||
resp["draw"] = draw;
|
||||
resp["recordsTotal"] = recordsTotal;
|
||||
resp["recordsFiltered"] = recordsFiltered;
|
||||
resp["data"] = data;
|
||||
return resp;
|
||||
}
|
||||
|
||||
bool RescueCharacter(const uint64_t characterId, const uint32_t zoneId) {
|
||||
// Minimal stub: not implemented here. Return false to indicate no-op.
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace DashboardHelpers
|
||||
24
dDashboardServer/DashboardHelpers.h
Normal file
24
dDashboardServer/DashboardHelpers.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <crow.h>
|
||||
#include <string>
|
||||
|
||||
namespace DashboardHelpers {
|
||||
|
||||
struct DataTablesParams {
|
||||
int draw{0};
|
||||
int start{0};
|
||||
int length{10};
|
||||
int orderColumn{-1};
|
||||
std::string orderDir{"asc"};
|
||||
};
|
||||
|
||||
// Parse common DataTables GET params from the request
|
||||
DataTablesParams ParseDataTablesParams(const crow::request& req);
|
||||
|
||||
// Create a DataTables response object
|
||||
crow::json::wvalue CreateDataTablesResponse(int draw, uint32_t recordsTotal, uint32_t recordsFiltered, const crow::json::wvalue::list& data);
|
||||
|
||||
// Rescue character stub (real logic may be project-specific)
|
||||
bool RescueCharacter(const uint64_t characterId, const uint32_t zoneId);
|
||||
|
||||
}
|
||||
248
dDashboardServer/DashboardServer.cpp
Normal file
248
dDashboardServer/DashboardServer.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#ifndef PROJECT_VERSION
|
||||
#define PROJECT_VERSION "dev"
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
//DLU Includes:
|
||||
#include "dCommonVars.h"
|
||||
#include "dServer.h"
|
||||
#include "Logger.h"
|
||||
#include "Database.h"
|
||||
#include "dConfig.h"
|
||||
#include "Diagnostics.h"
|
||||
#include "AssetManager.h"
|
||||
#include "BinaryPathFinder.h"
|
||||
#include "ServiceType.h"
|
||||
#include "StringifiedEnum.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Server.h"
|
||||
|
||||
//RakNet includes:
|
||||
#include "RakNetDefines.h"
|
||||
#include "MessageIdentifiers.h"
|
||||
|
||||
#include "MessageType/Server.h"
|
||||
|
||||
#include "DashboardWeb.h"
|
||||
#include "DashboardShared.h"
|
||||
|
||||
namespace Game {
|
||||
Logger* logger = nullptr;
|
||||
dServer* server = nullptr;
|
||||
dConfig* config = nullptr;
|
||||
AssetManager* assetManager = nullptr;
|
||||
Game::signal_t lastSignal = 0;
|
||||
std::mt19937 randomEngine;
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
void HandlePacket(Packet* packet);
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
constexpr uint32_t dashboardFramerate = mediumFramerate;
|
||||
constexpr uint32_t dashboardFrameDelta = mediumFrameDelta;
|
||||
Diagnostics::SetProcessName("Dashboard");
|
||||
Diagnostics::SetProcessFileName(argv[0]);
|
||||
Diagnostics::Initialize();
|
||||
|
||||
std::signal(SIGINT, Game::OnSignal);
|
||||
std::signal(SIGTERM, Game::OnSignal);
|
||||
|
||||
Game::config = new dConfig("dashboardconfig.ini");
|
||||
|
||||
//Create all the objects we need to run our service:
|
||||
Server::SetupLogger("DashboardServer");
|
||||
if (!Game::logger) return EXIT_FAILURE;
|
||||
Game::config->LogSettings();
|
||||
|
||||
//Read our config:
|
||||
|
||||
LOG("Starting Dashboard server...");
|
||||
LOG("Version: %s", PROJECT_VERSION);
|
||||
LOG("Compiled on: %s", __TIMESTAMP__);
|
||||
|
||||
try {
|
||||
std::string clientPathStr = Game::config->GetValue("client_location");
|
||||
if (clientPathStr.empty()) clientPathStr = "./res";
|
||||
std::filesystem::path clientPath = std::filesystem::path(clientPathStr);
|
||||
if (clientPath.is_relative()) {
|
||||
clientPath = BinaryPathFinder::GetBinaryDir() / clientPath;
|
||||
}
|
||||
|
||||
Game::assetManager = new AssetManager(clientPath);
|
||||
} catch (std::runtime_error& ex) {
|
||||
LOG("Got an error while setting up assets: %s", ex.what());
|
||||
delete Game::logger;
|
||||
delete Game::config;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
//Connect to the Database
|
||||
try {
|
||||
Database::Connect();
|
||||
} catch (std::exception& ex) {
|
||||
LOG("Got an error while connecting to the database: %s", ex.what());
|
||||
Database::Destroy("DashboardServer");
|
||||
delete Game::logger;
|
||||
delete Game::config;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Setup and start the Crow web server (runs in its own thread)
|
||||
const uint32_t web_server_port = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("web_server_port")).value_or(8080);
|
||||
DashboardWeb::Initialize(web_server_port);
|
||||
|
||||
//Find out the master's IP:
|
||||
std::string masterIP;
|
||||
uint32_t masterPort = 1000;
|
||||
std::string masterPassword;
|
||||
auto masterInfo = Database::Get()->GetMasterInfo();
|
||||
if (masterInfo) {
|
||||
masterIP = masterInfo->ip;
|
||||
masterPort = masterInfo->port;
|
||||
masterPassword = masterInfo->password;
|
||||
}
|
||||
|
||||
//It's safe to pass 'localhost' here, as the IP is only used as the external IP.
|
||||
std::string ourIP = "localhost";
|
||||
const uint32_t maxClients = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("max_clients")).value_or(999);
|
||||
const uint32_t ourPort = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("dashboard_server_port")).value_or(2006);
|
||||
const auto externalIPString = Game::config->GetValue("external_ip");
|
||||
if (!externalIPString.empty()) ourIP = externalIPString;
|
||||
|
||||
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServiceType::COMMON, Game::config, &Game::lastSignal, masterPassword);
|
||||
|
||||
// Update shared state with master server info
|
||||
DashboardShared::g_Stats.SetMasterInfo(masterIP, masterPort);
|
||||
|
||||
Game::randomEngine = std::mt19937(time(0));
|
||||
|
||||
//Run it until server gets a kill message from Master:
|
||||
auto t = std::chrono::high_resolution_clock::now();
|
||||
Packet* packet = nullptr;
|
||||
constexpr uint32_t logFlushTime = 30 * dashboardFramerate; // 30 seconds in frames
|
||||
constexpr uint32_t sqlPingTime = 10 * 60 * dashboardFramerate; // 10 minutes in frames
|
||||
uint32_t framesSinceLastFlush = 0;
|
||||
uint32_t framesSinceMasterDisconnect = 0;
|
||||
uint32_t framesSinceLastSQLPing = 0;
|
||||
|
||||
auto lastTime = std::chrono::high_resolution_clock::now();
|
||||
auto startTime = lastTime; // Track server start time for uptime
|
||||
|
||||
Game::logger->Flush(); // once immediately before main loop
|
||||
while (!Game::ShouldShutdown()) {
|
||||
// Check if we're still connected to master:
|
||||
if (!Game::server->GetIsConnectedToMaster()) {
|
||||
framesSinceMasterDisconnect++;
|
||||
|
||||
if (framesSinceMasterDisconnect >= dashboardFramerate)
|
||||
break; //Exit our loop, shut down.
|
||||
|
||||
DashboardShared::SetMasterConnected(false);
|
||||
} else {
|
||||
framesSinceMasterDisconnect = 0;
|
||||
DashboardShared::SetMasterConnected(true);
|
||||
}
|
||||
|
||||
const auto currentTime = std::chrono::high_resolution_clock::now();
|
||||
const float deltaTime = std::chrono::duration<float>(currentTime - lastTime).count();
|
||||
lastTime = currentTime;
|
||||
|
||||
// Check for packets from master:
|
||||
Game::server->ReceiveFromMaster();
|
||||
|
||||
// Process queued packet sends from Crow threads
|
||||
if (DashboardShared::g_PacketQueue.HasPending()) {
|
||||
auto pendingPackets = DashboardShared::g_PacketQueue.DequeueAll();
|
||||
for (const auto& request : pendingPackets) {
|
||||
// Create BitStream from queued data
|
||||
RakNet::BitStream bitStream(const_cast<unsigned char*>(request.data.data()), request.data.size(), false);
|
||||
|
||||
// Send via RakNet (safe - we're in the RakNet thread)
|
||||
Game::server->Send(bitStream, request.target, request.broadcast);
|
||||
DashboardShared::OnPacketSent();
|
||||
|
||||
LOG("Sent queued packet from web request (%zu bytes)", request.data.size());
|
||||
}
|
||||
}
|
||||
|
||||
// Check for RakNet packets:
|
||||
packet = Game::server->Receive();
|
||||
if (packet) {
|
||||
HandlePacket(packet);
|
||||
DashboardShared::OnPacketReceived(); // Update shared stats
|
||||
Game::server->DeallocatePacket(packet);
|
||||
packet = nullptr;
|
||||
}
|
||||
|
||||
//Push our log every 30s:
|
||||
if (framesSinceLastFlush >= logFlushTime) {
|
||||
Game::logger->Flush();
|
||||
framesSinceLastFlush = 0;
|
||||
} else framesSinceLastFlush++;
|
||||
|
||||
//Every 10 min we ping our sql server to keep it alive hopefully:
|
||||
if (framesSinceLastSQLPing >= sqlPingTime) {
|
||||
//Find out the master's IP for absolutely no reason:
|
||||
std::string masterIP;
|
||||
uint32_t masterPort;
|
||||
|
||||
auto masterInfo = Database::Get()->GetMasterInfo();
|
||||
if (masterInfo) {
|
||||
masterIP = masterInfo->ip;
|
||||
masterPort = masterInfo->port;
|
||||
}
|
||||
|
||||
framesSinceLastSQLPing = 0;
|
||||
} else framesSinceLastSQLPing++;
|
||||
|
||||
//Sleep our thread since dashboard can afford to.
|
||||
t += std::chrono::milliseconds(dashboardFrameDelta);
|
||||
std::this_thread::sleep_until(t);
|
||||
}
|
||||
|
||||
// Stop the Crow web server
|
||||
DashboardWeb::Stop();
|
||||
|
||||
//Delete our objects here:
|
||||
Database::Destroy("DashboardServer");
|
||||
delete Game::server;
|
||||
delete Game::logger;
|
||||
delete Game::config;
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
void HandlePacket(Packet* packet) {
|
||||
if (packet->length < 4) return;
|
||||
|
||||
if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) {
|
||||
LOG("A client has disconnected");
|
||||
DashboardShared::OnClientDisconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet->data[0] == ID_NEW_INCOMING_CONNECTION) {
|
||||
LOG("New incoming connection from %s", packet->systemAddress.ToString());
|
||||
DashboardShared::OnClientConnected();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet->data[0] != ID_USER_PACKET_ENUM) return;
|
||||
|
||||
// Handle server packets
|
||||
if (static_cast<ServiceType>(packet->data[1]) == ServiceType::COMMON) {
|
||||
if (static_cast<MessageType::Server>(packet->data[3]) == MessageType::Server::VERSION_CONFIRM) {
|
||||
LOG("Version confirmation received from client");
|
||||
DashboardShared::OnPacketReceived("VERSION_CONFIRM");
|
||||
}
|
||||
}
|
||||
|
||||
// Add more packet handling as needed
|
||||
// This is where you would handle custom dashboard-specific packets
|
||||
// All packet handling can safely update DashboardShared state
|
||||
}
|
||||
187
dDashboardServer/DashboardShared.h
Normal file
187
dDashboardServer/DashboardShared.h
Normal file
@@ -0,0 +1,187 @@
|
||||
#ifndef __DASHBOARDSHARED_H__
|
||||
#define __DASHBOARDSHARED_H__
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <functional>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <ctime>
|
||||
#include <random>
|
||||
#include <optional>
|
||||
#include "dCommonVars.h"
|
||||
#include "RakNetTypes.h"
|
||||
#include "GameDatabase.h"
|
||||
#include "crow.h"
|
||||
|
||||
// Forward declaration
|
||||
class GameDatabase;
|
||||
namespace RakNet {
|
||||
class BitStream;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared state between the Crow web server (runs in background threads)
|
||||
* and the RakNet game loop (runs in main thread).
|
||||
*
|
||||
* All members use thread-safe types (atomic, mutex-protected)
|
||||
*
|
||||
* IMPORTANT: RakNet is NOT thread-safe!
|
||||
* - Crow threads can READ state and QUEUE packet send requests
|
||||
* - Only the RakNet thread (main loop) can actually send packets
|
||||
*/
|
||||
namespace DashboardShared {
|
||||
|
||||
// ===== Atomic Counters (lock-free, safe for simple reads/writes) =====
|
||||
|
||||
inline std::atomic<uint32_t> g_ConnectedClients{0};
|
||||
inline std::atomic<bool> g_ConnectedToMaster{false};
|
||||
inline std::atomic<uint64_t> g_PacketsReceived{0};
|
||||
inline std::atomic<uint64_t> g_PacketsSent{0};
|
||||
|
||||
// ===== Mutex-Protected Data (for complex structures) =====
|
||||
|
||||
struct ServerStats {
|
||||
std::mutex mutex;
|
||||
uint64_t uptime_seconds = 0;
|
||||
std::string last_packet_type;
|
||||
uint32_t raknet_port = 0;
|
||||
std::string master_ip;
|
||||
|
||||
// Thread-safe getters
|
||||
uint64_t GetUptime() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return uptime_seconds;
|
||||
}
|
||||
|
||||
std::string GetLastPacketType() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return last_packet_type;
|
||||
}
|
||||
|
||||
void SetLastPacketType(const std::string& type) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
last_packet_type = type;
|
||||
}
|
||||
|
||||
void SetMasterInfo(const std::string& ip, uint32_t port) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
master_ip = ip;
|
||||
raknet_port = port;
|
||||
}
|
||||
};
|
||||
|
||||
inline ServerStats g_Stats;
|
||||
|
||||
// ===== Packet Send Queue (for Crow -> RakNet communication) =====
|
||||
|
||||
/**
|
||||
* Represents a packet send request from Crow to RakNet.
|
||||
* Crow threads add to the queue, RakNet thread processes them.
|
||||
*/
|
||||
struct PacketSendRequest {
|
||||
std::vector<uint8_t> data; // Packet data (owns the memory)
|
||||
SystemAddress target; // Target address (or UNASSIGNED for broadcast)
|
||||
bool broadcast; // Whether to broadcast
|
||||
|
||||
PacketSendRequest(const std::vector<uint8_t>& packetData,
|
||||
const SystemAddress& addr,
|
||||
bool isBroadcast)
|
||||
: data(packetData), target(addr), broadcast(isBroadcast) {}
|
||||
};
|
||||
|
||||
// Thread-safe queue of packet send requests
|
||||
struct PacketQueue {
|
||||
std::mutex mutex;
|
||||
std::queue<PacketSendRequest> queue;
|
||||
|
||||
// Called from Crow threads to queue a packet for sending
|
||||
void Enqueue(const std::vector<uint8_t>& data, const SystemAddress& addr, bool broadcast) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
queue.emplace(data, addr, broadcast);
|
||||
}
|
||||
|
||||
// Called from RakNet thread to get all pending packets
|
||||
std::vector<PacketSendRequest> DequeueAll() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
std::vector<PacketSendRequest> result;
|
||||
while (!queue.empty()) {
|
||||
result.push_back(std::move(queue.front()));
|
||||
queue.pop();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if queue has pending packets
|
||||
bool HasPending() {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
return !queue.empty();
|
||||
}
|
||||
};
|
||||
|
||||
inline PacketQueue g_PacketQueue;
|
||||
|
||||
// ===== Helper Functions =====
|
||||
|
||||
// Called from RakNet thread when a client connects
|
||||
inline void OnClientConnected() {
|
||||
g_ConnectedClients++;
|
||||
}
|
||||
|
||||
// Called from RakNet thread when a client disconnects
|
||||
inline void OnClientDisconnected() {
|
||||
if (g_ConnectedClients > 0) {
|
||||
g_ConnectedClients--;
|
||||
}
|
||||
}
|
||||
|
||||
// Called from RakNet thread when master connection status changes
|
||||
inline void SetMasterConnected(bool connected) {
|
||||
g_ConnectedToMaster = connected;
|
||||
}
|
||||
|
||||
// Called from RakNet thread when a packet is processed
|
||||
inline void OnPacketReceived(const std::string& packetType = "") {
|
||||
g_PacketsReceived++;
|
||||
if (!packetType.empty()) {
|
||||
g_Stats.SetLastPacketType(packetType);
|
||||
}
|
||||
}
|
||||
|
||||
// Called from RakNet thread when a packet is sent
|
||||
inline void OnPacketSent() {
|
||||
g_PacketsSent++;
|
||||
}
|
||||
|
||||
// ===== Crow -> RakNet Communication =====
|
||||
|
||||
/**
|
||||
* Queue a RakNet packet to be sent (called from Crow threads).
|
||||
* The packet will be sent on the next RakNet thread update.
|
||||
*
|
||||
* @param data Packet data to send
|
||||
* @param target Target system address (use UNASSIGNED_SYSTEM_ADDRESS for broadcast)
|
||||
* @param broadcast Whether to broadcast to all connected clients
|
||||
*/
|
||||
inline void QueuePacketSend(const std::vector<uint8_t>& data,
|
||||
const SystemAddress& target = UNASSIGNED_SYSTEM_ADDRESS,
|
||||
bool broadcast = false) {
|
||||
g_PacketQueue.Enqueue(data, target, broadcast);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to queue a BitStream for sending (called from Crow threads).
|
||||
* Converts BitStream to raw data and queues it.
|
||||
*/
|
||||
inline void QueueBitStreamSend(RakNet::BitStream& bitStream,
|
||||
const SystemAddress& target = UNASSIGNED_SYSTEM_ADDRESS,
|
||||
bool broadcast = false) {
|
||||
std::vector<uint8_t> data(bitStream.GetData(),
|
||||
bitStream.GetData() + bitStream.GetNumberOfBytesUsed());
|
||||
QueuePacketSend(data, target, broadcast);
|
||||
}
|
||||
}
|
||||
#endif // __DASHBOARDSHARED_H__
|
||||
153
dDashboardServer/DashboardWeb.cpp
Normal file
153
dDashboardServer/DashboardWeb.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "DashboardWeb.h"
|
||||
#include "DashboardShared.h"
|
||||
|
||||
// Blueprint includes
|
||||
#include "blueprints/AuthBlueprint.h"
|
||||
#include "blueprints/ApiBlueprint.h"
|
||||
#include "blueprints/PageBlueprint.h"
|
||||
#include "blueprints/PlayKeysBlueprint.h"
|
||||
#include "blueprints/CharactersBlueprint.h"
|
||||
#include "blueprints/MailBlueprint.h"
|
||||
#include "blueprints/BugReportsBlueprint.h"
|
||||
#include "blueprints/ModerationBlueprint.h"
|
||||
|
||||
// Crow headers - must come before ASIO to avoid conflicts
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
// thanks bill gates
|
||||
#ifdef _WIN32
|
||||
#undef min
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
namespace DashboardWeb {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
|
||||
static crow::App<crow::CookieParser, Session> g_App {
|
||||
Session{
|
||||
// cookie config: use "session" cookie name, 24h max_age
|
||||
crow::CookieParser::Cookie("session").max_age(24 * 60 * 60).path("/"),
|
||||
// session id length
|
||||
32,
|
||||
// storage backend (InMemoryStore)
|
||||
crow::InMemoryStore{}
|
||||
}
|
||||
};
|
||||
|
||||
static std::future<void> g_ServerFuture;
|
||||
static bool g_Running = false;
|
||||
static bool g_Initialized = false;
|
||||
|
||||
void SetupRoutes() {
|
||||
static bool setupCalled = false;
|
||||
if (setupCalled) {
|
||||
std::cerr << "WARNING: SetupRoutes() called multiple times!" << std::endl;
|
||||
return;
|
||||
}
|
||||
setupCalled = true;
|
||||
|
||||
std::cerr << "Setting up dashboard routes..." << std::endl;
|
||||
|
||||
// Set mustache template base directory
|
||||
crow::mustache::set_base("./templates");
|
||||
|
||||
// Setup all blueprint routes
|
||||
try {
|
||||
std::cerr << " - Setting up AuthBlueprint..." << std::endl;
|
||||
AuthBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << " - Setting up ApiBlueprint..." << std::endl;
|
||||
ApiBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << " - Setting up PageBlueprint..." << std::endl;
|
||||
PageBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << " - Setting up PlayKeysBlueprint..." << std::endl;
|
||||
PlayKeysBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << " - Setting up CharactersBlueprint..." << std::endl;
|
||||
CharactersBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << " - Setting up MailBlueprint..." << std::endl;
|
||||
MailBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << " - Setting up BugReportsBlueprint..." << std::endl;
|
||||
BugReportsBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << " - Setting up ModerationBlueprint..." << std::endl;
|
||||
ModerationBlueprint::Setup(g_App);
|
||||
|
||||
std::cerr << "All routes set up successfully!" << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
// Print to stderr since LOG might not be available
|
||||
std::cerr << "Error setting up routes: " << e.what() << std::endl;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize(uint32_t port) {
|
||||
// Only allow initialization once per process lifetime
|
||||
// Crow apps cannot be restarted once stopped
|
||||
if (g_Initialized) {
|
||||
std::cerr << "Dashboard web server already initialized. Cannot reinitialize." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Setup routes (only happens once)
|
||||
SetupRoutes();
|
||||
|
||||
// Configure Crow app
|
||||
g_App.loglevel(crow::LogLevel::Info); // Changed to Info to see startup messages
|
||||
|
||||
// Start the server in a separate thread
|
||||
g_ServerFuture = std::async(std::launch::async, [port]() {
|
||||
try {
|
||||
g_App.port(port).multithreaded().run();
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error running Crow server: " << e.what() << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
g_Running = true;
|
||||
g_Initialized = true;
|
||||
|
||||
// Give the server a moment to start
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error initializing dashboard web server: " << e.what() << std::endl;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void Update() {
|
||||
// Crow runs in its own thread, nothing to update here
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
if (!g_Running) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_App.stop();
|
||||
|
||||
// Wait for the server thread to finish (with timeout)
|
||||
if (g_ServerFuture.valid()) {
|
||||
auto status = g_ServerFuture.wait_for(std::chrono::seconds(5));
|
||||
if (status == std::future_status::timeout) {
|
||||
std::cerr << "Warning: Dashboard web server did not stop gracefully" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
g_Running = false;
|
||||
}
|
||||
|
||||
} // namespace DashboardWeb
|
||||
19
dDashboardServer/DashboardWeb.h
Normal file
19
dDashboardServer/DashboardWeb.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef __DASHBOARDWEB_H__
|
||||
#define __DASHBOARDWEB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace DashboardWeb {
|
||||
|
||||
// Initialize the web server and configure routes using blueprints
|
||||
void Initialize(uint32_t port);
|
||||
|
||||
// Process pending web requests (call each frame/tick)
|
||||
void Update();
|
||||
|
||||
// Stop the web server
|
||||
void Stop();
|
||||
};
|
||||
|
||||
#endif // __DASHBOARDWEB_H__
|
||||
143
dDashboardServer/better-templates/base.mustache
Normal file
143
dDashboardServer/better-templates/base.mustache
Normal file
@@ -0,0 +1,143 @@
|
||||
<!doctype html>
|
||||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
|
||||
<!-- Title -->
|
||||
<title>{{#title}}{{title}}{{/title}}{{^title}}Dashboard{{/title}} - {{config.APP_NAME}}</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
{{! CSS }}
|
||||
<style>
|
||||
.required:after {
|
||||
content:" *";
|
||||
color: red;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
|
||||
<!-- DataTables CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<!-- Custom CSS consolidated -->
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
|
||||
</head>
|
||||
<body class="bg-dark text-white">
|
||||
|
||||
{{> header}}
|
||||
|
||||
<!-- Content -->
|
||||
|
||||
<div class="container py-0">
|
||||
|
||||
<!-- Text -->
|
||||
<div class="text-center">
|
||||
<span class="h3 mb-0"><br/>{{content_before}}<br/><br/></span>
|
||||
</div>
|
||||
|
||||
<!-- Flashed messages: expect `messages` to be an array of {category, message} -->
|
||||
{! TODO: make this dynamic toasts !!}
|
||||
{{#messages}}
|
||||
<div class="alert alert-{{category}}" role="alert">
|
||||
{{message}}
|
||||
</div>
|
||||
{{/messages}}
|
||||
|
||||
</div>
|
||||
|
||||
<div class='container mt-4'>
|
||||
{{content}}
|
||||
</div>
|
||||
|
||||
<div class='container mt-4'>
|
||||
{{content_after}}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
{{#footer}}
|
||||
<hr class="my-5"/>
|
||||
{{/footer}}
|
||||
</footer>
|
||||
|
||||
{{! JS assets }}
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- jQuery (optional fallback for older scripts) -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
|
||||
<!-- DataTables JS -->
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<!-- Shared helper: wait for jQuery/DataTables (keeps pages resilient to CDN timing) -->
|
||||
<script src="/static/js/wait-for-jq-dt.js"></script>
|
||||
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
<script src="/static/js/login.js"></script>
|
||||
<script>
|
||||
// set the active nav-link item (use vanilla JS, fallback to jQuery)
|
||||
(function(){
|
||||
var endpoint = '{{request_endpoint}}' || '';
|
||||
try{
|
||||
var target_nav = '#' + endpoint.replace(/\./g, '-');
|
||||
var el = document.querySelector(target_nav);
|
||||
if(el) el.classList.add('active');
|
||||
else if(window.jQuery) $(target_nav).addClass('active');
|
||||
}catch(e){}
|
||||
})();
|
||||
|
||||
// initialize Bootstrap 5 tooltips (no jQuery required)
|
||||
(function(){
|
||||
try{
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}catch(e){
|
||||
// fallback for legacy attribute name if still used
|
||||
// legacy jQuery tooltip fallback (only runs if bootstrap init failed and jQuery tooltip is present)
|
||||
if(window.jQuery && window.jQuery.fn && window.jQuery.fn.tooltip) $(function(){ $('[data-toggle="tooltip"]').tooltip(); });
|
||||
}
|
||||
})();
|
||||
|
||||
function setInnerHTML(elm, html) {
|
||||
elm.innerHTML = html;
|
||||
// re-init Bootstrap tooltips inside newly injected content
|
||||
try{
|
||||
var tooltipTriggerList = [].slice.call(elm.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}catch(e){
|
||||
if(window.jQuery && window.jQuery.fn && window.jQuery.fn.tooltip) $("body").tooltip({ selector: '[data-toggle=tooltip]' });
|
||||
}
|
||||
Array.from(elm.querySelectorAll("script")).forEach(function(oldScriptEl) {
|
||||
var newScriptEl = document.createElement("script");
|
||||
Array.from(oldScriptEl.attributes).forEach(function(attr) {
|
||||
newScriptEl.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
var scriptText = document.createTextNode(oldScriptEl.innerHTML || '');
|
||||
newScriptEl.appendChild(scriptText);
|
||||
oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
113
dDashboardServer/better-templates/header.mustache
Normal file
113
dDashboardServer/better-templates/header.mustache
Normal file
@@ -0,0 +1,113 @@
|
||||
{{! Navigation brand, nav toggle bar }}
|
||||
<nav class='navbar navbar-expand-sm navbar-dark bg-primary flex-row pb-3'>
|
||||
<div class='container md-0 flex-nowrap'>
|
||||
|
||||
{{! Logo and App Name }}
|
||||
<nav class="navbar">
|
||||
<a class="navbar-brand" href="{{url.main_index}}">
|
||||
<img src="{{static.logo}}" width="30" height="30" class="d-inline-block align-top" alt="">
|
||||
{{config.APP_NAME}}
|
||||
</a>
|
||||
</nav>
|
||||
{{! Navigation brand, nav toggle bar }}
|
||||
<nav class='navbar navbar-expand-sm navbar-dark bg-primary flex-row pb-3'>
|
||||
<div class='container md-0 flex-nowrap'>
|
||||
|
||||
{{! Logo and App Name }}
|
||||
<nav class="navbar">
|
||||
<a class="navbar-brand" href="{{url.main_index}}">
|
||||
<img src="{{static.logo}}" width="30" height="30" class="d-inline-block align-top" alt="">
|
||||
{{config.APP_NAME}}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
|
||||
{{! Visible only on large devices }}
|
||||
<nav class='navbar-nav'>
|
||||
<div class='collapse navbar-collapse'>
|
||||
{{#current_user_authenticated}}
|
||||
{{#USER_ENABLE_INVITE_USER}}
|
||||
<a class='btn-nav-dashboard me-2' href='{{url.user_invite_user}}'>Invite</a>
|
||||
{{/USER_ENABLE_INVITE_USER}}
|
||||
<a class='btn-nav-dashboard' href='{{url.user_logout}}'><i class='fas fa-sign-out-alt me-1'></i>Logout</a>
|
||||
{{/current_user_authenticated}}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<button class='navbar-toggler' type='button' data-bs-toggle='collapse' data-bs-target='#navbarSupportedContent' aria-controls='navbarSupportedContent' aria-expanded='false' aria-label='Toggle navigation'>
|
||||
<span class='navbar-toggler-icon'></span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{! Navigation menu / links bar }}
|
||||
<nav class='navbar navbar-expand-sm navbar-dark bg-primary p-sm-0 py-0 {{#navbar_shadow}}shadow-sm{{/navbar_shadow}}'>
|
||||
<div class='container mt-0 pt-0'>
|
||||
<div class='collapse navbar-collapse' id='navbarSupportedContent' style='margin-top: -16px;'>
|
||||
<nav class='navbar-nav me-auto'>
|
||||
<a id='main-index' class='nav-link' href='{{url.main_index}}'>Home</a>
|
||||
|
||||
{{#gm_ge_3}}
|
||||
{{! General Moderation Links }}
|
||||
<a id='accounts-index' class='nav-link' href='{{url.accounts_index}}'>Accounts</a>
|
||||
<a id='character-index' class='nav-link' href='{{url.characters_index}}'>Characters</a>
|
||||
<a id='property-index' class='nav-link' href='{{url.properties_index}}'>Properties</a>
|
||||
{{/gm_ge_3}}
|
||||
|
||||
{{#gm_ge_5_require_play_key}}
|
||||
{{! Play Keys }}
|
||||
<a id='play_keys-index' class='nav-link' href='{{url.play_keys_index}}'>Play Keys</a>
|
||||
{{/gm_ge_5_require_play_key}}
|
||||
|
||||
{{#gm_ge_2}}
|
||||
<a id='report-index' class='nav-link' href='{{url.reports_index}}'>Reports</a>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tools</a>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
<a class="dropdown-item text-center" href='{{url.mail_send}}'>Send Mail</a>
|
||||
<hr/>
|
||||
<h3 class="text-center">Moderation</h3>
|
||||
<a class="dropdown-item text-center" href='{{url.moderation_unapproved}}'>Unapproved Items</a>
|
||||
<a class="dropdown-item text-center" href='{{url.moderation_approved}}'>Approved Items</a>
|
||||
<a class="dropdown-item text-center" href='{{url.moderation_all}}'>All Items</a>
|
||||
<hr/>
|
||||
<h3 class="text-center">Bug Reports</h3>
|
||||
<a class="dropdown-item text-center" href='{{url.bug_reports_unresolved}}'>Unresolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{url.bug_reports_resolved}}'>Resolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{url.bug_reports_all}}'>All Reports</a>
|
||||
{{#gm_ge_8}}
|
||||
<hr/>
|
||||
<h3 class="text-center">Logs</h3>
|
||||
<a class="dropdown-item text-center" href='{{url.log_command}}'>Command Log</a>
|
||||
<a class="dropdown-item text-center" href='{{url.log_activity}}'>Activity Log</a>
|
||||
<a class="dropdown-item text-center" href='{{url.log_audit}}'>Audit Log</a>
|
||||
<a class="dropdown-item text-center" href='{{url.log_system}}'>System Log</a>
|
||||
{{/gm_ge_8}}
|
||||
</div>
|
||||
</li>
|
||||
{{/gm_ge_2}}
|
||||
|
||||
{{#gm_eq_0}}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Bug Reports</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item text-center" href='{{url.bug_reports_unresolved}}'>Unresolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{url.bug_reports_resolved}}'>Resolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{url.bug_reports_all}}'>All Reports</a>
|
||||
</div>
|
||||
</li>
|
||||
{{/gm_eq_0}}
|
||||
|
||||
{{#current_user_authenticated}}
|
||||
<a id='main-about' class='nav-link' href='{{url.main_about}}'>About</a>
|
||||
{{/current_user_authenticated}}
|
||||
|
||||
{{#current_user_authenticated}}
|
||||
<a class='nav-link d-sm-none' href='{{url.user_logout}}'><i class='fas fa-sign-out-alt me-1'></i>Logout</a>
|
||||
{{/current_user_authenticated}}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
1344
dDashboardServer/blueprints/ApiBlueprint.cpp
Normal file
1344
dDashboardServer/blueprints/ApiBlueprint.cpp
Normal file
File diff suppressed because it is too large
Load Diff
17
dDashboardServer/blueprints/ApiBlueprint.h
Normal file
17
dDashboardServer/blueprints/ApiBlueprint.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace ApiBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup API routes
|
||||
* Registers all API endpoints for stats, accounts, and moderation
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace ApiBlueprint
|
||||
129
dDashboardServer/blueprints/AuthBlueprint.cpp
Normal file
129
dDashboardServer/blueprints/AuthBlueprint.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "AuthBlueprint.h"
|
||||
#include "Database.h"
|
||||
#include <bcrypt/BCrypt.hpp>
|
||||
|
||||
namespace AuthBlueprint {
|
||||
|
||||
void Setup(DashboardApp& app) {
|
||||
// Login route
|
||||
CROW_ROUTE(app, "/api/login")
|
||||
.methods("POST"_method)
|
||||
([&](crow::request& req, crow::response& res) {
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
res.code = 400;
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write("{\"error\": \"Invalid JSON\"}");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string username = body["username"].s();
|
||||
std::string password = body["password"].s();
|
||||
|
||||
if (username.empty() || password.empty()) {
|
||||
res.code = 400;
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write("{\"error\": \"Username and password required\"}");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get account info from database
|
||||
auto accountInfo = Database::Get()->GetAccountInfo(username);
|
||||
if (!accountInfo) {
|
||||
res.code = 401;
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write("{\"error\": \"Invalid credentials\"}");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify password using bcrypt
|
||||
if (!BCrypt::validatePassword(password, accountInfo->bcryptPassword)) {
|
||||
res.code = 401;
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write("{\"error\": \"Invalid credentials\"}");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if account is banned or locked
|
||||
if (accountInfo->banned) {
|
||||
res.code = 403;
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write("{\"error\": \"Account is banned\"}");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (accountInfo->locked) {
|
||||
res.code = 403;
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write("{\"error\": \"Account is locked\"}");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create session
|
||||
auto& session = app.get_context<Session>(req);
|
||||
session.set("username", username);
|
||||
session.set("account_id", static_cast<int>(accountInfo->id));
|
||||
session.set("gm_level", static_cast<int>(accountInfo->maxGmLevel));
|
||||
|
||||
// Return success with user info
|
||||
crow::json::wvalue response;
|
||||
response["success"] = true;
|
||||
response["username"] = username;
|
||||
response["account_id"] = accountInfo->id;
|
||||
response["gm_level"] = static_cast<uint8_t>(accountInfo->maxGmLevel);
|
||||
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write(response.dump());
|
||||
res.end();
|
||||
});
|
||||
|
||||
// Logout route
|
||||
CROW_ROUTE(app, "/api/logout")
|
||||
.methods("POST"_method)
|
||||
([&](crow::request& req, crow::response& res) {
|
||||
auto& session = app.get_context<Session>(req);
|
||||
|
||||
// Clear session
|
||||
session.remove("username");
|
||||
session.remove("account_id");
|
||||
session.remove("gm_level");
|
||||
|
||||
crow::json::wvalue response;
|
||||
response["success"] = true;
|
||||
|
||||
res.set_header("Content-Type", "application/json");
|
||||
res.write(response.dump());
|
||||
res.end();
|
||||
});
|
||||
|
||||
// Auth status route
|
||||
CROW_ROUTE(app, "/api/auth/status")
|
||||
([&](const crow::request& req) {
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
if (!username.empty()) {
|
||||
int account_id = session.template get<int>("account_id", -1);
|
||||
int gm_level = session.template get<int>("gm_level", -1);
|
||||
|
||||
response["authenticated"] = true;
|
||||
response["username"] = username;
|
||||
response["account_id"] = account_id;
|
||||
response["gm_level"] = gm_level;
|
||||
} else {
|
||||
response["authenticated"] = false;
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace AuthBlueprint
|
||||
17
dDashboardServer/blueprints/AuthBlueprint.h
Normal file
17
dDashboardServer/blueprints/AuthBlueprint.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace AuthBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup authentication routes
|
||||
* Registers login, logout, and auth status endpoints
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace AuthBlueprint
|
||||
234
dDashboardServer/blueprints/BugReportsBlueprint.cpp
Normal file
234
dDashboardServer/blueprints/BugReportsBlueprint.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
#include "BugReportsBlueprint.h"
|
||||
#include "Database.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "Logger.h"
|
||||
#include <ctime>
|
||||
|
||||
namespace BugReportsBlueprint {
|
||||
|
||||
// Helper function to get current user's account info from session
|
||||
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
if (username.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Database::Get()->GetAccountInfo(username);
|
||||
}
|
||||
|
||||
// Helper function to get user's GM level
|
||||
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return eGameMasterLevel::CIVILIAN;
|
||||
}
|
||||
return user->maxGmLevel;
|
||||
}
|
||||
|
||||
// Helper function to check if user has minimum GM level
|
||||
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
|
||||
auto level = GetUserGMLevel(req, app);
|
||||
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
|
||||
}
|
||||
|
||||
void Setup(DashboardApp& app) {
|
||||
// Get all bug reports (filtered by status)
|
||||
CROW_ROUTE(app, "/api/bugreports")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req) {
|
||||
// Anyone authenticated can view their own bug reports
|
||||
// GMs can view all
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return crow::response(401, "{\"error\": \"Not authenticated\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
crow::json::wvalue::list data;
|
||||
|
||||
try {
|
||||
auto statusParam = req.url_params.get("status");
|
||||
std::string status = statusParam ? statusParam : "all";
|
||||
|
||||
std::vector<IBugReports::DetailedInfo> reports;
|
||||
|
||||
if (status == "resolved") {
|
||||
reports = Database::Get()->GetResolvedBugReports();
|
||||
} else if (status == "unresolved") {
|
||||
reports = Database::Get()->GetUnresolvedBugReports();
|
||||
} else {
|
||||
reports = Database::Get()->GetAllBugReports();
|
||||
}
|
||||
|
||||
bool isGM = static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR);
|
||||
|
||||
for (const auto& report : reports) {
|
||||
// If not a GM, only show reports from user's own characters
|
||||
if (!isGM) {
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(report.characterId);
|
||||
if (!charInfo || charInfo->accountId != user->id) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
crow::json::wvalue item;
|
||||
item["id"] = report.id;
|
||||
item["body"] = report.body;
|
||||
item["client_version"] = report.clientVersion;
|
||||
item["other_player"] = report.otherPlayer;
|
||||
item["selection"] = report.selection;
|
||||
item["character_id"] = static_cast<uint64_t>(report.characterId);
|
||||
item["submitted"] = report.submitted;
|
||||
item["resolved_time"] = report.resolved_time;
|
||||
item["resolved_by_id"] = report.resolved_by_id;
|
||||
item["resolution"] = report.resolution;
|
||||
|
||||
// Get character name
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(report.characterId);
|
||||
if (charInfo) {
|
||||
item["character_name"] = charInfo->name;
|
||||
} else {
|
||||
item["character_name"] = "Unknown";
|
||||
}
|
||||
|
||||
data.push_back(std::move(item));
|
||||
}
|
||||
|
||||
response["data"] = std::move(data);
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["error"] = ex.what();
|
||||
return crow::response(500, response);
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Get a single bug report by ID
|
||||
CROW_ROUTE(app, "/api/bugreports/<uint>")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req, uint64_t report_id) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return crow::response(401, "{\"error\": \"Not authenticated\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto report = Database::Get()->GetBugReportById(report_id);
|
||||
if (!report) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Bug report not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
// Check access rights
|
||||
bool canAccess = false;
|
||||
if (static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR)) {
|
||||
canAccess = true;
|
||||
} else {
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(report->characterId);
|
||||
if (charInfo && charInfo->accountId == user->id) {
|
||||
canAccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!canAccess) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Access denied";
|
||||
return crow::response(403, response);
|
||||
}
|
||||
|
||||
response["success"] = true;
|
||||
response["id"] = report->id;
|
||||
response["body"] = report->body;
|
||||
response["client_version"] = report->clientVersion;
|
||||
response["other_player"] = report->otherPlayer;
|
||||
response["selection"] = report->selection;
|
||||
response["character_id"] = static_cast<uint64_t>(report->characterId);
|
||||
response["submitted"] = report->submitted;
|
||||
response["resolved_time"] = report->resolved_time;
|
||||
response["resolved_by_id"] = report->resolved_by_id;
|
||||
response["resolution"] = report->resolution;
|
||||
|
||||
// Get character name
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(report->characterId);
|
||||
if (charInfo) {
|
||||
response["character_name"] = charInfo->name;
|
||||
}
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Resolve a bug report
|
||||
CROW_ROUTE(app, "/api/bugreports/<uint>/resolve")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t report_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Not authenticated";
|
||||
return crow::response(401, response);
|
||||
}
|
||||
|
||||
std::string resolution;
|
||||
if (body.has("resolution"))
|
||||
resolution = std::string(body["resolution"].s());
|
||||
else
|
||||
resolution = "";
|
||||
|
||||
if (resolution.empty()) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Resolution message is required";
|
||||
return crow::response(response);
|
||||
}
|
||||
|
||||
// Check if report exists and is not already resolved
|
||||
auto report = Database::Get()->GetBugReportById(report_id);
|
||||
if (!report) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Bug report not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
if (report->resolved_time > 0) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Bug report already resolved";
|
||||
return crow::response(response);
|
||||
}
|
||||
|
||||
Database::Get()->ResolveBugReport(report_id, user->id, resolution);
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Bug report resolved successfully";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace BugReportsBlueprint
|
||||
20
dDashboardServer/blueprints/BugReportsBlueprint.h
Normal file
20
dDashboardServer/blueprints/BugReportsBlueprint.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef __BUGREPORTSBLUEPRINT_H__
|
||||
#define __BUGREPORTSBLUEPRINT_H__
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace BugReportsBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup bug reports management routes
|
||||
* Registers routes for viewing and resolving bug reports
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace BugReportsBlueprint
|
||||
|
||||
#endif // __BUGREPORTSBLUEPRINT_H__
|
||||
14
dDashboardServer/blueprints/CMakeLists.txt
Normal file
14
dDashboardServer/blueprints/CMakeLists.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
set(DDASHBOARDSERVER_BLUEPRINTS
|
||||
"AuthBlueprint.cpp"
|
||||
"ApiBlueprint.cpp"
|
||||
"PageBlueprint.cpp"
|
||||
"PlayKeysBlueprint.cpp"
|
||||
"CharactersBlueprint.cpp"
|
||||
"MailBlueprint.cpp"
|
||||
"BugReportsBlueprint.cpp"
|
||||
"ModerationBlueprint.cpp"
|
||||
)
|
||||
|
||||
foreach(file ${DDASHBOARDSERVER_BLUEPRINTS})
|
||||
set(DDASHBOARDSERVER_BLUEPRINTS_SOURCES ${DDASHBOARDSERVER_BLUEPRINTS_SOURCES} "blueprints/${file}" PARENT_SCOPE)
|
||||
endforeach()
|
||||
263
dDashboardServer/blueprints/CharactersBlueprint.cpp
Normal file
263
dDashboardServer/blueprints/CharactersBlueprint.cpp
Normal file
@@ -0,0 +1,263 @@
|
||||
#include "CharactersBlueprint.h"
|
||||
#include "Database.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "ePermissionMap.h"
|
||||
#include "Logger.h"
|
||||
|
||||
namespace CharactersBlueprint {
|
||||
|
||||
// Helper function to get current user's account info from session
|
||||
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
if (username.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Database::Get()->GetAccountInfo(username);
|
||||
}
|
||||
|
||||
// Helper function to get user's GM level
|
||||
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return eGameMasterLevel::CIVILIAN;
|
||||
}
|
||||
return user->maxGmLevel;
|
||||
}
|
||||
|
||||
// Helper function to check if user has minimum GM level
|
||||
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
|
||||
auto level = GetUserGMLevel(req, app);
|
||||
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
|
||||
}
|
||||
|
||||
// Helper to check if user can access a character (owns it or is GM 3+)
|
||||
bool CanAccessCharacter(const crow::request& req, DashboardApp& app, LWOOBJID characterId) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) return false;
|
||||
|
||||
// GMs can access any character
|
||||
if (static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user owns this character
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(characterId);
|
||||
if (charInfo && charInfo->accountId == user->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Setup(DashboardApp& app) {
|
||||
// Get character by ID
|
||||
CROW_ROUTE(app, "/api/characters/<uint>")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req, uint64_t character_id) {
|
||||
if (!CanAccessCharacter(req, app, character_id)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(character_id);
|
||||
if (!charInfo) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Character not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
response["success"] = true;
|
||||
response["id"] = static_cast<uint64_t>(charInfo->id);
|
||||
response["name"] = charInfo->name;
|
||||
response["pending_name"] = charInfo->pendingName;
|
||||
response["account_id"] = charInfo->accountId;
|
||||
response["needs_rename"] = charInfo->needsRename;
|
||||
response["clone_id"] = static_cast<uint64_t>(charInfo->cloneId);
|
||||
response["permission_map"] = static_cast<uint64_t>(charInfo->permissionMap);
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Get character XML
|
||||
CROW_ROUTE(app, "/api/characters/<uint>/xml")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req, uint64_t character_id) {
|
||||
if (!CanAccessCharacter(req, app, character_id)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
try {
|
||||
auto xml = Database::Get()->GetCharacterXml(character_id);
|
||||
|
||||
auto res = crow::response(xml);
|
||||
res.set_header("Content-Type", "application/xml");
|
||||
res.set_header("Content-Disposition", "attachment; filename=\"character_" + std::to_string(character_id) + ".xml\"");
|
||||
return res;
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
crow::json::wvalue response;
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
return crow::response(500, response);
|
||||
}
|
||||
});
|
||||
|
||||
// Rescue character (teleport to safe zone)
|
||||
CROW_ROUTE(app, "/api/characters/<uint>/rescue")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t character_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
uint32_t zoneId = 1200; // Default to Avant Gardens
|
||||
if (body.has("zone_id")) {
|
||||
zoneId = body["zone_id"].i();
|
||||
}
|
||||
|
||||
// RescueCharacter logic removed; this server does not perform live rescues.
|
||||
// Return not-implemented to indicate the operation must be performed via the chat server.
|
||||
response["success"] = false;
|
||||
response["error"] = "Rescue character not implemented on this server. Use chat server tools.";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Toggle character restrictions (trade, mail, chat)
|
||||
CROW_ROUTE(app, "/api/characters/<uint>/restrict/<int>")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t character_id, int restriction_bit) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(character_id);
|
||||
if (!charInfo) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Character not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
// Toggle the restriction bit
|
||||
uint64_t currentPerms = static_cast<uint64_t>(charInfo->permissionMap);
|
||||
uint64_t newPerms = currentPerms ^ (1ULL << restriction_bit);
|
||||
|
||||
Database::Get()->UpdateCharacterPermissions(character_id, static_cast<ePermissionMap>(newPerms));
|
||||
|
||||
response["success"] = true;
|
||||
response["permission_map"] = newPerms;
|
||||
response["message"] = "Character restrictions updated";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Force character rename
|
||||
CROW_ROUTE(app, "/api/characters/<uint>/force-rename")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t character_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(character_id);
|
||||
if (!charInfo) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Character not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
Database::Get()->SetCharacterNeedsRename(character_id, true);
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Character will be forced to rename on next login";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Set character name (admin override)
|
||||
CROW_ROUTE(app, "/api/characters/<uint>/set-name")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t character_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
std::string newName = body["name"].s();
|
||||
|
||||
if (newName.empty() || newName.length() > 33) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Invalid name length (must be 1-33 characters)";
|
||||
return crow::response(response);
|
||||
}
|
||||
|
||||
// Check if name is already in use
|
||||
if (Database::Get()->IsNameInUse(newName)) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Name is already in use";
|
||||
return crow::response(response);
|
||||
}
|
||||
|
||||
Database::Get()->SetCharacterName(character_id, newName);
|
||||
Database::Get()->SetPendingCharacterName(character_id, "");
|
||||
Database::Get()->SetCharacterNeedsRename(character_id, false);
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Character name updated successfully";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace CharactersBlueprint
|
||||
20
dDashboardServer/blueprints/CharactersBlueprint.h
Normal file
20
dDashboardServer/blueprints/CharactersBlueprint.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef __CHARACTERSBLUEPRINT_H__
|
||||
#define __CHARACTERSBLUEPRINT_H__
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace CharactersBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup character management routes
|
||||
* Registers routes for viewing, editing, and managing characters
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace CharactersBlueprint
|
||||
|
||||
#endif // __CHARACTERSBLUEPRINT_H__
|
||||
207
dDashboardServer/blueprints/MailBlueprint.cpp
Normal file
207
dDashboardServer/blueprints/MailBlueprint.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
#include "MailBlueprint.h"
|
||||
#include "Database.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "MailInfo.h"
|
||||
#include "Logger.h"
|
||||
#include <ctime>
|
||||
|
||||
namespace MailBlueprint {
|
||||
|
||||
// Helper function to get current user's account info from session
|
||||
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
if (username.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Database::Get()->GetAccountInfo(username);
|
||||
}
|
||||
|
||||
// Helper function to get user's GM level
|
||||
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return eGameMasterLevel::CIVILIAN;
|
||||
}
|
||||
return user->maxGmLevel;
|
||||
}
|
||||
|
||||
// Helper function to check if user has minimum GM level
|
||||
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
|
||||
auto level = GetUserGMLevel(req, app);
|
||||
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
|
||||
}
|
||||
|
||||
void Setup(DashboardApp& app) {
|
||||
// Send mail to a character or all characters
|
||||
CROW_ROUTE(app, "/api/mail/send")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Not authenticated";
|
||||
return crow::response(401, response);
|
||||
}
|
||||
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
// Get mail parameters
|
||||
std::string subject;
|
||||
if (body.has("subject"))
|
||||
subject = std::string(body["subject"].s());
|
||||
else
|
||||
subject = "";
|
||||
|
||||
std::string message;
|
||||
if (body.has("body"))
|
||||
message = std::string(body["body"].s());
|
||||
else
|
||||
message = "";
|
||||
int64_t recipientId = body.has("recipient_id") ? body["recipient_id"].i() : 0;
|
||||
bool sendToAll = body.has("send_to_all") ? body["send_to_all"].b() : false;
|
||||
|
||||
// Item attachment (optional)
|
||||
int32_t itemLot = body.has("attachment_lot") ? body["attachment_lot"].i() : 0;
|
||||
int32_t itemCount = body.has("attachment_count") ? body["attachment_count"].i() : 0;
|
||||
|
||||
if (subject.empty() || message.empty()) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Subject and body are required";
|
||||
return crow::response(response);
|
||||
}
|
||||
|
||||
// Prefix sender name with [GM]
|
||||
std::string senderName = "[GM] " + username;
|
||||
|
||||
std::vector<LWOOBJID> recipients;
|
||||
|
||||
if (sendToAll) {
|
||||
// Get all accounts and their characters
|
||||
auto allAccounts = Database::Get()->GetAllAccounts();
|
||||
for (const auto& acct : allAccounts) {
|
||||
auto chars = Database::Get()->GetAccountCharacterIds(acct.id);
|
||||
for (const auto& charId : chars) {
|
||||
recipients.push_back(charId);
|
||||
}
|
||||
}
|
||||
} else if (recipientId > 0) {
|
||||
recipients.push_back(recipientId);
|
||||
} else {
|
||||
response["success"] = false;
|
||||
response["error"] = "No recipients specified";
|
||||
return crow::response(response);
|
||||
}
|
||||
|
||||
// Send mail to all recipients
|
||||
uint64_t currentTime = static_cast<uint64_t>(std::time(nullptr));
|
||||
int mailSent = 0;
|
||||
|
||||
for (const auto& recipId : recipients) {
|
||||
// Get recipient character name
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(recipId);
|
||||
if (!charInfo) continue;
|
||||
|
||||
MailInfo mail;
|
||||
mail.senderUsername = senderName;
|
||||
mail.recipient = charInfo->name;
|
||||
mail.receiverId = recipId;
|
||||
mail.subject = subject;
|
||||
mail.body = message;
|
||||
mail.itemID = itemLot > 0 ? 1 : 0; // If there's an item, set ID to 1
|
||||
mail.itemLOT = itemLot;
|
||||
mail.itemCount = itemCount > 0 ? itemCount : 1;
|
||||
mail.timeSent = currentTime;
|
||||
mail.wasRead = false;
|
||||
|
||||
Database::Get()->InsertNewMail(mail);
|
||||
mailSent++;
|
||||
}
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Mail sent successfully";
|
||||
response["recipients"] = mailSent;
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Get mail by ID (for viewing)
|
||||
CROW_ROUTE(app, "/api/mail/<uint>")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req, uint64_t mail_id) {
|
||||
// Any authenticated user can view mail
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return crow::response(401, "{\"error\": \"Not authenticated\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto mail = Database::Get()->GetMail(mail_id);
|
||||
if (!mail) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Mail not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
// Check if user can access this mail (owns the character or is GM)
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(mail->receiverId);
|
||||
bool canAccess = false;
|
||||
|
||||
if (charInfo && charInfo->accountId == user->id) {
|
||||
canAccess = true;
|
||||
}
|
||||
|
||||
if (static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(eGameMasterLevel::MODERATOR)) {
|
||||
canAccess = true;
|
||||
}
|
||||
|
||||
if (!canAccess) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Access denied";
|
||||
return crow::response(403, response);
|
||||
}
|
||||
|
||||
response["success"] = true;
|
||||
response["id"] = mail->id;
|
||||
response["sender_name"] = mail->senderUsername;
|
||||
response["receiver_name"] = mail->recipient;
|
||||
response["receiver_id"] = static_cast<uint64_t>(mail->receiverId);
|
||||
response["subject"] = mail->subject;
|
||||
response["body"] = mail->body;
|
||||
response["attachment_lot"] = mail->itemLOT;
|
||||
response["attachment_count"] = mail->itemCount;
|
||||
response["time_sent"] = mail->timeSent;
|
||||
response["was_read"] = mail->wasRead;
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace MailBlueprint
|
||||
20
dDashboardServer/blueprints/MailBlueprint.h
Normal file
20
dDashboardServer/blueprints/MailBlueprint.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef __MAILBLUEPRINT_H__
|
||||
#define __MAILBLUEPRINT_H__
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace MailBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup mail management routes
|
||||
* Registers routes for sending and viewing mail
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace MailBlueprint
|
||||
|
||||
#endif // __MAILBLUEPRINT_H__
|
||||
279
dDashboardServer/blueprints/ModerationBlueprint.cpp
Normal file
279
dDashboardServer/blueprints/ModerationBlueprint.cpp
Normal file
@@ -0,0 +1,279 @@
|
||||
#include "ModerationBlueprint.h"
|
||||
#include "Database.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "Logger.h"
|
||||
|
||||
namespace ModerationBlueprint {
|
||||
|
||||
// Helper function to get current user's account info from session
|
||||
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
if (username.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Database::Get()->GetAccountInfo(username);
|
||||
}
|
||||
|
||||
// Helper function to check if user has minimum GM level
|
||||
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return static_cast<uint8_t>(user->maxGmLevel) >= static_cast<uint8_t>(required);
|
||||
}
|
||||
|
||||
void Setup(DashboardApp& app) {
|
||||
// Get pet names by status
|
||||
CROW_ROUTE(app, "/api/moderation/pets")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
crow::json::wvalue::list data;
|
||||
|
||||
try {
|
||||
auto statusParam = req.url_params.get("status");
|
||||
std::string status = statusParam ? statusParam : "all";
|
||||
|
||||
std::vector<IPetNames::DetailedInfo> pets;
|
||||
|
||||
if (status == "approved") {
|
||||
pets = Database::Get()->GetPetNamesByStatus(2);
|
||||
} else if (status == "unapproved") {
|
||||
pets = Database::Get()->GetPetNamesByStatus(1);
|
||||
} else {
|
||||
pets = Database::Get()->GetAllPetNames();
|
||||
}
|
||||
|
||||
for (const auto& pet : pets) {
|
||||
crow::json::wvalue item;
|
||||
item["id"] = static_cast<uint64_t>(pet.id);
|
||||
item["pet_name"] = pet.petName;
|
||||
item["approval_status"] = pet.approvalStatus;
|
||||
item["owner_id"] = static_cast<uint64_t>(pet.ownerId);
|
||||
|
||||
// Get owner character name
|
||||
if (pet.ownerId > 0) {
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(pet.ownerId);
|
||||
if (charInfo) {
|
||||
item["owner_name"] = charInfo->name;
|
||||
} else {
|
||||
item["owner_name"] = "Unknown";
|
||||
}
|
||||
} else {
|
||||
item["owner_name"] = "None";
|
||||
}
|
||||
|
||||
data.push_back(std::move(item));
|
||||
}
|
||||
|
||||
response["data"] = std::move(data);
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["error"] = ex.what();
|
||||
return crow::response(500, response);
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Approve a pet name
|
||||
CROW_ROUTE(app, "/api/moderation/pets/<uint>/approve")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t pet_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
Database::Get()->SetPetApprovalStatus(pet_id, 2); // 2 = approved
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Pet name approved";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Reject a pet name
|
||||
CROW_ROUTE(app, "/api/moderation/pets/<uint>/reject")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t pet_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
Database::Get()->SetPetApprovalStatus(pet_id, 0); // 0 = rejected
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Pet name rejected";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Get properties by approval status
|
||||
CROW_ROUTE(app, "/api/moderation/properties")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
crow::json::wvalue::list data;
|
||||
|
||||
try {
|
||||
auto statusParam = req.url_params.get("status");
|
||||
std::string status = statusParam ? statusParam : "all";
|
||||
|
||||
std::vector<IProperty::Info> properties;
|
||||
|
||||
if (status == "approved") {
|
||||
properties = Database::Get()->GetPropertiesByApprovalStatus(1);
|
||||
} else if (status == "unapproved") {
|
||||
properties = Database::Get()->GetPropertiesByApprovalStatus(0);
|
||||
} else {
|
||||
properties = Database::Get()->GetAllProperties();
|
||||
}
|
||||
|
||||
for (const auto& prop : properties) {
|
||||
crow::json::wvalue item;
|
||||
item["id"] = static_cast<uint64_t>(prop.id);
|
||||
item["name"] = prop.name;
|
||||
item["description"] = prop.description;
|
||||
item["owner_id"] = static_cast<uint64_t>(prop.ownerId);
|
||||
item["clone_id"] = static_cast<uint64_t>(prop.cloneId);
|
||||
item["privacy_option"] = prop.privacyOption;
|
||||
item["mod_approved"] = prop.modApproved;
|
||||
item["last_updated"] = prop.lastUpdatedTime;
|
||||
item["claimed_time"] = prop.claimedTime;
|
||||
item["reputation"] = prop.reputation;
|
||||
item["performance_cost"] = prop.performanceCost;
|
||||
item["rejection_reason"] = prop.rejectionReason;
|
||||
|
||||
// Get owner character name
|
||||
auto charInfo = Database::Get()->GetCharacterInfo(prop.ownerId);
|
||||
if (charInfo) {
|
||||
item["owner_name"] = charInfo->name;
|
||||
} else {
|
||||
item["owner_name"] = "Unknown";
|
||||
}
|
||||
|
||||
data.push_back(std::move(item));
|
||||
}
|
||||
|
||||
response["data"] = std::move(data);
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["error"] = ex.what();
|
||||
return crow::response(500, response);
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Approve/unapprove a property
|
||||
CROW_ROUTE(app, "/api/moderation/properties/<uint>/approve")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t property_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto prop = Database::Get()->GetPropertyInfo(property_id);
|
||||
if (!prop) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Property not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
// Toggle approval
|
||||
IProperty::Info updatedInfo = *prop;
|
||||
updatedInfo.modApproved = prop->modApproved ? 0 : 1;
|
||||
updatedInfo.rejectionReason = "";
|
||||
|
||||
Database::Get()->UpdatePropertyModerationInfo(updatedInfo);
|
||||
|
||||
response["success"] = true;
|
||||
response["approved"] = updatedInfo.modApproved;
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Reject a property with reason
|
||||
CROW_ROUTE(app, "/api/moderation/properties/<uint>/reject")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req, uint64_t property_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto prop = Database::Get()->GetPropertyInfo(property_id);
|
||||
if (!prop) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Property not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
std::string reason;
|
||||
if (body.has("reason"))
|
||||
reason = std::string(body["reason"].s());
|
||||
else
|
||||
reason = "No reason provided";
|
||||
|
||||
IProperty::Info updatedInfo = *prop;
|
||||
updatedInfo.modApproved = 0;
|
||||
updatedInfo.rejectionReason = reason;
|
||||
|
||||
Database::Get()->UpdatePropertyModerationInfo(updatedInfo);
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Property rejected";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace ModerationBlueprint
|
||||
20
dDashboardServer/blueprints/ModerationBlueprint.h
Normal file
20
dDashboardServer/blueprints/ModerationBlueprint.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef __MODERATIONBLUEPRINT_H__
|
||||
#define __MODERATIONBLUEPRINT_H__
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace ModerationBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup moderation routes
|
||||
* Registers routes for pet name moderation and property approval
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace ModerationBlueprint
|
||||
|
||||
#endif // __MODERATIONBLUEPRINT_H__
|
||||
380
dDashboardServer/blueprints/PageBlueprint.cpp
Normal file
380
dDashboardServer/blueprints/PageBlueprint.cpp
Normal file
@@ -0,0 +1,380 @@
|
||||
#include "PageBlueprint.h"
|
||||
#include "Logger.h"
|
||||
#include "Database.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
|
||||
namespace PageBlueprint {
|
||||
|
||||
// Helper to get GM level name
|
||||
std::string GetGMLevelName(eGameMasterLevel level) {
|
||||
switch (level) {
|
||||
case eGameMasterLevel::CIVILIAN: return "Civilian";
|
||||
case eGameMasterLevel::FORUM_MODERATOR: return "Forum Moderator";
|
||||
case eGameMasterLevel::JUNIOR_MODERATOR: return "Junior Moderator";
|
||||
case eGameMasterLevel::MODERATOR: return "Moderator";
|
||||
case eGameMasterLevel::SENIOR_MODERATOR: return "Senior Moderator";
|
||||
case eGameMasterLevel::LEAD_MODERATOR: return "Lead Moderator";
|
||||
case eGameMasterLevel::JUNIOR_DEVELOPER: return "Junior Developer";
|
||||
case eGameMasterLevel::INACTIVE_DEVELOPER: return "Inactive Developer";
|
||||
case eGameMasterLevel::DEVELOPER: return "Developer";
|
||||
case eGameMasterLevel::OPERATOR: return "Operator";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get current user's account info from session
|
||||
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
if (username.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Database::Get()->GetAccountInfo(username);
|
||||
}
|
||||
|
||||
// Helper to get user's GM level
|
||||
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return eGameMasterLevel::CIVILIAN;
|
||||
}
|
||||
return user->maxGmLevel;
|
||||
}
|
||||
|
||||
// Helper to check if user has minimum GM level
|
||||
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
|
||||
auto level = GetUserGMLevel(req, app);
|
||||
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
|
||||
}
|
||||
|
||||
// Helper to create base context for all templates
|
||||
crow::mustache::context GetBaseContext(const crow::request& req, DashboardApp& app) {
|
||||
crow::mustache::context ctx;
|
||||
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
int account_id = session.template get<int>("account_id", -1);
|
||||
int gm_level = session.template get<int>("gm_level", -1);
|
||||
|
||||
if (!username.empty() && account_id != -1) {
|
||||
LOG("User '%s' (Account ID: %d) is authenticated with GM level %d", username.c_str(), account_id, gm_level);
|
||||
ctx["is_authenticated"] = true;
|
||||
ctx["show_navbar"] = true;
|
||||
ctx["username"] = username;
|
||||
ctx["account_id"] = account_id;
|
||||
ctx["gm_level"] = gm_level;
|
||||
ctx["gm_level_name"] = GetGMLevelName(static_cast<eGameMasterLevel>(gm_level));
|
||||
|
||||
// Set permission flags
|
||||
ctx["is_gm_3_plus"] = (gm_level >= 3);
|
||||
ctx["is_gm_5_plus"] = (gm_level >= 5);
|
||||
ctx["is_gm_8_plus"] = (gm_level >= 8);
|
||||
ctx["is_gm_9_plus"] = (gm_level >= 9);
|
||||
} else {
|
||||
LOG("User is not authenticated");
|
||||
ctx["is_authenticated"] = false;
|
||||
ctx["show_navbar"] = false;
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// Helper to render a page with layout
|
||||
std::string RenderPage(const crow::request& req, DashboardApp& app, const std::string& template_name, const std::string& page_title, crow::mustache::context& page_ctx) {
|
||||
auto base_ctx = GetBaseContext(req, app);
|
||||
|
||||
// Merge base context with page-specific context
|
||||
for (const auto& key : page_ctx.keys()) {
|
||||
base_ctx[key] = crow::json::wvalue(page_ctx[key]);
|
||||
}
|
||||
|
||||
// Load the content template and render to string
|
||||
auto content_page = crow::mustache::load(template_name);
|
||||
std::string content_html = content_page.render_string(base_ctx);
|
||||
|
||||
// Set content and page title in base context
|
||||
base_ctx["content"] = crow::json::wvalue(content_html);
|
||||
base_ctx["page_title"] = crow::json::wvalue(page_title);
|
||||
|
||||
// Render with layout
|
||||
auto layout = crow::mustache::load("layouts/base.html");
|
||||
return layout.render_string(base_ctx);
|
||||
}
|
||||
|
||||
void Setup(DashboardApp& app) {
|
||||
// Home/Dashboard page
|
||||
CROW_ROUTE(app, "/")
|
||||
([&](const crow::request& req) {
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_home"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "index.html", "Dashboard", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Login page
|
||||
CROW_ROUTE(app, "/login")
|
||||
([&](const crow::request& req) {
|
||||
crow::mustache::context ctx;
|
||||
|
||||
std::string html = RenderPage(req, app, "login.html", "Login", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Accounts page
|
||||
CROW_ROUTE(app, "/accounts")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_accounts"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "accounts/index.html", "Accounts", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Activity Logs page
|
||||
CROW_ROUTE(app, "/logs/activities")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Developers and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
// Set nav active state if needed
|
||||
|
||||
std::string html = RenderPage(req, app, "logs/activities.html", "Activity Logs", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Characters page
|
||||
CROW_ROUTE(app, "/characters")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_characters"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "characters/index.html", "Characters", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Play Keys page
|
||||
CROW_ROUTE(app, "/playkeys")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Lead Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_playkeys"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "playkeys/index.html", "Play Keys", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Registration page - public
|
||||
CROW_ROUTE(app, "/register")
|
||||
([&](const crow::request& req) {
|
||||
crow::mustache::context ctx;
|
||||
std::string html = RenderPage(req, app, "register.html", "Register", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Mail page
|
||||
CROW_ROUTE(app, "/mail/send")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_mail"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "mail/send.html", "Send Mail", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Bug Reports page
|
||||
CROW_ROUTE(app, "/bugreports")
|
||||
([&](const crow::request& req) {
|
||||
// Anyone authenticated can view their own bug reports
|
||||
// GMs can view all
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return crow::response(403, "Forbidden - Login required");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_bugreports"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "bugreports/index.html", "Bug Reports", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Moderation page - Pet Names
|
||||
CROW_ROUTE(app, "/moderation/pets")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_moderation"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "moderation/pets.html", "Pet Name Moderation", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Moderation page - Properties
|
||||
CROW_ROUTE(app, "/moderation/properties")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_moderation"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "moderation/properties.html", "Property Moderation", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Account view page
|
||||
CROW_ROUTE(app, "/accounts/view/<int>")
|
||||
([&](const crow::request& req, int account_id) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_accounts"] = true;
|
||||
ctx["account_id"] = account_id;
|
||||
|
||||
std::string html = RenderPage(req, app, "accounts/view.html", "View Account", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Character view page
|
||||
CROW_ROUTE(app, "/characters/view/<int>")
|
||||
([&](const crow::request& req, int character_id) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_characters"] = true;
|
||||
ctx["character_id"] = character_id;
|
||||
|
||||
std::string html = RenderPage(req, app, "characters/view.html", "View Character", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Logs - Command Logs page
|
||||
CROW_ROUTE(app, "/logs/commands")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Developers and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
// Set nav active state if needed
|
||||
|
||||
std::string html = RenderPage(req, app, "logs/commands.html", "Command Logs", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Logs - Audit Logs page
|
||||
CROW_ROUTE(app, "/logs/audits")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Developers and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::DEVELOPER)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
// Set nav active state if needed
|
||||
|
||||
std::string html = RenderPage(req, app, "logs/audits.html", "Audit Logs", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// About page
|
||||
CROW_ROUTE(app, "/about")
|
||||
([&](const crow::request& req) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return crow::response(403, "Forbidden - Login required");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
|
||||
std::string html = RenderPage(req, app, "about.html", "About", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Bug Reports page (fix routing)
|
||||
CROW_ROUTE(app, "/bugs")
|
||||
([&](const crow::request& req) {
|
||||
// Anyone authenticated can view their own bug reports
|
||||
// GMs can view all
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return crow::response(403, "Forbidden - Login required");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_bugs"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "bugreports/index.html", "Bug Reports", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Moderation page - Pending Pets
|
||||
CROW_ROUTE(app, "/moderation/pending")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_moderation"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "moderation/pets.html", "Pending Pet Names", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
|
||||
// Properties page
|
||||
CROW_ROUTE(app, "/properties")
|
||||
([&](const crow::request& req) {
|
||||
// Check GM level - Moderators and above
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::MODERATOR)) {
|
||||
return crow::response(403, "Forbidden - Insufficient GM level");
|
||||
}
|
||||
|
||||
crow::mustache::context ctx;
|
||||
ctx["nav_moderation"] = true;
|
||||
|
||||
std::string html = RenderPage(req, app, "moderation/properties.html", "Property Moderation", ctx);
|
||||
return crow::response(html);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace PageBlueprint
|
||||
17
dDashboardServer/blueprints/PageBlueprint.h
Normal file
17
dDashboardServer/blueprints/PageBlueprint.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace PageBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup page rendering routes
|
||||
* Registers routes that render HTML pages (dashboard, login, accounts, etc.)
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace PageBlueprint
|
||||
288
dDashboardServer/blueprints/PlayKeysBlueprint.cpp
Normal file
288
dDashboardServer/blueprints/PlayKeysBlueprint.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include "PlayKeysBlueprint.h"
|
||||
#include "Database.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "Logger.h"
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
namespace PlayKeysBlueprint {
|
||||
|
||||
// Helper to generate a random play key string (format: XXXX-XXXX-XXXX-XXXX)
|
||||
std::string GeneratePlayKeyString() {
|
||||
static const char charset[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // Excluding ambiguous chars
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<> dis(0, sizeof(charset) - 2);
|
||||
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (i > 0 && i % 4 == 0) ss << '-';
|
||||
ss << charset[dis(gen)];
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Helper function to get current user's account info from session
|
||||
std::optional<IAccounts::Info> GetCurrentUser(const crow::request& req, DashboardApp& app) {
|
||||
auto& session = app.get_context<Session>(const_cast<crow::request&>(req));
|
||||
std::string username = session.template get<std::string>("username");
|
||||
|
||||
if (username.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Database::Get()->GetAccountInfo(username);
|
||||
}
|
||||
|
||||
// Helper function to get user's GM level
|
||||
eGameMasterLevel GetUserGMLevel(const crow::request& req, DashboardApp& app) {
|
||||
auto user = GetCurrentUser(req, app);
|
||||
if (!user) {
|
||||
return eGameMasterLevel::CIVILIAN;
|
||||
}
|
||||
return user->maxGmLevel;
|
||||
}
|
||||
|
||||
// Helper function to check if user has minimum GM level
|
||||
bool HasMinimumGMLevel(const crow::request& req, DashboardApp& app, eGameMasterLevel required) {
|
||||
auto level = GetUserGMLevel(req, app);
|
||||
return static_cast<uint8_t>(level) >= static_cast<uint8_t>(required);
|
||||
}
|
||||
|
||||
void Setup(DashboardApp& app) {
|
||||
// Get all play keys (DataTables endpoint)
|
||||
CROW_ROUTE(app, "/api/playkeys")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
crow::json::wvalue::list data;
|
||||
|
||||
try {
|
||||
auto keys = Database::Get()->GetAllPlayKeys();
|
||||
|
||||
for (const auto& key : keys) {
|
||||
crow::json::wvalue item;
|
||||
item["id"] = key.id;
|
||||
item["key_string"] = key.key_string;
|
||||
item["key_uses"] = key.key_uses;
|
||||
item["times_used"] = key.times_used;
|
||||
item["active"] = key.active;
|
||||
item["notes"] = key.notes;
|
||||
item["created_at"] = static_cast<uint64_t>(key.created_at);
|
||||
|
||||
data.push_back(std::move(item));
|
||||
}
|
||||
} catch (std::exception& ex) {
|
||||
// return empty list on failure
|
||||
}
|
||||
|
||||
response["data"] = std::move(data);
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Create a new play key
|
||||
CROW_ROUTE(app, "/api/playkeys/create")
|
||||
.methods("POST"_method)
|
||||
([&](const crow::request& req) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
uint32_t count = body.has("count") ? body["count"].i() : 1;
|
||||
uint32_t uses = body.has("uses") ? body["uses"].i() : 1;
|
||||
std::string notes;
|
||||
if (body.has("notes"))
|
||||
notes = std::string(body["notes"].s());
|
||||
else
|
||||
notes = "";
|
||||
|
||||
// Limit to prevent abuse
|
||||
if (count > 100) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Cannot create more than 100 keys at once";
|
||||
return crow::response(response);
|
||||
}
|
||||
|
||||
crow::json::wvalue::list keys;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
std::string keyString = GeneratePlayKeyString();
|
||||
Database::Get()->CreatePlayKey(keyString, uses, notes);
|
||||
keys.push_back(keyString);
|
||||
}
|
||||
|
||||
response["success"] = true;
|
||||
response["keys"] = std::move(keys);
|
||||
response["count"] = count;
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Get single play key by ID
|
||||
CROW_ROUTE(app, "/api/playkeys/<int>")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req, int key_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
auto key = Database::Get()->GetPlayKeyById(key_id);
|
||||
if (!key) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Play key not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
response["success"] = true;
|
||||
response["id"] = key->id;
|
||||
response["key_string"] = key->key_string;
|
||||
response["key_uses"] = key->key_uses;
|
||||
response["times_used"] = key->times_used;
|
||||
response["active"] = key->active;
|
||||
response["notes"] = key->notes;
|
||||
response["created_at"] = static_cast<uint64_t>(key->created_at);
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Update a play key
|
||||
CROW_ROUTE(app, "/api/playkeys/<int>")
|
||||
.methods("PUT"_method, "POST"_method)
|
||||
([&](const crow::request& req, int key_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
auto body = crow::json::load(req.body);
|
||||
if (!body) {
|
||||
return crow::response(400, "{\"error\": \"Invalid JSON\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
// Get current key info
|
||||
auto key = Database::Get()->GetPlayKeyById(key_id);
|
||||
if (!key) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Play key not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
uint32_t uses = body.has("uses") ? body["uses"].i() : key->key_uses;
|
||||
bool active = body.has("active") ? body["active"].b() : key->active;
|
||||
std::string notes;
|
||||
if (body.has("notes"))
|
||||
notes = std::string(body["notes"].s());
|
||||
else
|
||||
notes = key->notes;
|
||||
|
||||
Database::Get()->UpdatePlayKey(key_id, uses, active, notes);
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Play key updated successfully";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Delete a play key
|
||||
CROW_ROUTE(app, "/api/playkeys/<int>")
|
||||
.methods("DELETE"_method)
|
||||
([&](const crow::request& req, int key_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
|
||||
try {
|
||||
// Check if key exists
|
||||
auto key = Database::Get()->GetPlayKeyById(key_id);
|
||||
if (!key) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Play key not found";
|
||||
return crow::response(404, response);
|
||||
}
|
||||
|
||||
Database::Get()->DeletePlayKey(key_id);
|
||||
|
||||
response["success"] = true;
|
||||
response["message"] = "Play key deleted successfully";
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["success"] = false;
|
||||
response["error"] = ex.what();
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
|
||||
// Get accounts associated with a play key
|
||||
CROW_ROUTE(app, "/api/playkeys/<int>/accounts")
|
||||
.methods("GET"_method)
|
||||
([&](const crow::request& req, int key_id) {
|
||||
if (!HasMinimumGMLevel(req, app, eGameMasterLevel::LEAD_MODERATOR)) {
|
||||
return crow::response(403, "{\"error\": \"Forbidden\"}");
|
||||
}
|
||||
|
||||
crow::json::wvalue response;
|
||||
crow::json::wvalue::list accounts;
|
||||
|
||||
try {
|
||||
// Get all accounts and filter by play_key_id
|
||||
auto allAccounts = Database::Get()->GetAllAccounts();
|
||||
for (const auto& acct : allAccounts) {
|
||||
if (acct.play_key_id == static_cast<uint32_t>(key_id)) {
|
||||
crow::json::wvalue item;
|
||||
item["id"] = acct.id;
|
||||
item["name"] = acct.name;
|
||||
item["gm_level"] = static_cast<int>(acct.gm_level);
|
||||
item["banned"] = acct.banned;
|
||||
item["locked"] = acct.locked;
|
||||
|
||||
accounts.push_back(std::move(item));
|
||||
}
|
||||
}
|
||||
|
||||
response["data"] = std::move(accounts);
|
||||
|
||||
} catch (std::exception& ex) {
|
||||
response["error"] = ex.what();
|
||||
return crow::response(500, response);
|
||||
}
|
||||
|
||||
return crow::response(response);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace PlayKeysBlueprint
|
||||
20
dDashboardServer/blueprints/PlayKeysBlueprint.h
Normal file
20
dDashboardServer/blueprints/PlayKeysBlueprint.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef __PLAYKEYSBLUEPRINT_H__
|
||||
#define __PLAYKEYSBLUEPRINT_H__
|
||||
|
||||
#include "crow.h"
|
||||
#include "crow/middlewares/session.h"
|
||||
|
||||
namespace PlayKeysBlueprint {
|
||||
|
||||
using Session = crow::SessionMiddleware<crow::InMemoryStore>;
|
||||
using DashboardApp = crow::App<crow::CookieParser, Session>;
|
||||
|
||||
/**
|
||||
* Setup play keys management routes
|
||||
* Registers routes for creating, viewing, editing, and deleting play keys
|
||||
*/
|
||||
void Setup(DashboardApp& app);
|
||||
|
||||
} // namespace PlayKeysBlueprint
|
||||
|
||||
#endif // __PLAYKEYSBLUEPRINT_H__
|
||||
144
dDashboardServer/static/css/dashboard.css
Normal file
144
dDashboardServer/static/css/dashboard.css
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Consolidated NexusDashboard CSS
|
||||
* Combined from nexus-theme.css and dashboard.css to provide a single
|
||||
* consistent stylesheet for the DarkflameServer dashboard.
|
||||
*/
|
||||
|
||||
/* ------------------------ Nexus theme (dark) variables ------------------------ */
|
||||
:root {
|
||||
--nexus-dark-bg: #212529;
|
||||
--nexus-darker-bg: #1a1d20;
|
||||
--nexus-card-bg: #2c3034;
|
||||
--nexus-border: #404448;
|
||||
--nexus-text: #f8f9fa;
|
||||
--nexus-text-muted: #adb5bd;
|
||||
--nexus-primary: #0d6efd;
|
||||
--nexus-success: #198754;
|
||||
--nexus-warning: #ffc107;
|
||||
--nexus-danger: #dc3545;
|
||||
--nexus-info: #0dcaf0;
|
||||
/* legacy dashboard variables */
|
||||
--primary-color: #0d6efd;
|
||||
--success-color: #198754;
|
||||
--warning-color: #ffc107;
|
||||
--danger-color: #dc3545;
|
||||
--dark-bg: #1a1a1a;
|
||||
--light-bg: #f8f9fa;
|
||||
}
|
||||
|
||||
/* ------------------------ Base layout, navbar, cards ------------------------ */
|
||||
body {
|
||||
background-color: var(--nexus-dark-bg);
|
||||
color: var(--nexus-text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main { flex: 1; padding-bottom: 60px; }
|
||||
.footer { margin-top: auto; border-top: 1px solid var(--nexus-border); background-color: var(--nexus-dark-bg); }
|
||||
|
||||
/* Ensure footer text is visible on dark background */
|
||||
.footer, .footer .text-muted { color: var(--nexus-text-muted) !important; }
|
||||
|
||||
.navbar { box-shadow: 0 2px 4px rgba(0,0,0,.1); }
|
||||
.navbar-brand { font-weight: bold; font-size: 1.25rem; }
|
||||
.nav-link { transition: all 0.3s ease; }
|
||||
.nav-link:hover { background-color: rgba(255,255,255,0.05); border-radius: 4px; }
|
||||
.nav-link.active { background-color: rgba(255,255,255,0.08); border-radius: 4px; }
|
||||
|
||||
.card { background-color: var(--nexus-card-bg); border-color: var(--nexus-border); color: var(--nexus-text); border: none; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 1.5rem; transition: transform 0.2s ease, box-shadow 0.2s ease; }
|
||||
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
||||
.card-header { background-color: var(--nexus-darker-bg); border-bottom-color: var(--nexus-border); color: var(--nexus-text); font-weight: 600; }
|
||||
|
||||
/* ------------------------ Tables and DataTables ------------------------ */
|
||||
.table { color: var(--nexus-text); background-color: #1e1e1e; }
|
||||
.table thead th { background-color: #242526; color: var(--nexus-text); border-bottom: 1px solid var(--nexus-border); font-weight: 600; }
|
||||
.table tbody td { color: var(--nexus-text); }
|
||||
.table-striped > tbody > tr:nth-of-type(odd) > * { background-color: rgba(255,255,255,0.02); }
|
||||
.table-hover > tbody > tr:hover > * { background-color: rgba(255,255,255,0.035); }
|
||||
|
||||
/* DataTables adds `odd`/`even` classes and sometimes doesn't use `.table-striped`.
|
||||
Normalize striping across Bootstrap tables and DataTables instances so every
|
||||
other row has a visible background in dark mode. Use slightly stronger contrast
|
||||
and cover different DOM shapes that DataTables can produce (cells or `*`). */
|
||||
.dataTable tbody tr.odd > *,
|
||||
.dataTable tbody tr.odd td,
|
||||
.table.table-striped tbody tr.odd > *,
|
||||
.table.table-striped tbody tr.odd td {
|
||||
background-color: rgba(255,255,255,0.03);
|
||||
}
|
||||
.dataTable tbody tr.even > *,
|
||||
.dataTable tbody tr.even td,
|
||||
.table.table-striped tbody tr.even > *,
|
||||
.table.table-striped tbody tr.even td {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Some DataTables setups use nested wrappers (.dataTables_scrollBody) so ensure
|
||||
striping still applies inside scroll bodies. */
|
||||
.dataTables_scrollBody table tbody tr.odd > *,
|
||||
.dataTables_scrollBody table tbody tr.odd td {
|
||||
background-color: rgba(255,255,255,0.03);
|
||||
}
|
||||
.dataTables_scrollBody table tbody tr.even > *,
|
||||
.dataTables_scrollBody table tbody tr.even td {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Keep hover state clear above striping */
|
||||
.dataTable tbody tr:hover > *,
|
||||
.table tbody tr:hover > * {
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
}
|
||||
.table > :not(caption) > * > * { border-bottom-color: var(--nexus-border); }
|
||||
|
||||
/* Light-theme overrides (explicit) */
|
||||
@media (prefers-color-scheme: light) {
|
||||
body { background-color: var(--light-bg); color: #212529; }
|
||||
.card { background-color: #fff; color: #212529; }
|
||||
.card-header { background-color: #fff; border-bottom: 2px solid var(--primary-color); color: #212529; }
|
||||
.table { background-color: white; color: #212529; }
|
||||
.table thead th { background-color: #f8f9fa; color: #212529; border-bottom: 2px solid #dee2e6; font-weight: 600; text-transform: uppercase; font-size: 0.85rem; letter-spacing: 0.5px; }
|
||||
.dataTables_wrapper select, .dataTables_wrapper input { background-color: #fff; border-color: #ced4da; color: #212529; }
|
||||
}
|
||||
|
||||
/* Dark mode explicit styling (prefers-color-scheme: dark) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background-color: var(--nexus-dark-bg); color: var(--nexus-text); }
|
||||
.card { background-color: var(--nexus-card-bg); color: var(--nexus-text); }
|
||||
.card-header { background-color: var(--nexus-darker-bg); color: var(--nexus-text); }
|
||||
.table { background-color: #1e1e1e; color: var(--nexus-text); }
|
||||
.table thead th { background-color: #252525; border-bottom-color: #3a3a3a; color: var(--nexus-text); }
|
||||
.dataTables_wrapper select, .dataTables_wrapper input { background-color: var(--nexus-darker-bg); border-color: var(--nexus-border); color: var(--nexus-text); }
|
||||
}
|
||||
|
||||
/* DataTables specific visual rules */
|
||||
.dataTables_wrapper { padding: 0; }
|
||||
.dataTables_filter input { margin-left: 0.5rem; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; }
|
||||
.dataTables_length select { padding: 0.375rem 2rem 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem; margin: 0 0.5rem; }
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button { color: var(--nexus-text) !important; }
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.current { background: var(--nexus-primary); border-color: var(--nexus-primary); color: white !important; }
|
||||
|
||||
/* Forms, badges, buttons, utilities */
|
||||
.form-control, .form-select { background-color: var(--nexus-darker-bg); border-color: var(--nexus-border); color: var(--nexus-text); }
|
||||
.form-control::placeholder { color: var(--nexus-text-muted); }
|
||||
.form-label { color: var(--nexus-text); }
|
||||
.badge { padding: 0.35em 0.65em; font-weight: 500; }
|
||||
.btn { transition: all 0.2s ease; }
|
||||
.btn:hover { transform: translateY(-1px); box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
|
||||
|
||||
/* Utilities and accessibility */
|
||||
.loading { position: relative; pointer-events: none; opacity: 0.6; }
|
||||
.loading::after { content: ""; position: absolute; top: 50%; left: 50%; width: 2rem; height: 2rem; margin: -1rem 0 0 -1rem; border: 0.25rem solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spinner 0.75s linear infinite; }
|
||||
@keyframes spinner { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Responsive tweaks */
|
||||
@media (max-width: 768px) { .navbar-brand { font-size: 1rem; } .card { margin-bottom: 1rem; } .alerts-container { left: 10px; right: 10px; max-width: none; } .btn-group { flex-wrap: wrap; } }
|
||||
|
||||
/* Extra helpers */
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
.text-truncate-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.ws-nowrap { white-space: nowrap; }
|
||||
|
||||
/* End of consolidated stylesheet */
|
||||
144
dDashboardServer/static/js/api.js
Normal file
144
dDashboardServer/static/js/api.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* API Client for DarkflameServer Dashboard
|
||||
* Provides a simple interface for making API calls with error handling
|
||||
*/
|
||||
|
||||
const API = {
|
||||
/**
|
||||
* Base URL for API endpoints
|
||||
*/
|
||||
baseURL: '',
|
||||
|
||||
/**
|
||||
* Make a GET request
|
||||
* @param {string} endpoint - The API endpoint
|
||||
* @param {object} params - Query parameters
|
||||
* @returns {Promise<any>} Response data
|
||||
*/
|
||||
async get(endpoint, params = {}) {
|
||||
const url = new URL(this.baseURL + endpoint, window.location.origin);
|
||||
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a POST request
|
||||
* @param {string} endpoint - The API endpoint
|
||||
* @param {object} data - Request body data
|
||||
* @returns {Promise<any>} Response data
|
||||
*/
|
||||
async post(endpoint, data = {}) {
|
||||
const response = await fetch(this.baseURL + endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a PUT request
|
||||
* @param {string} endpoint - The API endpoint
|
||||
* @param {object} data - Request body data
|
||||
* @returns {Promise<any>} Response data
|
||||
*/
|
||||
async put(endpoint, data = {}) {
|
||||
const response = await fetch(this.baseURL + endpoint, {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a DELETE request
|
||||
* @param {string} endpoint - The API endpoint
|
||||
* @returns {Promise<any>} Response data
|
||||
*/
|
||||
async delete(endpoint) {
|
||||
const response = await fetch(this.baseURL + endpoint, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return this.handleResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle fetch response
|
||||
* @param {Response} response - Fetch response object
|
||||
* @returns {Promise<any>} Parsed response data
|
||||
*/
|
||||
async handleResponse(response) {
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
// Try to parse as JSON first (even if content-type is missing)
|
||||
try {
|
||||
const text = await response.text();
|
||||
|
||||
// Try to parse as JSON
|
||||
if (text) {
|
||||
try {
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (jsonError) {
|
||||
// Not JSON, return as text
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return text;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout function
|
||||
*/
|
||||
async function logout() {
|
||||
try {
|
||||
await API.post('/api/logout');
|
||||
window.location.href = '/login';
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
// Force redirect even on error
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}
|
||||
188
dDashboardServer/static/js/dashboard.js
Normal file
188
dDashboardServer/static/js/dashboard.js
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Main Dashboard JavaScript
|
||||
* Common utilities and functions for all pages
|
||||
*/
|
||||
|
||||
/**
|
||||
* Show an alert message
|
||||
* @param {string} type - Alert type (success, danger, warning, info)
|
||||
* @param {string} message - Alert message
|
||||
* @param {number} duration - Auto-dismiss duration in ms (0 = no auto-dismiss)
|
||||
*/
|
||||
function showAlert(type, message, duration = 5000) {
|
||||
const alertsContainer = document.getElementById('alerts-container') || createAlertsContainer();
|
||||
|
||||
const alertId = 'alert-' + Date.now();
|
||||
const alertHTML = `
|
||||
<div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
alertsContainer.insertAdjacentHTML('beforeend', alertHTML);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
const alert = document.getElementById(alertId);
|
||||
if (alert) {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create alerts container if it doesn't exist
|
||||
*/
|
||||
function createAlertsContainer() {
|
||||
const main = document.querySelector('main');
|
||||
const container = document.createElement('div');
|
||||
container.id = 'alerts-container';
|
||||
container.className = 'alerts-container';
|
||||
main.insertBefore(container, main.firstChild);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to localized date/time
|
||||
* @param {number} timestamp - Unix timestamp
|
||||
* @returns {string} Formatted date/time
|
||||
*/
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp || timestamp === 0) return '-';
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format GM level to human-readable name
|
||||
* @param {number} level - GM level number
|
||||
* @returns {string} GM level name
|
||||
*/
|
||||
function formatGMLevel(level) {
|
||||
const levels = {
|
||||
0: 'Civilian',
|
||||
1: 'Forum Moderator',
|
||||
2: 'Junior Moderator',
|
||||
3: 'Moderator',
|
||||
4: 'Senior Moderator',
|
||||
5: 'Lead Moderator',
|
||||
6: 'Junior Developer',
|
||||
7: 'Inactive Developer',
|
||||
8: 'Developer',
|
||||
9: 'Operator'
|
||||
};
|
||||
return levels[level] || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm action with modal
|
||||
* @param {string} title - Modal title
|
||||
* @param {string} message - Modal message
|
||||
* @param {function} callback - Callback function if confirmed
|
||||
*/
|
||||
function confirmAction(title, message, callback) {
|
||||
if (confirm(message)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
* @param {string} text - Text to copy
|
||||
*/
|
||||
async function copyToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showAlert('success', 'Copied to clipboard!', 2000);
|
||||
} catch (err) {
|
||||
showAlert('danger', 'Failed to copy to clipboard');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function calls
|
||||
* @param {function} func - Function to debounce
|
||||
* @param {number} wait - Wait time in ms
|
||||
* @returns {function} Debounced function
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize DataTables default settings
|
||||
*/
|
||||
$.extend(true, $.fn.dataTable.defaults, {
|
||||
responsive: true,
|
||||
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
|
||||
pageLength: 25,
|
||||
language: {
|
||||
search: "_INPUT_",
|
||||
searchPlaceholder: "Search...",
|
||||
lengthMenu: "Show _MENU_ entries",
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ entries",
|
||||
infoEmpty: "No entries found",
|
||||
infoFiltered: "(filtered from _MAX_ total entries)",
|
||||
zeroRecords: "No matching records found",
|
||||
emptyTable: "No data available in table"
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle form submission with API
|
||||
* @param {string} formId - Form element ID
|
||||
* @param {string} endpoint - API endpoint
|
||||
* @param {function} onSuccess - Success callback
|
||||
*/
|
||||
function handleFormSubmit(formId, endpoint, onSuccess) {
|
||||
const form = document.getElementById(formId);
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
const result = await API.post(endpoint, data);
|
||||
|
||||
if (result.success) {
|
||||
showAlert('success', result.message || 'Operation successful');
|
||||
if (onSuccess) onSuccess(result);
|
||||
} else {
|
||||
showAlert('danger', result.error || 'Operation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('danger', error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tooltips
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Bootstrap tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize Bootstrap popovers
|
||||
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
popoverTriggerList.map(function(popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
});
|
||||
46
dDashboardServer/static/js/login.js
Normal file
46
dDashboardServer/static/js/login.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Login page functionality
|
||||
*/
|
||||
|
||||
// Function to initialize login form
|
||||
function initLoginForm() {
|
||||
const form = document.getElementById('login-form');
|
||||
if (!form) return; // Not on login page
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const messageDiv = document.getElementById('login-message');
|
||||
|
||||
try {
|
||||
const response = await API.post('/api/login', { username, password });
|
||||
|
||||
if (response && response.success) {
|
||||
messageDiv.className = 'alert alert-success';
|
||||
messageDiv.textContent = 'Login successful! Redirecting...';
|
||||
messageDiv.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
messageDiv.className = 'alert alert-danger';
|
||||
messageDiv.textContent = response.error || 'Login failed';
|
||||
messageDiv.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
messageDiv.className = 'alert alert-danger';
|
||||
messageDiv.textContent = error.message || 'An error occurred during login';
|
||||
messageDiv.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initLoginForm);
|
||||
} else {
|
||||
initLoginForm();
|
||||
}
|
||||
43
dDashboardServer/static/js/register.js
Normal file
43
dDashboardServer/static/js/register.js
Normal file
@@ -0,0 +1,43 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('register-form');
|
||||
const alertBox = document.getElementById('register-alert');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
alertBox.style.display = 'none';
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const play_key = document.getElementById('play_key').value.trim();
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, play_key })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alertBox.className = 'alert alert-danger';
|
||||
alertBox.textContent = data.error || 'Registration failed';
|
||||
alertBox.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
alertBox.className = 'alert alert-success';
|
||||
alertBox.textContent = 'Account created successfully. You can now log in.';
|
||||
alertBox.style.display = 'block';
|
||||
form.reset();
|
||||
} else {
|
||||
alertBox.className = 'alert alert-danger';
|
||||
alertBox.textContent = data.error || 'Registration failed';
|
||||
alertBox.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
alertBox.className = 'alert alert-danger';
|
||||
alertBox.textContent = err.message || 'Registration failed';
|
||||
alertBox.style.display = 'block';
|
||||
}
|
||||
});
|
||||
});
|
||||
75
dDashboardServer/static/js/wait-for-jq-dt.js
Normal file
75
dDashboardServer/static/js/wait-for-jq-dt.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// Helper to wait for jQuery and DataTables (and optionally API) to be available
|
||||
// Usage:
|
||||
// safeInit(callback, { timeout: 5000, interval: 100, requireApi: false })
|
||||
// The callback receives `window.jQuery` as its first argument.
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
function waitFor(conditionFn, timeoutMs, intervalMs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const iv = setInterval(() => {
|
||||
try {
|
||||
if (conditionFn()) {
|
||||
clearInterval(iv);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
clearInterval(iv);
|
||||
reject(new Error('waitFor: timed out'));
|
||||
}
|
||||
}, intervalMs);
|
||||
});
|
||||
}
|
||||
|
||||
async function safeInit(cb, opts) {
|
||||
opts = opts || {};
|
||||
const timeout = typeof opts.timeout === 'number' ? opts.timeout : 5000;
|
||||
const interval = typeof opts.interval === 'number' ? opts.interval : 100;
|
||||
const requireApi = !!opts.requireApi;
|
||||
|
||||
// Wait for DOM ready first so scripts included at end of body have run
|
||||
if (document.readyState === 'loading') {
|
||||
await new Promise(r => document.addEventListener('DOMContentLoaded', r, { once: true }));
|
||||
}
|
||||
|
||||
try {
|
||||
await waitFor(() => window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable, timeout, interval);
|
||||
if (requireApi) {
|
||||
await waitFor(() => window.API, timeout, interval);
|
||||
}
|
||||
// call callback with jQuery
|
||||
try { cb(window.jQuery); } catch (e) { console.error('safeInit callback error', e); }
|
||||
} catch (err) {
|
||||
console.error('safeInit: required libraries failed to load', err);
|
||||
// If callback provided an onError handler, call it
|
||||
if (opts.onError && typeof opts.onError === 'function') {
|
||||
try { opts.onError(err); } catch (e) { console.error(e); }
|
||||
} else {
|
||||
// default fallback: show a banner if possible
|
||||
const tableEls = document.querySelectorAll('table');
|
||||
if (tableEls && tableEls.length) {
|
||||
tableEls.forEach(el => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'alert alert-danger';
|
||||
wrapper.textContent = 'Required JavaScript libraries failed to load (jQuery/DataTables). Please check your network or CDN allowlist.';
|
||||
el.replaceWith(wrapper);
|
||||
});
|
||||
} else {
|
||||
console.warn('safeInit: libraries missing');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose globally
|
||||
window.safeInit = safeInit;
|
||||
window.waitForLibraries = function(timeoutMs, intervalMs) {
|
||||
return waitFor(() => window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable, timeoutMs || 5000, intervalMs || 100);
|
||||
};
|
||||
|
||||
})(window);
|
||||
102
dDashboardServer/templates/about.html
Normal file
102
dDashboardServer/templates/about.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
About DarkflameServer Dashboard
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Dashboard Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h4 class="mb-3">DarkflameServer Web Dashboard</h4>
|
||||
<p class="lead">
|
||||
A modern C++ web interface for managing your Darkflame Universe server.
|
||||
</p>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>Features</h5>
|
||||
<ul>
|
||||
<li><strong>Account Management:</strong> Create, modify, ban, lock, and mute player accounts</li>
|
||||
<li><strong>Character Management:</strong> View, rescue, and manage player characters</li>
|
||||
<li><strong>Moderation Tools:</strong> Approve pet names, manage properties, and review bug reports</li>
|
||||
<li><strong>Mail System:</strong> Send in-game mail to players with item attachments</li>
|
||||
<li><strong>Play Keys:</strong> Manage registration keys for new accounts</li>
|
||||
<li><strong>Activity Logs:</strong> Monitor player activity and track logins/logouts</li>
|
||||
<li><strong>Audit Trail:</strong> Track all administrative actions for accountability</li>
|
||||
</ul>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>Technology Stack</h5>
|
||||
<ul>
|
||||
<li><strong>Backend:</strong> C++ with Crow web framework</li>
|
||||
<li><strong>Frontend:</strong> Bootstrap 5, jQuery, DataTables</li>
|
||||
<li><strong>Templates:</strong> Mustache templating engine</li>
|
||||
<li><strong>Database:</strong> MySQL/MariaDB or SQLite</li>
|
||||
</ul>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>GM Levels</h5>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Level 0</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-secondary">Civilian</span> - Regular player</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 1</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-info">Forum Moderator</span> - Forum moderation only</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 2</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-primary">Junior Moderator</span> - Basic moderation tools</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 3</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-success">Moderator</span> - Full moderation access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 4</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-success">Senior Moderator</span> - Advanced moderation</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 5</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-warning">Lead Moderator</span> - Moderation leadership</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 6</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-warning">Junior Developer</span> - Development access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 7</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-warning">Inactive Developer</span> - Limited dev access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 8</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-danger">Developer</span> - Full development access</dd>
|
||||
|
||||
<dt class="col-sm-3">Level 9</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-danger">Operator</span> - Full system access</dd>
|
||||
</dl>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5>About Darkflame Universe</h5>
|
||||
<p>
|
||||
DarkflameServer is an open-source server emulator for LEGO Universe,
|
||||
a massively multiplayer online game that was officially discontinued in 2012.
|
||||
The Darkflame Universe project aims to preserve and revive this beloved game
|
||||
for fans to continue enjoying.
|
||||
</p>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-start mt-4">
|
||||
<a href="https://github.com/DarkflameUniverse/DarkflameServer" target="_blank" class="btn btn-primary">
|
||||
<i class="bi bi-github"></i> GitHub Repository
|
||||
</a>
|
||||
<a href="https://github.com/DarkflameUniverse/DarkflameServer/tree/main/docs" target="_blank" class="btn btn-secondary">
|
||||
<i class="bi bi-book"></i> Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user