Compare commits

...

71 Commits

Author SHA1 Message Date
Aaron Kimbrell
8372202d8f WIP working state 2026-02-26 09:56:11 -06:00
Aaron Kimbrell
f1847d1f20 WIP: basic server, no features 2026-01-25 22:33:51 -06:00
David Markowitz
c723ce2588 fix: donations requiring new high score vs adding to previous one (#1951)
tested that jawbox works as intended now for donation counting on the leaderboards
2026-01-13 22:48:29 -08:00
Terrev
66b7d3606e fix: flower activity (#1950) 2025-12-26 13:58:10 -08:00
David Markowitz
40fef36530 fix: coins dropping on killer (#1948)
tested that when a player dies the coins spawn on their body instead
2025-12-13 22:00:58 -08:00
David Markowitz
bf020baa17 fix: temp fixes for ghosting so I can continue being on break (#1947)
* fix: temp fixes for ghosting so I can continue being on break

disables the ghost feature for now so i can continue my break

* Update GhostComponent.cpp
2025-12-08 20:39:33 -08:00
David Markowitz
a713216540 fix: saving gm invis for non gms (#1940) 2025-11-18 22:04:07 -06:00
David Markowitz
ea86a708e4 fix: uninitialized memory (#1937) 2025-11-18 19:06:03 -08:00
David Markowitz
ca7424cbeb fix: chest loot not working (#1933)
* fix bons and dragon loot

* fix chest server loot
2025-11-16 16:17:49 -06:00
David Markowitz
991e55f305 feat: dont drop loot for dead players if configured in the zone activity settings (#1935)
* feat: dont drop loot for dead players if configured in the zone activity settings

* fix errors

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dGame/dComponents/ActivityComponent.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update dGame/dUtilities/Loot.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 16:17:26 -06:00
David Markowitz
5410acffaa fix: chat server crash (#1931)
* fix: chat server crash

* Update dChatServer/ChatServer.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 13:48:57 -08:00
David Markowitz
86f8601bbd feat: various debug command improvements (#1934)
* feat: various debug command improvements

* add missing utility function

* Update dGame/dUtilities/SlashCommands/DEVGMCommands.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-16 13:48:16 -08:00
David Markowitz
4658318a3a fix: deactivate bubble buff from server too (#1936) 2025-11-16 13:46:59 -08:00
Aaron Kimbrell
11d44ffb98 feat: proper gminvs with ghosting (#1920)
* feat: proper gminvis ghosting

* address feedback

---------

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-11-15 16:43:33 -08:00
David Markowitz
2fb16420f3 fix: ape anchor not respawning (#1927)
* fix: ape anchor not respawning

* add return

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-15 13:30:02 -08:00
David Markowitz
96089a8d9a fix: fb race activityid (#1929) 2025-11-15 13:29:11 -08:00
David Markowitz
eac50acfcc fix: correct mission tracking (#1930)
checked that live captures did not track achievements in this count
2025-11-15 13:29:03 -08:00
David Markowitz
ca60787055 fix: ffa -> shared loot for activities (#1925) 2025-10-26 01:01:21 -07:00
David Markowitz
396dcb0465 feat: add logger feature to log on function entry and exit (#1924)
* feat: add logger feature to log on function entry and exit

* i didnt save the file
2025-10-25 14:53:49 -05:00
David Markowitz
6e545eb1b9 Update Loot.cpp (#1923) 2025-10-24 21:53:00 -07:00
David Markowitz
46aac016fd fix: unintended stopping (#1922) 2025-10-23 23:41:16 -05:00
David Markowitz
83823fa64f fix: resurrect not available for non-gms (#1919) 2025-10-20 23:05:22 -07:00
David Markowitz
0dd504c803 feat: behavior states (#1918) 2025-10-20 01:16:36 -05:00
David Markowitz
a70c365c23 feat banana (#1917) 2025-10-19 14:00:14 -05:00
David Markowitz
281d9762ef fix: tac arc sorting and target acquisition (#1916) 2025-10-19 07:23:54 -05:00
David Markowitz
002aa896d8 feat: debug information (#1915) 2025-10-19 07:22:45 -05:00
David Markowitz
f3a5f60d81 feat: more destroyable debug info (#1912)
* feat: more destroyable info

* Change type and remove duplicate value
2025-10-16 14:15:02 -05:00
David Markowitz
4c9c773ec5 fix: powerup drops and hardcore loot drops (#1914)
tested the following are now functional
ag buff station
tiki torch
ve rocket part boxes
ns statue
property behavior
extra items from full inventory
hardcore drops (items and coins)
2025-10-16 14:13:38 -05:00
David Markowitz
ec6253c80c fix: coin dupe on same team (#1911)
* feat: Loot rework

* Allow dupe powerup pickups

* change default team loot to shared

* fix: coin dupe on team
2025-10-15 22:36:45 -05:00
Aaron Kimbrell
c2dba31f70 fix: bbb splitting dupe issue (#1908)
* fix bbb group splitting issues

* address feedback
2025-10-15 16:45:09 -07:00
David Markowitz
74630b56c8 feat: Loot rework (#1909)
* feat: Loot rework

* Allow dupe powerup pickups

* change default team loot to shared
2025-10-15 00:53:39 -05:00
David Markowitz
fd6029ae10 feat: read from server macros folder as well (#1906) 2025-10-11 15:33:38 -07:00
David Markowitz
ff645a6662 feat: model debug (#1907) 2025-10-11 15:33:28 -07:00
David Markowitz
e051229fb6 feat: InventoryComponent debug info (#1902) 2025-10-11 00:58:52 -05:00
Aaron Kimbrell
ce28834dce feat: lxfml splitting for bbb (#1877)
* LXFML SPLITTING
Included test file

* move base to global namespace

* wip need to test

* update last fixes

* update world sending bbb to be more efficient

* Address feedback form Emo in doscord

* Make LXFML class for robust and add more tests to edge cases and malformed data

* get rid of the string copy and make the deep clone have a recursive limit

* cleanup tests

* fix test file locations

* fix file path

* KISS

* add cmakelists

* fix typos

* NL @ EOF

* tabs and split out to func

* naming standard
2025-10-10 23:07:16 -05:00
David Markowitz
cbdd5d9bc6 fix: dying while dead (#1905) 2025-10-10 01:15:21 -05:00
David Markowitz
62ac65c520 feat: Mission Component debug (#1901)
* feat: Mission Component debug

* Add player argument to inspect command

* Add completion details

* Remove unlocalized server string

done on client instead
2025-10-05 22:13:27 -05:00
HailStorm32
5d5bce53d0 feat: Add configurable restrictions for muted accounts (#1887)
* Add configurable restrictions for muted accounts

* switched to and updated GetRandomElement

* Update config option check

* implement cached config values for mute settings and update handlers

* Address review

* Update dGame/dComponents/PetComponent.cpp

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

* Update dGame/dComponents/PetComponent.cpp

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

* reduce if argument chain

---------

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-10-05 22:09:43 -05:00
David Markowitz
5791c55a9e fix: the exploding script is the most amazing piece of code i have ev… (#1900)
* fix: the exploding script is the most amazing piece of code i have ever had the pleasure of working with and has been amazing to work on and translate from lua

hahahahahahahahahahwwwwwwwwwwwwwwww草

* Enhance hit detection with proximity object checks

Refactor hit handling to include proximity checks for destroyable entities.
2025-10-05 00:19:46 -07:00
David Markowitz
17d0c45382 fix: why oh why is the aggro radius apart of the enemy (#1899) 2025-10-04 20:45:42 -07:00
David Markowitz
7dbbef81ac fix: regenerated proxy items dont need new ids and fix equip item ids (#1897)
* fix: changed item ids not reflected in equipped items

* dont do it for proxy items
2025-10-04 18:42:34 -07:00
David Markowitz
06958cb9cd feat: hardcore limit % coins dropped on death (#1898)
* feat: hardcore limit % coins dropped on death

Update EntityManager.cpp

* fix log msg
2025-10-04 17:25:23 -07:00
David Markowitz
69b1a694a6 fix: ignore foreign key checks more (#1895)
fixes an issue if you delete users in an earlier build of dlu.
2025-10-04 13:57:16 -05:00
David Markowitz
b2609ff6cb fix: live accurate player flag missions and flag debugging (#1894)
* feat: Add component ID to root component object

* fix: live accurate player flag missions and flag debugging

Tested that the client reflects the correct server progression after a test map and manually setting a flag off.
tested that session flags correctly pick up on progression updates

* banana
2025-10-04 01:07:52 -05:00
David Markowitz
e8c0b3e6da feat: Add component ID to root component object (#1893) 2025-10-03 20:57:42 -05:00
David Markowitz
25418fd8b2 fix: exploding asset bugs (#1890) 2025-10-01 20:48:08 -05:00
David Markowitz
502c965d97 feat: script debug info (#1891) 2025-10-01 14:21:25 -05:00
David Markowitz
205c190c61 fix: proxy items not equipping on login (#1892)
tested that items now equip on login
2025-10-01 07:55:51 -05:00
David Markowitz
670cb124c0 Initialize m_ActivityInfo with default constructor (#1889) 2025-09-30 23:33:04 -05:00
David Markowitz
76c2f380bf feat: re-write persistent object ID tracker (#1888)
* feat: re-write persistent object ID tracker

Features:
- Remove random objectIDs entirely
- Replace random objectIDs with persistentIDs
- Remove the need to contact the MASTER server for a persistent ID
- Add persistent ID logic to WorldServers that use transactions to guarantee unique IDs no matter when they are generated
- Default character xml version to be the most recent one

Fixes:
- Return optional from GetModel (and check for nullopt where it may exist)
- Regenerate inventory item ids on first login to be unique item IDs (fixes all those random IDs

Pet IDs and subkeys are left alone and are assumed to be reserved (checks are there to prevent this)
There is also duplicate check logic in place for properties and UGC/Models

* Update comment and log

* fix: sqlite transaction bug

* fix colliding temp item ids

temp items should not be saved. would cause issues between worlds as experienced before this commit
2025-09-29 08:54:37 -05:00
David Markowitz
b5a3cc9187 fix: Extend saved ugc id to 64 bits (#1885)
Tetsed that ugc models are saved and loaded with the correct bits
2025-09-24 06:01:46 -05:00
David Markowitz
74e1d36bb1 feat: Hardcore mode settings (#1884) 2025-09-23 07:02:29 -05:00
David Markowitz
64faac714c feat: Remove PERSISTENT ObjectID bit because it's not an ObjectID bit (#1881)
* feat: Remove PERSISTENT ObjectID bit because it's not an ObjectID bit

TODO: Need to add character save migration for the pet subkey in the inventory
Tested that the migrations work on mysql and sqlite and that properties have all their contents as before.
Need to test pets still

* fix: ugc, pet ids. remove persistent bit
2025-09-22 23:41:38 -05:00
David Markowitz
4a5dd68e87 fix: hardcore mode fixes (#1882)
fixes hardcore modes uscore drops
adds config option for excluded item drops
2025-09-21 18:36:32 -07:00
David Markowitz
4a577f233d fix: hardcore mode fixes (#1883)
fixes hardcore modes uscore drops
adds config option for excluded item drops

f

feat: Add logging for config options on load and reload

feat: Add logging for config options on load and reload
2025-09-21 20:12:50 -05:00
David Markowitz
bb05b3ac0d feat: Add logging for testing Johnny missions (#1880) 2025-09-20 17:22:16 -07:00
David Markowitz
06022e4b19 fix: use after free in TCPInterface (#1879)
lol
2025-09-19 01:12:34 -05:00
David Markowitz
6389876c6e feat: convert character ids to 64 bits (#1878)
* feat: convert character ids to 64 bits

remove all usages of the PERSISTENT bit with regards to storing of playerIDs on the server.  the bit does not exist and was a phantom in the first place.
Tested that a full playthrough of ag, ns and gf was still doable.  slash commands work, ugc works, friends works, ignore list works, properties work and have names, teaming works.
migrating an old mysql database works . need to test an old sqlite database

* fix sqlite migration

* remove nd specific column migration
2025-09-19 01:12:23 -05:00
David Markowitz
68f2e2dee2 Fix FetchContent_Declare speed (#1875) 2025-09-12 03:32:15 -05:00
HailStorm32
b798da8ef8 fix: Update mute expiry from database (#1871)
* Update mute expiry from database

* Address review comments

* Address review comment

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

---------

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-09-08 23:07:08 -07:00
Copilot
154112050f feat: Implement Minecraft-style execute command with relative positioning (#1864)
* Initial plan

* Implement Minecraft-style execute command

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>

* Add relative positioning support to execute command using ~ syntax

Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>

* update the parsing and fix chat response

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com>
Co-authored-by: Aaron Kimbrell <aronwk.aaron@gmail.com>
2025-09-08 22:35:18 -07:00
David Markowitz
6d3bf2fdc3 fix: need to create account twice due to commit latency?? (#1873)
idk fixes the issue
2025-09-08 22:50:22 -05:00
David Markowitz
566a18df38 Show git download progress with FetchContent (#1869)
Some downloads are slower and showing progress is better than sitting there doing nothing
2025-09-07 20:03:00 -05:00
David Markowitz
f6c13d9ee6 Replace Quaternion with glm math (#1868) 2025-09-06 19:18:03 -07:00
David Markowitz
8198ad70f6 fix: zero out component in destructor (#1863) 2025-09-01 19:06:00 -05:00
Gie "Max" Vanommeslaeghe
4c3bace601 Merge pull request #1862 from DarkflameUniverse/fix-item-exploits
fix: item exploits
2025-09-01 22:33:07 +02:00
David Markowitz
6d2a21450b fix item exploits
Update VendorComponent.cpp

Update Mail.cpp
2025-09-01 13:17:44 -07:00
David Markowitz
f9e74e6994 Add more logging around rewarding items (#1861) 2025-08-31 20:33:16 -07:00
jadebenn
21a2ddcfd9 Merge pull request #1856 from DarkflameUniverse/revert-uint8_t
Revert uint8_t in game message tests
2025-08-21 08:57:35 -05:00
jadebenn
50e6cf9059 revert ServiceType test change 2025-08-21 08:00:20 -05:00
jadebenn
3364884126 Consolidate serviceID enums into one enum (#1855)
* merge ServerType and ServiceID enums

* rename eConnectionType to ServiceType in preparation for enum unification

* unify ServiceID and ServiceType enums

* shrink ServiceType to an 8-bit integer

* fix linux compilation error and update gamemsg test

* return to uint16_t

* Update dNet/AuthPackets.cpp

Use cast instead of padding

Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>

* Add default case to MasterServer.cpp

* move ref back to type

* Another formatting fix

* Fix comment to be more accurate

---------

Co-authored-by: jadebenn <9892985+jadebenn@users.noreply.github.com>
Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com>
2025-08-20 20:26:48 -07:00
425 changed files with 15745 additions and 2652 deletions

29
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,29 @@
# GitHub Copilot Instructions
* c++20 standard, please use the latest features except NO modules.
* use `.contains` for searching in associative containers
* use const as much as possible. If it can be const, it should be made const
* DO NOT USE const_cast EVER.
* use `cstdint` bitwidth types ALWAYS for integral types.
* NEVER use std::wstring. If wide strings are necessary, use std::u16string with conversion utilties in GeneralUtils.h.
* Functions are ALWAYS PascalCase.
* local variables are camelCase
* NEVER use snake case
* indentation is TABS, not SPACES.
* TABS are 4 spaces by default
* Use trailing braces ALWAYS
* global variables are prefixed with `g_`
* if global variables or functions are needed, they should be located in an anonymous namespace
* Use `GeneralUtils::TryParse` for ANY parsing of strings to integrals.
* Use brace initialization when possible.
* ALWAYS default initialize variables.
* Pointers should be avoided unless necessary. Use references when the pointer has been checked and should not be null
* headers should be as compact as possible. Do NOT include extra data that isnt needed.
* Remember to include logs (LOG macro uses printf style logging) while putting verbose logs under LOG_DEBUG.
* NEVER USE `RakNet::BitStream::ReadBit`
* NEVER assume pointers are good, always check if they are null. Once a pointer is checked and is known to be non-null, further accesses no longer need checking
* Be wary of TOCTOU. Prevent all possible issues relating to TOCTOU.
* new memory allocations should never be used unless absolutely necessary.
* new for reconstruction of objects is allowed
* Prefer following the format of the file over correct formatting. Consistency over correctness.
* When using auto, ALWAYS put a * for pointers.

View File

@@ -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)
@@ -109,6 +110,8 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
find_package(MariaDB)
find_package(OpenSSL REQUIRED)
# Create a /resServer directory
make_directory(${CMAKE_BINARY_DIR}/resServer)
@@ -125,7 +128,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" "worldconfig.ini" "masterconfig.ini" "dashboardconfig.ini" "blocklist.dcf")
message(STATUS "Checking resource file integrity")
include(Utils)
@@ -306,7 +309,7 @@ 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)
@@ -321,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(

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,6 @@ add_compile_definitions(ChatServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}
add_library(dChatServer ${DCHATSERVER_SOURCES})
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer" "${PROJECT_SOURCE_DIR}/dChatFilter")
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter)
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter glm::glm)
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose dWeb)

View File

@@ -13,11 +13,11 @@
// 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 MessageType::Client type) {
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
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,7 +43,6 @@ 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);
}
}
@@ -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,7 +155,7 @@ 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;

View File

@@ -12,7 +12,7 @@
#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"
@@ -35,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
@@ -61,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());
@@ -161,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
@@ -318,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);
@@ -375,10 +371,10 @@ 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());
@@ -398,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());
@@ -533,7 +529,7 @@ void ChatPacketHandler::OnAchievementNotify(RakNet::BitStream& bitstream, const
LOG_DEBUG("Sending achievement notify to %s", notify.targetPlayerName.GetAsString().c_str());
RakNet::BitStream worldStream;
BitStreamUtils::WriteHeader(worldStream, eConnectionType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
BitStreamUtils::WriteHeader(worldStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
worldStream.Write(friendData.playerID);
notify.WriteHeader(worldStream);
notify.Serialize(worldStream);
@@ -544,10 +540,10 @@ void ChatPacketHandler::OnAchievementNotify(RakNet::BitStream& bitstream, const
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
@@ -579,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();
@@ -616,11 +612,11 @@ 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.
@@ -630,11 +626,11 @@ void ChatPacketHandler::SendFriendRequest(const PlayerData& receiver, const Play
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.worldServerSysAddr != UNASSIGNED_SYSTEM_ADDRESS);
@@ -653,11 +649,11 @@ void ChatPacketHandler::SendFriendResponse(const PlayerData& receiver, const Pla
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));

View File

@@ -13,7 +13,7 @@
#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"
@@ -59,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:
@@ -123,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);
@@ -201,8 +202,11 @@ int main(int argc, char** argv) {
//Delete our objects here:
Database::Destroy("ChatServer");
delete Game::server;
Game::server = nullptr;
delete Game::logger;
Game::logger = nullptr;
delete Game::config;
Game::config = nullptr;
return EXIT_SUCCESS;
}
@@ -218,11 +222,11 @@ 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

View File

@@ -19,23 +19,24 @@
#include "eGameMasterLevel.h"
#include "dChatFilter.h"
#include "TeamContainer.h"
#include "HTTPContext.h"
using json = nlohmann::json;
void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
void HandleHTTPPlayersRequest(HTTPReply& reply, const HTTPContext& context) {
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 HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) {
void HandleHTTPTeamsRequest(HTTPReply& reply, const HTTPContext& context) {
const json data = TeamContainer::GetTeamContainer();
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
}
void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
auto data = GeneralUtils::TryParse<json>(body);
void HandleHTTPAnnounceRequest(HTTPReply& reply, const HTTPContext& context) {
auto data = GeneralUtils::TryParse<json>(context.body);
if (!data) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
@@ -96,18 +97,21 @@ namespace ChatWeb {
Game::web.RegisterHTTPRoute({
.path = v1_route + "players",
.method = eHTTPMethod::GET,
.middleware = {},
.handle = HandleHTTPPlayersRequest
});
Game::web.RegisterHTTPRoute({
.path = v1_route + "teams",
.method = eHTTPMethod::GET,
.middleware = {},
.handle = HandleHTTPTeamsRequest
});
Game::web.RegisterHTTPRoute({
.path = v1_route + "announce",
.method = eHTTPMethod::POST,
.middleware = {},
.handle = HandleHTTPAnnounceRequest
});

View File

@@ -8,7 +8,7 @@
#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"
@@ -147,7 +147,7 @@ void PlayerContainer::MuteUpdate(Packet* packet) {
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);

View File

@@ -264,11 +264,11 @@ void TeamContainer::HandleTeamStatusRequest(Packet* packet) {
void TeamContainer::SendTeamInvite(const PlayerData& receiver, const PlayerData& sender) {
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::TEAM_INVITE);
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::TEAM_INVITE);
bitStream.Write(LUWString(sender.playerName.c_str()));
bitStream.Write(sender.playerID);
@@ -279,7 +279,7 @@ void TeamContainer::SendTeamInvite(const PlayerData& receiver, const PlayerData&
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, 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:
@@ -306,7 +306,7 @@ void TeamContainer::SendTeamInviteConfirm(const PlayerData& receiver, bool bLead
void TeamContainer::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);
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
bitStream.Write(receiver.playerID);
//portion that will get routed:
@@ -331,7 +331,7 @@ void TeamContainer::SendTeamStatus(const PlayerData& receiver, LWOOBJID i64Leade
void TeamContainer::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64PlayerID) {
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:
@@ -348,7 +348,7 @@ void TeamContainer::SendTeamSetLeader(const PlayerData& receiver, LWOOBJID i64Pl
void TeamContainer::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);
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET);
bitStream.Write(receiver.playerID);
//portion that will get routed:
@@ -377,7 +377,7 @@ void TeamContainer::SendTeamAddPlayer(const PlayerData& receiver, bool bIsFreeTr
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, 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:
@@ -403,7 +403,7 @@ void TeamContainer::SendTeamRemovePlayer(const PlayerData& receiver, bool bDisba
void TeamContainer::SendTeamSetOffWorldFlag(const PlayerData& receiver, LWOOBJID i64PlayerID, LWOZONEID zoneID) {
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:
@@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
}
}
newTeam->lootFlag = 1;
newTeam->lootFlag = 0;
TeamStatusUpdate(newTeam);
@@ -652,7 +652,7 @@ void TeamContainer::TeamStatusUpdate(TeamData* team) {
void TeamContainer::UpdateTeamsOnWorld(TeamData* team, bool deleteTeam) {
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::TEAM_GET_STATUS);
BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::TEAM_GET_STATUS);
bitStream.Write(team->teamID);
bitStream.Write(deleteTeam);

View File

@@ -374,6 +374,21 @@ public:
return value->Insert<AmfType>("value", std::make_unique<AmfType>());
}
AMFArrayValue& PushDebug(const NiPoint3& point) {
PushDebug<AMFDoubleValue>("X") = point.x;
PushDebug<AMFDoubleValue>("Y") = point.y;
PushDebug<AMFDoubleValue>("Z") = point.z;
return *this;
}
AMFArrayValue& PushDebug(const NiQuaternion& rot) {
PushDebug<AMFDoubleValue>("W") = rot.w;
PushDebug<AMFDoubleValue>("X") = rot.x;
PushDebug<AMFDoubleValue>("Y") = rot.y;
PushDebug<AMFDoubleValue>("Z") = rot.z;
return *this;
}
private:
/**
* The associative portion. These values are key'd with strings to an AMFValue.

View File

@@ -54,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.
@@ -74,5 +76,6 @@ else ()
endif ()
target_link_libraries(dCommon
PUBLIC glm::glm
PRIVATE ZLIB::ZLIB bcrypt tinyxml2
INTERFACE dDatabase)

View File

@@ -3,7 +3,7 @@
// C++
#include <charconv>
#include <cstdint>
#include <cmath>
#include <cmath>
#include <ctime>
#include <functional>
#include <optional>
@@ -19,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;
@@ -244,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;
@@ -252,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;
}
/**
@@ -261,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>
@@ -303,7 +306,7 @@ namespace GeneralUtils {
template<typename Container>
inline Container::value_type GetRandomElement(const Container& container) {
DluAssert(!container.empty());
return container[GenerateRandomNumber<typename Container::value_type>(0, container.size() - 1)];
return container[GenerateRandomNumber<typename Container::size_type>(0, container.size() - 1)];
}
/**

View File

@@ -96,3 +96,17 @@ bool Logger::GetLogToConsole() const {
}
return toReturn;
}
FuncEntry::FuncEntry(const char* funcName, const char* fileName, const uint32_t line) {
m_FuncName = funcName;
if (!m_FuncName) m_FuncName = "Unknown";
m_Line = line;
m_FileName = fileName;
LOG("--> %s::%s:%i", m_FileName, m_FuncName, m_Line);
}
FuncEntry::~FuncEntry() {
if (!m_FuncName || !m_FileName) return;
LOG("<-- %s::%s:%i", m_FileName, m_FuncName, m_Line);
}

View File

@@ -32,6 +32,19 @@ constexpr const char* GetFileNameFromAbsolutePath(const char* 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)
// Place this right at the start of a function. Will log a message when called and then once you leave the function.
#define LOG_ENTRY auto str_ = GetFileNameFromAbsolutePath(__FILE__); FuncEntry funcEntry_(__FUNCTION__, str_, __LINE__)
class FuncEntry {
public:
FuncEntry(const char* funcName, const char* fileName, const uint32_t line);
~FuncEntry();
private:
const char* m_FuncName = nullptr;
const char* m_FileName = nullptr;
uint32_t m_Line = 0;
};
// Writer class for writing data to files.
class Writer {
public:

View File

@@ -5,13 +5,43 @@
#include "TinyXmlUtils.h"
#include <ranges>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <sstream>
namespace {
// The base LXFML xml file to use when creating new models.
std::string g_base = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<LXFML versionMajor="5" versionMinor="0">
<Meta>
<Application name="LEGO Universe" versionMajor="0" versionMinor="0"/>
<Brand name="LEGOUniverse"/>
<BrickSet version="457"/>
</Meta>
<Bricks>
</Bricks>
<RigidSystems>
</RigidSystems>
<GroupSystems>
<GroupSystem>
</GroupSystem>
</GroupSystems>
</LXFML>)";
}
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
Result toReturn;
// Handle empty or invalid input
if (data.empty()) {
return toReturn;
}
tinyxml2::XMLDocument doc;
const auto err = doc.Parse(data.data());
// Use length-based parsing to avoid expensive string copy
const auto err = doc.Parse(data.data(), data.size());
if (err != tinyxml2::XML_SUCCESS) {
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
return toReturn;
}
@@ -20,7 +50,6 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
auto lxfml = reader["LXFML"];
if (!lxfml) {
LOG("Failed to find LXFML element.");
return toReturn;
}
@@ -49,16 +78,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
// 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 (split.size() < 12) continue;
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) continue;
auto x = xOpt.value();
auto y = yOpt.value();
auto z = zOpt.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;
@@ -87,13 +119,19 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
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;
auto xOpt = GeneralUtils::TryParse<float>(split[9]);
auto yOpt = GeneralUtils::TryParse<float>(split[10]);
auto zOpt = GeneralUtils::TryParse<float>(split[11]);
if (!xOpt.has_value() || !yOpt.has_value() || !zOpt.has_value()) {
continue;
}
auto x = xOpt.value() - newRootPos.x + curPosition.x;
auto y = yOpt.value() - newRootPos.y + curPosition.y;
auto z = zOpt.value() - newRootPos.z + curPosition.z;
std::stringstream stream;
for (int i = 0; i < 9; i++) {
stream << split[i];
@@ -128,3 +166,345 @@ Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoin
toReturn.center = newRootPos;
return toReturn;
}
// Deep-clone an XMLElement (attributes, text, and child elements) into a target document
// with maximum depth protection to prevent infinite loops
static tinyxml2::XMLElement* CloneElementDeep(const tinyxml2::XMLElement* src, tinyxml2::XMLDocument& dstDoc, int maxDepth = 100) {
if (!src || maxDepth <= 0) return nullptr;
auto* dst = dstDoc.NewElement(src->Name());
// copy attributes
for (const tinyxml2::XMLAttribute* attr = src->FirstAttribute(); attr; attr = attr->Next()) {
dst->SetAttribute(attr->Name(), attr->Value());
}
// copy children (elements and text)
for (const tinyxml2::XMLNode* child = src->FirstChild(); child; child = child->NextSibling()) {
if (const tinyxml2::XMLElement* childElem = child->ToElement()) {
// Recursively clone child elements with decremented depth
auto* clonedChild = CloneElementDeep(childElem, dstDoc, maxDepth - 1);
if (clonedChild) dst->InsertEndChild(clonedChild);
} else if (const tinyxml2::XMLText* txt = child->ToText()) {
auto* n = dstDoc.NewText(txt->Value());
dst->InsertEndChild(n);
} else if (const tinyxml2::XMLComment* c = child->ToComment()) {
auto* n = dstDoc.NewComment(c->Value());
dst->InsertEndChild(n);
}
}
return dst;
}
std::vector<Lxfml::Result> Lxfml::Split(const std::string_view data, const NiPoint3& curPosition) {
std::vector<Result> results;
// Handle empty or invalid input
if (data.empty()) {
return results;
}
// Prevent processing extremely large inputs that could cause hangs
if (data.size() > 10000000) { // 10MB limit
return results;
}
tinyxml2::XMLDocument doc;
// Use length-based parsing to avoid expensive string copy
const auto err = doc.Parse(data.data(), data.size());
if (err != tinyxml2::XML_SUCCESS) {
return results;
}
auto* lxfml = doc.FirstChildElement("LXFML");
if (!lxfml) {
return results;
}
// Build maps: partRef -> Part element, partRef -> Brick element, boneRef -> partRef, brickRef -> Brick element
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToPart;
std::unordered_map<std::string, tinyxml2::XMLElement*> partRefToBrick;
std::unordered_map<std::string, std::string> boneRefToPartRef;
std::unordered_map<std::string, tinyxml2::XMLElement*> brickByRef;
auto* bricksParent = lxfml->FirstChildElement("Bricks");
if (bricksParent) {
for (auto* brick = bricksParent->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
const char* brickRef = brick->Attribute("refID");
if (brickRef) brickByRef.emplace(std::string(brickRef), brick);
for (auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
const char* partRef = part->Attribute("refID");
if (partRef) {
partRefToPart.emplace(std::string(partRef), part);
partRefToBrick.emplace(std::string(partRef), brick);
}
auto* bone = part->FirstChildElement("Bone");
if (bone) {
const char* boneRef = bone->Attribute("refID");
if (boneRef) boneRefToPartRef.emplace(std::string(boneRef), partRef ? std::string(partRef) : std::string());
}
}
}
}
// Collect RigidSystem elements
std::vector<tinyxml2::XMLElement*> rigidSystems;
auto* rigidSystemsParent = lxfml->FirstChildElement("RigidSystems");
if (rigidSystemsParent) {
for (auto* rs = rigidSystemsParent->FirstChildElement("RigidSystem"); rs; rs = rs->NextSiblingElement("RigidSystem")) {
rigidSystems.push_back(rs);
}
}
// Collect top-level groups (immediate children of GroupSystem)
std::vector<tinyxml2::XMLElement*> groupRoots;
auto* groupSystemsParent = lxfml->FirstChildElement("GroupSystems");
if (groupSystemsParent) {
for (auto* gs = groupSystemsParent->FirstChildElement("GroupSystem"); gs; gs = gs->NextSiblingElement("GroupSystem")) {
for (auto* group = gs->FirstChildElement("Group"); group; group = group->NextSiblingElement("Group")) {
groupRoots.push_back(group);
}
}
}
// Track used bricks and rigidsystems
std::unordered_set<std::string> usedBrickRefs;
std::unordered_set<tinyxml2::XMLElement*> usedRigidSystems;
// Track used groups to avoid processing them twice
std::unordered_set<tinyxml2::XMLElement*> usedGroups;
// Helper to create output document from sets of brick refs and rigidsystem pointers
auto makeOutput = [&](const std::unordered_set<std::string>& bricksToInclude, const std::vector<tinyxml2::XMLElement*>& rigidSystemsToInclude, const std::vector<tinyxml2::XMLElement*>& groupsToInclude = {}) {
tinyxml2::XMLDocument outDoc;
outDoc.Parse(g_base.c_str());
auto* outRoot = outDoc.FirstChildElement("LXFML");
auto* outBricks = outRoot->FirstChildElement("Bricks");
auto* outRigidSystems = outRoot->FirstChildElement("RigidSystems");
auto* outGroupSystems = outRoot->FirstChildElement("GroupSystems");
// clone and insert bricks
for (const auto& bref : bricksToInclude) {
auto it = brickByRef.find(bref);
if (it == brickByRef.end()) continue;
tinyxml2::XMLElement* cloned = CloneElementDeep(it->second, outDoc);
if (cloned) outBricks->InsertEndChild(cloned);
}
// clone and insert rigidsystems
for (auto* rsPtr : rigidSystemsToInclude) {
tinyxml2::XMLElement* cloned = CloneElementDeep(rsPtr, outDoc);
if (cloned) outRigidSystems->InsertEndChild(cloned);
}
// clone and insert group(s) if requested
if (outGroupSystems && !groupsToInclude.empty()) {
// clear default children
while (outGroupSystems->FirstChild()) outGroupSystems->DeleteChild(outGroupSystems->FirstChild());
// create a GroupSystem element and append requested groups
auto* newGS = outDoc.NewElement("GroupSystem");
for (auto* gptr : groupsToInclude) {
tinyxml2::XMLElement* clonedG = CloneElementDeep(gptr, outDoc);
if (clonedG) newGS->InsertEndChild(clonedG);
}
outGroupSystems->InsertEndChild(newGS);
}
// Print to string
tinyxml2::XMLPrinter printer;
outDoc.Print(&printer);
// Normalize position and compute center using existing helper
std::string xmlString = printer.CStr();
if (xmlString.size() > 5000000) { // 5MB limit for normalization
Result emptyResult;
emptyResult.lxfml = xmlString;
return emptyResult;
}
auto normalized = NormalizePosition(xmlString, curPosition);
return normalized;
};
// 1) Process groups (each top-level Group becomes one output; nested groups are included)
for (auto* groupRoot : groupRoots) {
// Skip if this group was already processed as part of another group
if (usedGroups.find(groupRoot) != usedGroups.end()) continue;
// Helper to collect all partRefs in a group's subtree
std::function<void(const tinyxml2::XMLElement*, std::unordered_set<std::string>&)> collectParts = [&](const tinyxml2::XMLElement* g, std::unordered_set<std::string>& partRefs) {
if (!g) return;
const char* partAttr = g->Attribute("partRefs");
if (partAttr) {
for (auto& tok : GeneralUtils::SplitString(partAttr, ',')) partRefs.insert(tok);
}
for (auto* child = g->FirstChildElement("Group"); child; child = child->NextSiblingElement("Group")) collectParts(child, partRefs);
};
// Collect all groups that need to be merged into this output
std::vector<tinyxml2::XMLElement*> groupsToInclude{ groupRoot };
usedGroups.insert(groupRoot);
// Build initial sets of bricks and boneRefs from the starting group
std::unordered_set<std::string> partRefs;
collectParts(groupRoot, partRefs);
std::unordered_set<std::string> bricksIncluded;
std::unordered_set<std::string> boneRefsIncluded;
for (const auto& pref : partRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
auto partIt = partRefToPart.find(pref);
if (partIt != partRefToPart.end()) {
auto* bone = partIt->second->FirstChildElement("Bone");
if (bone) {
const char* bref = bone->Attribute("refID");
if (bref) boneRefsIncluded.insert(std::string(bref));
}
}
}
// Iteratively include any RigidSystems that reference any boneRefsIncluded
// and check if those rigid systems' bricks span other groups
bool changed = true;
std::vector<tinyxml2::XMLElement*> rigidSystemsToInclude;
int maxIterations = 1000; // Safety limit to prevent infinite loops
int iteration = 0;
while (changed && iteration < maxIterations) {
changed = false;
iteration++;
// First, expand rigid systems based on current boneRefsIncluded
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
// parse boneRefs of this rigid system (from its <Rigid> children)
bool intersects = false;
std::vector<std::string> rsBoneRefs;
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
rsBoneRefs.push_back(tok);
if (boneRefsIncluded.find(tok) != boneRefsIncluded.end()) intersects = true;
}
}
if (!intersects) continue;
// include this rigid system and all boneRefs it references
usedRigidSystems.insert(rs);
rigidSystemsToInclude.push_back(rs);
for (const auto& br : rsBoneRefs) {
boneRefsIncluded.insert(br);
auto bpIt = boneRefToPartRef.find(br);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref && bricksIncluded.insert(std::string(bref)).second) changed = true;
}
}
}
}
// Second, check if the newly included bricks span any other groups
// If so, merge those groups into the current output
for (auto* otherGroup : groupRoots) {
if (usedGroups.find(otherGroup) != usedGroups.end()) continue;
// Collect partRefs from this other group
std::unordered_set<std::string> otherPartRefs;
collectParts(otherGroup, otherPartRefs);
// Check if any of these partRefs correspond to bricks we've already included
bool spansOtherGroup = false;
for (const auto& pref : otherPartRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref && bricksIncluded.find(std::string(bref)) != bricksIncluded.end()) {
spansOtherGroup = true;
break;
}
}
}
if (spansOtherGroup) {
// Merge this group into the current output
usedGroups.insert(otherGroup);
groupsToInclude.push_back(otherGroup);
changed = true;
// Add all partRefs, boneRefs, and bricks from this group
for (const auto& pref : otherPartRefs) {
auto pit = partRefToBrick.find(pref);
if (pit != partRefToBrick.end()) {
const char* bref = pit->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
auto partIt = partRefToPart.find(pref);
if (partIt != partRefToPart.end()) {
auto* bone = partIt->second->FirstChildElement("Bone");
if (bone) {
const char* bref = bone->Attribute("refID");
if (bref) boneRefsIncluded.insert(std::string(bref));
}
}
}
}
}
}
if (iteration >= maxIterations) {
// Iteration limit reached, stop processing to prevent infinite loops
// The file is likely malformed, so just skip further processing
return results;
}
// include bricks from bricksIncluded into used set
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
// make output doc and push result (include all merged groups' XML)
auto normalized = makeOutput(bricksIncluded, rigidSystemsToInclude, groupsToInclude);
results.push_back(normalized);
}
// 2) Process remaining RigidSystems (each becomes its own file)
for (auto* rs : rigidSystems) {
if (usedRigidSystems.find(rs) != usedRigidSystems.end()) continue;
std::unordered_set<std::string> bricksIncluded;
// collect boneRefs referenced by this rigid system
for (auto* rigid = rs->FirstChildElement("Rigid"); rigid; rigid = rigid->NextSiblingElement("Rigid")) {
const char* battr = rigid->Attribute("boneRefs");
if (!battr) continue;
for (auto& tok : GeneralUtils::SplitString(battr, ',')) {
auto bpIt = boneRefToPartRef.find(tok);
if (bpIt != boneRefToPartRef.end()) {
auto partRef = bpIt->second;
auto pbIt = partRefToBrick.find(partRef);
if (pbIt != partRefToBrick.end()) {
const char* bref = pbIt->second->Attribute("refID");
if (bref) bricksIncluded.insert(std::string(bref));
}
}
}
}
// mark used
for (const auto& b : bricksIncluded) usedBrickRefs.insert(b);
usedRigidSystems.insert(rs);
std::vector<tinyxml2::XMLElement*> rsVec{ rs };
auto normalized = makeOutput(bricksIncluded, rsVec);
results.push_back(normalized);
}
// 3) Any remaining bricks not included become their own files
for (const auto& [bref, brickPtr] : brickByRef) {
if (usedBrickRefs.find(bref) != usedBrickRefs.end()) continue;
std::unordered_set<std::string> bricksIncluded{ bref };
auto normalized = makeOutput(bricksIncluded, {});
results.push_back(normalized);
usedBrickRefs.insert(bref);
}
return results;
}

View File

@@ -6,6 +6,7 @@
#include <string>
#include <string_view>
#include <vector>
#include "NiPoint3.h"
@@ -18,6 +19,7 @@ namespace Lxfml {
// 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);
[[nodiscard]] std::vector<Result> Split(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);

View File

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

View File

@@ -4,6 +4,7 @@
#endif
#include "NiQuaternion.h"
#include <glm/ext/quaternion_float.hpp>
// MARK: Getters / Setters

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,6 +81,9 @@ public:
[[nodiscard]]
AssetStream GetFile(const char* name) const;
[[nodiscard]]
AssetStream GetFile(const std::string& name) const { return GetFile(name.c_str()); };
private:
void LoadPackIndex();

View File

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

View File

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

View File

@@ -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,
@@ -29,6 +27,8 @@ namespace MessageType {
AFFIRM_TRANSFER_REQUEST,
AFFIRM_TRANSFER_RESPONSE,
NEW_SESSION_ALERT
NEW_SESSION_ALERT,
REQUEST_SERVER_LIST
};
}

View File

@@ -0,0 +1,15 @@
#ifndef __SERVICETYPE__H__
#define __SERVICETYPE__H__
enum class ServiceType : uint16_t {
COMMON = 0,
AUTH,
CHAT,
DASHBOARD,
WORLD,
CLIENT,
MASTER,
UNKNOWN
};
#endif //!__SERVICETYPE__H__

View File

@@ -10,7 +10,7 @@
#include "BitStream.h"
#include "BitStreamUtils.h"
#include "MessageType/Client.h"
#include "eConnectionType.h"
#include "ServiceType.h"
#pragma warning (disable:4251) //Disables SQL warnings
@@ -34,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);

View File

@@ -18,7 +18,9 @@ enum class eCharacterVersion : uint32_t {
SPEED_BASE,
// Fixes nexus force explorer missions
NJ_JAYMISSIONS,
UP_TO_DATE, // will become NEXUS_FORCE_EXPLORER
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__

View File

@@ -1,14 +0,0 @@
#ifndef __ECONNECTIONTYPE__H__
#define __ECONNECTIONTYPE__H__
enum class eConnectionType : uint16_t {
SERVER = 0,
AUTH,
CHAT,
WORLD = 4,
CLIENT,
MASTER,
UNKNOWN
};
#endif //!__ECONNECTIONTYPE__H__

View File

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

View File

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

View File

@@ -0,0 +1,58 @@
set(DDASHBOARDSERVER_SOURCES
"DashboardServer.cpp"
)
add_subdirectory(routes)
add_subdirectory(auth)
add_executable(DashboardServer ${DDASHBOARDSERVER_SOURCES})
target_include_directories(DashboardServer PRIVATE
"${PROJECT_SOURCE_DIR}/dCommon"
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
"${PROJECT_SOURCE_DIR}/dDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL"
"${PROJECT_SOURCE_DIR}/dNet"
"${PROJECT_SOURCE_DIR}/dWeb"
"${PROJECT_SOURCE_DIR}/dServer"
"${PROJECT_SOURCE_DIR}/thirdparty"
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
"${PROJECT_SOURCE_DIR}/dDashboardServer"
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
"${PROJECT_SOURCE_DIR}/dDashboardServer/routes"
)
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto DashboardRoutes DashboardAuth)
# Copy static files and templates to build directory (always copy)
add_custom_command(TARGET DashboardServer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove_directory
${CMAKE_BINARY_DIR}/dDashboardServer/static
COMMENT "Removing old static files"
)
add_custom_command(TARGET DashboardServer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/static
${CMAKE_BINARY_DIR}/dDashboardServer/static
COMMENT "Copying DashboardServer static files"
)
add_custom_command(TARGET DashboardServer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E remove_directory
${CMAKE_BINARY_DIR}/dDashboardServer/templates
COMMENT "Removing old templates"
)
add_custom_command(TARGET DashboardServer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/templates
${CMAKE_BINARY_DIR}/dDashboardServer/templates
COMMENT "Copying DashboardServer templates"
)

View File

@@ -0,0 +1,203 @@
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <thread>
#include <csignal>
#include <memory>
#include "CDClientDatabase.h"
#include "CDClientManager.h"
#include "Database.h"
#include "dConfig.h"
#include "Logger.h"
#include "dServer.h"
#include "AssetManager.h"
#include "BinaryPathFinder.h"
#include "ServiceType.h"
#include "MessageType/Master.h"
#include "Game.h"
#include "BitStreamUtils.h"
#include "Diagnostics.h"
#include "Web.h"
#include "Server.h"
#include "ServerState.h"
#include "APIRoutes.h"
#include "StaticRoutes.h"
#include "DashboardRoutes.h"
#include "WSRoutes.h"
#include "AuthRoutes.h"
#include "AuthMiddleware.h"
namespace Game {
Logger* logger = nullptr;
dServer* server = nullptr;
dConfig* config = nullptr;
Game::signal_t lastSignal = 0;
std::mt19937 randomEngine;
}
// Define global server state
namespace ServerState {
ServerStatus g_AuthStatus{};
ServerStatus g_ChatStatus{};
std::vector<WorldInstanceInfo> g_WorldInstances{};
std::mutex g_StatusMutex{};
}
namespace {
dServer* g_Server = nullptr;
bool g_RequestedServerList = false;
}
int main(int argc, char** argv) {
Diagnostics::SetProduceMemoryDump(true);
std::signal(SIGINT, Game::OnSignal);
std::signal(SIGTERM, Game::OnSignal);
uint32_t maxClients = 999;
uint32_t ourPort = 2006;
std::string ourIP = "127.0.0.1";
// Read config
Game::config = new dConfig("dashboardconfig.ini");
// Setup logger
Server::SetupLogger("DashboardServer");
if (!Game::logger) return EXIT_FAILURE;
Game::config->LogSettings();
LOG("Starting Dashboard Server");
// Load settings
if (Game::config->GetValue("max_clients") != "")
maxClients = std::stoi(Game::config->GetValue("max_clients"));
if (Game::config->GetValue("port") != "")
ourPort = std::atoi(Game::config->GetValue("port").c_str());
if (Game::config->GetValue("listen_ip") != "")
ourIP = Game::config->GetValue("listen_ip");
// Connect to CDClient database
try {
const std::string cdclientPath = BinaryPathFinder::GetBinaryDir() / "resServer/CDServer.sqlite";
CDClientDatabase::Connect(cdclientPath);
} catch (std::exception& ex) {
LOG("Failed to connect to CDClient database: %s", ex.what());
return EXIT_FAILURE;
}
// Connect to the database
try {
Database::Connect();
} catch (std::exception& ex) {
LOG("Failed to connect to the database: %s", ex.what());
return EXIT_FAILURE;
}
// Get master info from database
std::string masterIP = "localhost";
uint32_t masterPort = 1000;
std::string masterPassword;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
masterPassword = masterInfo->password;
}
// Setup network server for communicating with Master
g_Server = new dServer(
masterIP,
ourPort,
0,
maxClients,
false,
false,
Game::logger,
masterIP,
masterPort,
ServiceType::DASHBOARD, // Connect as dashboard to master
Game::config,
&Game::lastSignal,
masterPassword
);
// Initialize web server
if (!Game::web.Startup(ourIP, ourPort)) {
LOG("Failed to start web server on %s:%d", ourIP.c_str(), ourPort);
return EXIT_FAILURE;
}
// Register global middleware
Game::web.AddGlobalMiddleware(std::make_shared<AuthMiddleware>());
// Register routes in order: API, Static, Auth, WebSocket, Dashboard (dashboard MUST be last)
RegisterAPIRoutes();
RegisterStaticRoutes();
RegisterAuthRoutes();
RegisterWSRoutes();
RegisterDashboardRoutes(); // Must be last - catches all unmatched routes
LOG("Dashboard Server started successfully on %s:%d", ourIP.c_str(), ourPort);
LOG("Connected to Master Server at %s:%d", masterIP.c_str(), masterPort);
// Main loop
auto lastTime = std::chrono::high_resolution_clock::now();
auto lastBroadcast = lastTime;
auto currentTime = lastTime;
constexpr float deltaTime = 1.0f / 60.0f; // 60 FPS
constexpr float broadcastInterval = 2000.0f; // Broadcast every 2 seconds
while (!Game::ShouldShutdown()) {
currentTime = std::chrono::high_resolution_clock::now();
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastTime).count();
const auto elapsedSinceBroadcast = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastBroadcast).count();
if (elapsed >= 1000.0f / 60.0f) {
// // Handle master server packets
// Packet* packet = g_Server->ReceiveFromMaster();
// if (packet) {
// RakNet::BitStream bitStream(packet->data, packet->length, false);
// PacketHandler::HandlePacket(bitStream, packet->systemAddress);
// g_Server->DeallocateMasterPacket(packet);
// }
// // Handle RakNet protocol packets from connected servers
// packet = g_Server->Receive();
// while (packet) {
// RakNet::BitStream bitStream(packet->data, packet->length, false);
// PacketHandler::HandlePacket(bitStream, packet->systemAddress);
// g_Server->DeallocatePacket(packet);
// packet = g_Server->Receive();
// }
// Handle web requests
Game::web.ReceiveRequests();
// Broadcast dashboard updates periodically
if (elapsedSinceBroadcast >= broadcastInterval) {
BroadcastDashboardUpdate();
lastBroadcast = currentTime;
}
lastTime = currentTime;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// Cleanup
Database::Destroy("DashboardServer");
delete g_Server;
g_Server = nullptr;
delete Game::logger;
Game::logger = nullptr;
delete Game::config;
Game::config = nullptr;
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,132 @@
#include "AuthMiddleware.h"
#include "DashboardAuthService.h"
#include "Game.h"
#include "Logger.h"
#include <string>
#include <cctype>
// Helper to extract cookie value from header
static std::string ExtractCookieValue(const std::string& cookieHeader, const std::string& cookieName) {
std::string searchStr = cookieName + "=";
size_t pos = cookieHeader.find(searchStr);
if (pos == std::string::npos) {
return "";
}
size_t valueStart = pos + searchStr.length();
size_t valueEnd = cookieHeader.find(";", valueStart);
if (valueEnd == std::string::npos) {
valueEnd = cookieHeader.length();
}
std::string value = cookieHeader.substr(valueStart, valueEnd - valueStart);
// URL decode the value
std::string decoded;
for (size_t i = 0; i < value.length(); ++i) {
if (value[i] == '%' && i + 2 < value.length()) {
std::string hex = value.substr(i + 1, 2);
char* endptr;
int charCode = static_cast<int>(std::strtol(hex.c_str(), &endptr, 16));
if (endptr - hex.c_str() == 2) {
decoded += static_cast<char>(charCode);
i += 2;
continue;
}
}
decoded += value[i];
}
return decoded;
}
std::string AuthMiddleware::ExtractTokenFromQueryString(const std::string& queryString) {
if (queryString.empty()) {
return "";
}
// Parse query string to find token parameter
// Expected format: "?token=eyJhbGc..."
std::string tokenPrefix = "token=";
size_t tokenPos = queryString.find(tokenPrefix);
if (tokenPos == std::string::npos) {
return "";
}
// Extract token value (from "token=" to next "&" or end of string)
size_t valueStart = tokenPos + tokenPrefix.length();
size_t valueEnd = queryString.find("&", valueStart);
if (valueEnd == std::string::npos) {
valueEnd = queryString.length();
}
return queryString.substr(valueStart, valueEnd - valueStart);
}
std::string AuthMiddleware::ExtractTokenFromCookies(const std::string& cookieHeader) {
if (cookieHeader.empty()) {
return "";
}
// Extract dashboardToken cookie value
return ExtractCookieValue(cookieHeader, "dashboardToken");
}
std::string AuthMiddleware::ExtractTokenFromAuthHeader(const std::string& authHeader) {
if (authHeader.empty()) {
return "";
}
// Check for "Bearer <token>" format
if (authHeader.substr(0, 7) == "Bearer ") {
return authHeader.substr(7);
}
// Check for "Token <token>" format
if (authHeader.substr(0, 6) == "Token ") {
return authHeader.substr(6);
}
// If no prefix, assume raw token
return authHeader;
}
bool AuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
// Try to extract token from various sources (in priority order)
std::string token = ExtractTokenFromQueryString(context.queryString);
if (token.empty()) {
const std::string& cookieHeader = context.GetHeader("Cookie");
token = ExtractTokenFromCookies(cookieHeader);
}
if (token.empty()) {
const std::string& authHeader = context.GetHeader("Authorization");
token = ExtractTokenFromAuthHeader(authHeader);
}
// If we found a token, try to verify it
if (!token.empty()) {
std::string username;
uint8_t gmLevel{};
if (DashboardAuthService::VerifyToken(token, username, gmLevel)) {
context.isAuthenticated = true;
context.authenticatedUser = username;
context.gmLevel = gmLevel;
LOG_DEBUG("User %s authenticated via API token (GM level %d)", username.c_str(), gmLevel);
return true;
} else {
LOG_DEBUG("Invalid authentication token provided");
return true; // Continue - let routes decide if auth is required
}
}
// No token found - continue without authentication
// Routes can use RequireAuthMiddleware to enforce authentication
return true;
}

View File

@@ -0,0 +1,34 @@
#ifndef __AUTHMIDDLEWARE_H__
#define __AUTHMIDDLEWARE_H__
#include <string>
#include <memory>
#include "IHTTPMiddleware.h"
/**
* AuthMiddleware: Extracts and verifies authentication tokens
*
* Token extraction sources (in priority order):
* 1. Query parameter: ?token=eyJhbGc...
* 2. Cookie: dashboardToken=...
* 3. Authorization header: Bearer <token> or Token <token>
*
* Sets HTTPContext.isAuthenticated, HTTPContext.authenticatedUser,
* and HTTPContext.gmLevel if token is valid.
*/
class AuthMiddleware final : public IHTTPMiddleware {
public:
AuthMiddleware() = default;
~AuthMiddleware() override = default;
bool Process(HTTPContext& context, HTTPReply& reply) override;
std::string GetName() const override { return "AuthMiddleware"; }
private:
// Extract token from various sources
static std::string ExtractTokenFromQueryString(const std::string& queryString);
static std::string ExtractTokenFromCookies(const std::string& cookieHeader);
static std::string ExtractTokenFromAuthHeader(const std::string& authHeader);
};
#endif // !__AUTHMIDDLEWARE_H__

View File

@@ -0,0 +1,28 @@
set(DASHBOARDAUTH_SOURCES
"JWTUtils.cpp"
"DashboardAuthService.cpp"
"AuthMiddleware.cpp"
"RequireAuthMiddleware.cpp"
)
add_library(DashboardAuth STATIC ${DASHBOARDAUTH_SOURCES})
target_include_directories(DashboardAuth PRIVATE
"${PROJECT_SOURCE_DIR}/dCommon"
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
"${PROJECT_SOURCE_DIR}/dDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL"
"${PROJECT_SOURCE_DIR}/dNet"
"${PROJECT_SOURCE_DIR}/dWeb"
"${PROJECT_SOURCE_DIR}/dServer"
"${PROJECT_SOURCE_DIR}/thirdparty"
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
)
target_link_libraries(DashboardAuth PRIVATE ${COMMON_LIBRARIES} dWeb dServer bcrypt OpenSSL::Crypto)

View File

@@ -0,0 +1,144 @@
#include "DashboardAuthService.h"
#include "JWTUtils.h"
#include "Database.h"
#include "Logger.h"
#include "Game.h"
#include "dConfig.h"
#include "GeneralUtils.h"
#include <bcrypt/bcrypt.h>
#include <ctime>
namespace {
constexpr int64_t LOCKOUT_DURATION = 15 * 60; // 15 minutes in seconds
}
DashboardAuthService::LoginResult DashboardAuthService::Login(
const std::string& username,
const std::string& password,
bool rememberMe) {
LoginResult result;
if (username.empty() || password.empty()) {
result.message = "Username and password are required";
return result;
}
if (password.length() > 40) {
result.message = "Password exceeds maximum length (40 characters)";
return result;
}
try {
// Get account info
auto accountInfo = Database::Get()->GetAccountInfo(username);
if (!accountInfo) {
result.message = "Invalid username or password";
LOG_DEBUG("Login attempt for non-existent user: %s", username.c_str());
return result;
}
uint32_t accountId = accountInfo->id;
// Check if account is locked
bool isLockedOut = Database::Get()->IsLockedOut(accountId);
if (isLockedOut) {
// Record failed attempt even without checking password
Database::Get()->RecordFailedAttempt(accountId);
uint8_t failedAttempts = Database::Get()->GetFailedAttempts(accountId);
result.message = "Account is locked due to too many failed attempts";
result.accountLocked = true;
LOG("Login attempt on locked account: %s (failed attempts: %d)", username.c_str(), failedAttempts);
return result;
}
// Check password
if (::bcrypt_checkpw(password.c_str(), accountInfo->bcryptPassword.c_str()) != 0) {
// Record failed attempt
Database::Get()->RecordFailedAttempt(accountId);
uint8_t newFailedAttempts = Database::Get()->GetFailedAttempts(accountId);
// Lock account after 3 failed attempts
if (newFailedAttempts >= 3) {
int64_t lockoutUntil = std::time(nullptr) + LOCKOUT_DURATION;
Database::Get()->SetLockout(accountId, lockoutUntil);
result.message = "Account locked due to too many failed attempts";
result.accountLocked = true;
LOG("Account locked after failed attempts: %s", username.c_str());
} else {
result.message = "Invalid username or password";
LOG_DEBUG("Failed login attempt for user: %s (attempt %d/3)",
username.c_str(), newFailedAttempts);
}
return result;
}
// Check GM level
if (!HasDashboardAccess(static_cast<uint8_t>(accountInfo->maxGmLevel))) {
result.message = "Access denied: insufficient permissions";
LOG("Access denied for non-admin user: %s", username.c_str());
return result;
}
// Successful login
Database::Get()->ClearFailedAttempts(accountId);
result.success = true;
result.gmLevel = static_cast<uint8_t>(accountInfo->maxGmLevel);
result.token = JWTUtils::GenerateToken(username, result.gmLevel, rememberMe);
result.message = "Login successful";
LOG("Successful login: %s (GM Level: %d)", username.c_str(), result.gmLevel);
return result;
} catch (const std::exception& ex) {
result.message = "An error occurred during login";
LOG("Error during login process: %s", ex.what());
return result;
}
}
bool DashboardAuthService::VerifyToken(const std::string& token, std::string& username, uint8_t& gmLevel) {
JWTUtils::JWTPayload payload;
if (!JWTUtils::ValidateToken(token, payload)) {
LOG_DEBUG("Token validation failed: invalid or expired JWT");
return false;
}
username = payload.username;
gmLevel = payload.gmLevel;
// Optionally verify user still exists and has access
try {
auto accountInfo = Database::Get()->GetAccountInfo(username);
if (!accountInfo || !HasDashboardAccess(static_cast<uint8_t>(accountInfo->maxGmLevel))) {
LOG_DEBUG("Token verification failed: user no longer has access");
return false;
}
} catch (const std::exception& ex) {
LOG_DEBUG("Error verifying user during token validation: %s", ex.what());
return false;
}
LOG_DEBUG("Token verified successfully for user: %s (GM Level: %d)", username.c_str(), gmLevel);
return true;
}
bool DashboardAuthService::HasDashboardAccess(uint8_t gmLevel) {
// Get minimum GM level from config (default 0 = any user)
uint8_t minGmLevel = 0;
if (Game::config) {
const std::string& minGmLevelStr = Game::config->GetValue("min_dashboard_gm_level");
if (!minGmLevelStr.empty()) {
const auto parsed = GeneralUtils::TryParse<uint8_t>(minGmLevelStr);
if (parsed) {
minGmLevel = parsed.value();
}
}
}
return gmLevel >= minGmLevel;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <string>
#include <cstdint>
/**
* Dashboard authentication service
* Handles user login, password verification, and account lockout
*/
class DashboardAuthService {
public:
/**
* Login result structure
*/
struct LoginResult {
bool success{false};
std::string message{};
std::string token{}; // JWT token if successful
uint8_t gmLevel{0}; // GM level if successful
bool accountLocked{false}; // Account is locked out
};
/**
* Attempt to log in with username and password
* @param username The username
* @param password The plaintext password (max 40 characters)
* @param rememberMe If true, extends token expiration to 30 days
* @return LoginResult with success status and JWT token if successful
*/
static LoginResult Login(const std::string& username, const std::string& password, bool rememberMe = false);
/**
* Verify that a token is valid and get the username
* @param token The JWT token
* @param username Output parameter for the username
* @param gmLevel Output parameter for the GM level
* @return true if token is valid
*/
static bool VerifyToken(const std::string& token, std::string& username, uint8_t& gmLevel);
/**
* Check if user has required GM level for dashboard access
* @param gmLevel The user's GM level
* @return true if user can access dashboard (GM level > 0)
*/
static bool HasDashboardAccess(uint8_t gmLevel);
};

View File

@@ -0,0 +1,186 @@
#include "JWTUtils.h"
#include "GeneralUtils.h"
#include "Logger.h"
#include "json.hpp"
#include <ctime>
#include <cstring>
#include <openssl/hmac.h>
#include <openssl/sha.h>
namespace {
std::string g_Secret = "default-secret-change-me";
// Simple base64 encoding
std::string Base64Encode(const std::string& input) {
static const char* base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string ret;
int i = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];
for (size_t n = 0; n < input.length(); n++) {
char_array_3[i++] = input[n];
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; i < 4; i++) ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
for (int j = i; j < 3; j++) char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
for (int j = 0; j <= i; j++) ret += base64_chars[char_array_4[j]];
while (i++ < 3) ret += '=';
}
return ret;
}
// Simple base64 decoding
std::string Base64Decode(const std::string& encoded_string) {
static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int in_len = encoded_string.size();
int i = 0, j = 0, in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::string ret;
while (in_len-- && (encoded_string[in_] != '=') &&
(isalnum(encoded_string[in_]) || encoded_string[in_] == '+' || encoded_string[in_] == '/')) {
char_array_4[i++] = encoded_string[in_]; in_++;
if (i == 4) {
for (i = 0; i < 4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; i < 3; i++) ret += char_array_3[i];
i = 0;
}
}
if (i) {
for (j = i; j < 4; j++) char_array_4[j] = 0;
for (j = 0; j < 4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
for (j = 0; j < i - 1; j++) ret += char_array_3[j];
}
return ret;
}
// HMAC-SHA256
std::string HmacSha256(const std::string& key, const std::string& message) {
unsigned char* digest = HMAC(EVP_sha256(),
reinterpret_cast<const unsigned char*>(key.c_str()), key.length(),
reinterpret_cast<const unsigned char*>(message.c_str()), message.length(),
nullptr, nullptr);
std::string result(reinterpret_cast<char*>(digest), SHA256_DIGEST_LENGTH);
return result;
}
// Create signature for JWT
std::string CreateSignature(const std::string& header, const std::string& payload, const std::string& secret) {
std::string message = header + "." + payload;
std::string signature = HmacSha256(secret, message);
return Base64Encode(signature);
}
// Verify JWT signature
bool VerifySignature(const std::string& header, const std::string& payload, const std::string& signature, const std::string& secret) {
std::string expected = CreateSignature(header, payload, secret);
return signature == expected;
}
}
namespace JWTUtils {
void SetSecretKey(const std::string& secret) {
if (secret.empty()) {
LOG("Warning: JWT secret key is empty, using default");
return;
}
g_Secret = secret;
}
std::string GenerateToken(const std::string& username, uint8_t gmLevel, bool rememberMe) {
// Header
std::string header = R"({"alg":"HS256","typ":"JWT"})";
std::string encodedHeader = Base64Encode(header);
// Payload
int64_t now = std::time(nullptr);
int64_t expiresAt = now + (rememberMe ? 30 * 24 * 60 * 60 : 24 * 60 * 60); // 30 days or 24 hours
std::string payload = R"({"username":")" + username + R"(","gmLevel":)" + std::to_string(gmLevel) +
R"(,"rememberMe":)" + (rememberMe ? "true" : "false") +
R"(,"iat":)" + std::to_string(now) +
R"(,"exp":)" + std::to_string(expiresAt) + "}";
std::string encodedPayload = Base64Encode(payload);
// Signature
std::string signature = CreateSignature(encodedHeader, encodedPayload, g_Secret);
return encodedHeader + "." + encodedPayload + "." + signature;
}
bool ValidateToken(const std::string& token, JWTPayload& payload) {
// Split token into parts
size_t firstDot = token.find('.');
size_t secondDot = token.find('.', firstDot + 1);
if (firstDot == std::string::npos || secondDot == std::string::npos) {
LOG_DEBUG("Invalid JWT format");
return false;
}
std::string header = token.substr(0, firstDot);
std::string encodedPayload = token.substr(firstDot + 1, secondDot - firstDot - 1);
std::string signature = token.substr(secondDot + 1);
// Verify signature
if (!VerifySignature(header, encodedPayload, signature, g_Secret)) {
LOG_DEBUG("Invalid JWT signature");
return false;
}
// Decode and parse payload
std::string decodedPayload = Base64Decode(encodedPayload);
try {
auto json = nlohmann::json::parse(decodedPayload);
payload.username = json.value("username", "");
payload.gmLevel = json.value("gmLevel", 0);
payload.rememberMe = json.value("rememberMe", false);
payload.issuedAt = json.value("iat", 0);
payload.expiresAt = json.value("exp", 0);
if (payload.username.empty()) {
LOG_DEBUG("JWT missing username");
return false;
}
// Check expiration
if (IsTokenExpired(payload.expiresAt)) {
LOG_DEBUG("JWT token expired");
return false;
}
return true;
} catch (const std::exception& ex) {
LOG_DEBUG("Error parsing JWT payload: %s", ex.what());
return false;
}
}
bool IsTokenExpired(int64_t expiresAt) {
return std::time(nullptr) > expiresAt;
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <string>
#include <ctime>
#include "json_fwd.hpp"
/**
* JWT Token utilities for dashboard authentication
* Provides secure token generation, validation, and parsing
*/
namespace JWTUtils {
/**
* JWT payload structure
*/
struct JWTPayload {
std::string username{};
uint8_t gmLevel{0};
bool rememberMe{false};
int64_t issuedAt{0};
int64_t expiresAt{0};
};
/**
* Generate a new JWT token
* @param username The username to encode in the token
* @param gmLevel The GM level of the user
* @param rememberMe If true, extends token expiration to 30 days; otherwise 24 hours
* @return Signed JWT token string
*/
std::string GenerateToken(const std::string& username, uint8_t gmLevel, bool rememberMe = false);
/**
* Validate and decode a JWT token
* @param token The JWT token to validate
* @param payload Output parameter for the decoded payload
* @return true if token is valid and not expired, false otherwise
*/
bool ValidateToken(const std::string& token, JWTPayload& payload);
/**
* Check if a token is expired
* @param expiresAt Expiration timestamp
* @return true if token is expired
*/
bool IsTokenExpired(int64_t expiresAt);
/**
* Set the JWT secret key (must be called once at startup)
* @param secret The secret key for signing tokens
*/
void SetSecretKey(const std::string& secret);
}

View File

@@ -0,0 +1,35 @@
#include "RequireAuthMiddleware.h"
#include "HTTPContext.h"
#include "Web.h"
#include "Game.h"
#include "Logger.h"
RequireAuthMiddleware::RequireAuthMiddleware(uint8_t minGmLevel) : minGmLevel(minGmLevel) {}
bool RequireAuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
// Check if user is authenticated
if (!context.isAuthenticated) {
LOG_DEBUG("Unauthorized access attempt to %s from %s", context.path.c_str(), context.clientIP.c_str());
reply.status = eHTTPStatusCode::FOUND;
reply.message = "";
reply.location = "/login";
reply.contentType = eContentType::TEXT_HTML;
return false; // Stop middleware chain and send reply
}
// Check if user has required GM level
if (context.gmLevel < minGmLevel) {
LOG_DEBUG("Forbidden access attempt by user %s (GM level %d < %d required) to %s from %s",
context.authenticatedUser.c_str(), context.gmLevel, minGmLevel,
context.path.c_str(), context.clientIP.c_str());
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "{\"error\":\"Forbidden - Insufficient permissions\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return false; // Stop middleware chain and send reply
}
// Authentication passed
LOG_DEBUG("User %s authenticated with GM level %d accessing %s",
context.authenticatedUser.c_str(), context.gmLevel, context.path.c_str());
return true; // Continue to next middleware or route handler
}

View File

@@ -0,0 +1,30 @@
#ifndef __REQUIREAUTHMIDDLEWARE_H__
#define __REQUIREAUTHMIDDLEWARE_H__
#include <memory>
#include <cstdint>
#include "IHTTPMiddleware.h"
/**
* RequireAuthMiddleware: Enforces authentication on protected routes
*
* Returns 401 Unauthorized if user is not authenticated
* Returns 403 Forbidden if user's GM level is below minimum required
*/
class RequireAuthMiddleware final : public IHTTPMiddleware {
public:
/**
* @param minGmLevel Minimum GM level required to access this route
* 0 = any authenticated user, higher numbers = GM-only
*/
explicit RequireAuthMiddleware(uint8_t minGmLevel = 0);
~RequireAuthMiddleware() override = default;
bool Process(HTTPContext& context, HTTPReply& reply) override;
std::string GetName() const override { return "RequireAuthMiddleware"; }
private:
uint8_t minGmLevel;
};
#endif // !__REQUIREAUTHMIDDLEWARE_H__

View File

@@ -0,0 +1,443 @@
#include "APIRoutes.h"
#include "ServerState.h"
#include "Web.h"
#include "eHTTPMethod.h"
#include "json.hpp"
#include "Game.h"
#include "Database.h"
#include "Logger.h"
#include "HTTPContext.h"
#include "RequireAuthMiddleware.h"
#include <memory>
void RegisterAPIRoutes() {
// GET /api/status - Get overall server status
Game::web.RegisterHTTPRoute({
.path = "/api/status",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
nlohmann::json response = ServerState::GetServerStateJson();
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
}
});
// GET /api/players - Get list of online players
Game::web.RegisterHTTPRoute({
.path = "/api/players",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
nlohmann::json response = {
{"players", nlohmann::json::array()},
{"count", 0}
};
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
}
});
// GET /api/accounts/count - Get total account count
Game::web.RegisterHTTPRoute({
.path = "/api/accounts/count",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
const uint32_t count = Database::Get()->GetAccountCount();
nlohmann::json response = {{"count", count}};
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/accounts/count: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /api/characters/count - Get total character count
Game::web.RegisterHTTPRoute({
.path = "/api/characters/count",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
const uint32_t count = Database::Get()->GetCharacterCount();
nlohmann::json response = {{"count", count}};
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/characters/count: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// POST /api/tables/accounts - Get accounts table data (DataTables.js format)
Game::web.RegisterHTTPRoute({
.path = "/api/tables/accounts",
.method = eHTTPMethod::POST,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Only admins (GM > 0) can access table data
if (context.gmLevel == 0) {
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
nlohmann::json requestData = nlohmann::json::parse(context.body);
// Extract DataTables parameters
uint32_t draw = requestData.value("draw", 1);
uint32_t start = requestData.value("start", 0);
uint32_t length = requestData.value("length", 10);
// Extract search - it can be a string or an object with a "value" property
std::string search = "";
if (requestData.contains("search")) {
if (requestData["search"].is_string()) {
search = requestData["search"].get<std::string>();
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
search = requestData["search"]["value"].get<std::string>();
}
}
uint32_t orderColumn = 0;
bool orderAsc = true;
// Extract order parameters
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
orderColumn = requestData["order"][0].value("column", 0);
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
}
// Get the accounts table data
nlohmann::json response = Database::Get()->GetAccountsTable(start, length, search, orderColumn, orderAsc);
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const nlohmann::json::exception& jsonEx) {
LOG("JSON error in /api/tables/accounts: %s", jsonEx.what());
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/tables/accounts: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// POST /api/tables/characters - Get characters table data (DataTables.js format)
Game::web.RegisterHTTPRoute({
.path = "/api/tables/characters",
.method = eHTTPMethod::POST,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Only admins (GM > 0) can access table data
if (context.gmLevel == 0) {
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
nlohmann::json requestData = nlohmann::json::parse(context.body);
uint32_t draw = requestData.value("draw", 1);
uint32_t start = requestData.value("start", 0);
uint32_t length = requestData.value("length", 10);
std::string search = "";
if (requestData.contains("search")) {
if (requestData["search"].is_string()) {
search = requestData["search"].get<std::string>();
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
search = requestData["search"]["value"].get<std::string>();
}
}
uint32_t orderColumn = 0;
bool orderAsc = true;
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
orderColumn = requestData["order"][0].value("column", 0);
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
}
std::string tableData = Database::Get()->GetCharactersTable(start, length, search, orderColumn, orderAsc);
nlohmann::json response = nlohmann::json::parse(tableData);
response["draw"] = draw;
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const nlohmann::json::exception& jsonEx) {
LOG("JSON error in /api/tables/characters: %s", jsonEx.what());
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/tables/characters: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// POST /api/tables/play_keys - Get play keys table data (DataTables.js format)
Game::web.RegisterHTTPRoute({
.path = "/api/tables/play_keys",
.method = eHTTPMethod::POST,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try { // Only admins (GM > 0) can access table data
if (context.gmLevel == 0) {
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
nlohmann::json requestData = nlohmann::json::parse(context.body);
uint32_t draw = requestData.value("draw", 1);
uint32_t start = requestData.value("start", 0);
uint32_t length = requestData.value("length", 10);
std::string search = "";
if (requestData.contains("search")) {
if (requestData["search"].is_string()) {
search = requestData["search"].get<std::string>();
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
search = requestData["search"]["value"].get<std::string>();
}
}
uint32_t orderColumn = 0;
bool orderAsc = true;
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
orderColumn = requestData["order"][0].value("column", 0);
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
}
std::string tableData = Database::Get()->GetPlayKeysTable(start, length, search, orderColumn, orderAsc);
nlohmann::json response = nlohmann::json::parse(tableData);
response["draw"] = draw;
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const nlohmann::json::exception& jsonEx) {
LOG("JSON error in /api/tables/play_keys: %s", jsonEx.what());
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/tables/play_keys: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// POST /api/tables/properties - Get properties table data (DataTables.js format)
Game::web.RegisterHTTPRoute({
.path = "/api/tables/properties",
.method = eHTTPMethod::POST,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Only admins (GM > 0) can access table data
if (context.gmLevel == 0) {
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
nlohmann::json requestData = nlohmann::json::parse(context.body);
uint32_t draw = requestData.value("draw", 1);
uint32_t start = requestData.value("start", 0);
uint32_t length = requestData.value("length", 10);
std::string search = "";
if (requestData.contains("search")) {
if (requestData["search"].is_string()) {
search = requestData["search"].get<std::string>();
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
search = requestData["search"]["value"].get<std::string>();
}
}
uint32_t orderColumn = 0;
bool orderAsc = true;
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
orderColumn = requestData["order"][0].value("column", 0);
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
}
std::string tableData = Database::Get()->GetPropertiesTable(start, length, search, orderColumn, orderAsc);
nlohmann::json response = nlohmann::json::parse(tableData);
response["draw"] = draw;
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const nlohmann::json::exception& jsonEx) {
LOG("JSON error in /api/tables/properties: %s", jsonEx.what());
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/tables/properties: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// POST /api/tables/bug_reports - Get bug reports table data (DataTables.js format)
Game::web.RegisterHTTPRoute({
.path = "/api/tables/bug_reports",
.method = eHTTPMethod::POST,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try { // Only admins (GM > 0) can access table data
if (context.gmLevel == 0) {
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "{\"error\":\"Forbidden - Admin access required\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
nlohmann::json requestData = nlohmann::json::parse(context.body);
uint32_t draw = requestData.value("draw", 1);
uint32_t start = requestData.value("start", 0);
uint32_t length = requestData.value("length", 10);
std::string search = "";
if (requestData.contains("search")) {
if (requestData["search"].is_string()) {
search = requestData["search"].get<std::string>();
} else if (requestData["search"].is_object() && requestData["search"].contains("value")) {
search = requestData["search"]["value"].get<std::string>();
}
}
uint32_t orderColumn = 0;
bool orderAsc = true;
if (requestData.contains("order") && requestData["order"].is_array() && requestData["order"].size() > 0) {
orderColumn = requestData["order"][0].value("column", 0);
orderAsc = requestData["order"][0].value("dir", "asc") == "asc";
}
std::string tableData = Database::Get()->GetBugReportsTable(start, length, search, orderColumn, orderAsc);
nlohmann::json response = nlohmann::json::parse(tableData);
response["draw"] = draw;
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const nlohmann::json::exception& jsonEx) {
LOG("JSON error in /api/tables/bug_reports: %s", jsonEx.what());
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/tables/bug_reports: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /api/accounts/:id - Get single account by ID
Game::web.RegisterHTTPRoute({
.path = "/api/accounts/:id",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Extract account ID from URL path
const std::string path = context.path;
size_t lastSlash = path.rfind('/');
if (lastSlash == std::string::npos) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid account ID\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
std::string idStr = path.substr(lastSlash + 1);
uint32_t accountId = 0;
try {
accountId = std::stoul(idStr);
} catch (...) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid account ID\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
// Permission check: GM 0 can only view own account, GM > 0 can view any account
if (context.gmLevel == 0) {
// Regular user - get their own account ID
auto currentUserInfo = Database::Get()->GetAccountInfo(context.authenticatedUser);
if (!currentUserInfo.has_value() || currentUserInfo->id != accountId) {
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "{\"error\":\"Forbidden - You do not have permission to view this account\"}";
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
}
// Get account data
nlohmann::json response = Database::Get()->GetAccountById(accountId);
reply.status = eHTTPStatusCode::OK;
reply.message = response.dump();
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const nlohmann::json::exception& jsonEx) {
LOG("JSON error in /api/accounts/:id: %s", jsonEx.what());
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = eContentType::APPLICATION_JSON;
} catch (std::exception& ex) {
LOG("Error in /api/accounts/:id: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Database error\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
}

View File

@@ -0,0 +1,3 @@
#pragma once
void RegisterAPIRoutes();

View File

@@ -0,0 +1,102 @@
#include "AuthRoutes.h"
#include "DashboardAuthService.h"
#include "json.hpp"
#include "Logger.h"
#include "GeneralUtils.h"
#include "Web.h"
#include "eHTTPMethod.h"
#include "HTTPContext.h"
void RegisterAuthRoutes() {
// POST /api/auth/login
// Request body: { "username": "string", "password": "string", "rememberMe": boolean }
// Response: { "success": boolean, "message": "string", "token": "string", "gmLevel": number }
Game::web.RegisterHTTPRoute({
.path = "/api/auth/login",
.method = eHTTPMethod::POST,
.middleware = {},
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
auto json = nlohmann::json::parse(context.body);
std::string username = json.value("username", "");
std::string password = json.value("password", "");
bool rememberMe = json.value("rememberMe", false);
// Validate input
if (username.empty() || password.empty()) {
reply.message = R"({"success":false,"message":"Username and password are required"})";
reply.status = eHTTPStatusCode::BAD_REQUEST;
return;
}
if (password.length() > 40) {
reply.message = R"({"success":false,"message":"Password exceeds maximum length"})";
reply.status = eHTTPStatusCode::BAD_REQUEST;
return;
}
// Attempt login
auto result = DashboardAuthService::Login(username, password, rememberMe);
nlohmann::json response;
response["success"] = result.success;
response["message"] = result.message;
if (result.success) {
response["token"] = result.token;
response["gmLevel"] = result.gmLevel;
}
reply.message = response.dump();
reply.status = result.success ? eHTTPStatusCode::OK : eHTTPStatusCode::UNAUTHORIZED;
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const std::exception& ex) {
LOG("Error processing login request: %s", ex.what());
reply.message = R"({"success":false,"message":"Internal server error"})";
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// POST /api/auth/verify
// Request body: { "token": "string" }
// Response: { "valid": boolean, "username": "string", "gmLevel": number }
Game::web.RegisterHTTPRoute({
.path = "/api/auth/verify",
.method = eHTTPMethod::POST,
.middleware = {},
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
auto json = nlohmann::json::parse(context.body);
std::string token = json.value("token", "");
if (token.empty()) {
reply.message = R"({"valid":false})";
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.contentType = eContentType::APPLICATION_JSON;
return;
}
std::string username;
uint8_t gmLevel{};
bool valid = DashboardAuthService::VerifyToken(token, username, gmLevel);
nlohmann::json response;
response["valid"] = valid;
if (valid) {
response["username"] = username;
response["gmLevel"] = gmLevel;
}
reply.message = response.dump();
reply.status = eHTTPStatusCode::OK;
reply.contentType = eContentType::APPLICATION_JSON;
} catch (const std::exception& ex) {
LOG("Error processing verify request: %s", ex.what());
reply.message = R"({"valid":false})";
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Web.h"
/**
* Register authentication routes
* /api/auth/login - POST login endpoint
* /api/auth/verify - POST verify token endpoint
*/
void RegisterAuthRoutes();

View File

@@ -0,0 +1,30 @@
set(DASHBOARDROUTES_SOURCES
"APIRoutes.cpp"
"StaticRoutes.cpp"
"DashboardRoutes.cpp"
"WSRoutes.cpp"
"AuthRoutes.cpp"
)
add_library(DashboardRoutes STATIC ${DASHBOARDROUTES_SOURCES})
target_include_directories(DashboardRoutes PRIVATE
"${PROJECT_SOURCE_DIR}/dCommon"
"${PROJECT_SOURCE_DIR}/dCommon/dClient"
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
"${PROJECT_SOURCE_DIR}/dDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/CDClientDatabase/CDClientTables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/ITables"
"${PROJECT_SOURCE_DIR}/dDatabase/GameDatabase/MySQL"
"${PROJECT_SOURCE_DIR}/dNet"
"${PROJECT_SOURCE_DIR}/dWeb"
"${PROJECT_SOURCE_DIR}/dServer"
"${PROJECT_SOURCE_DIR}/thirdparty"
"${PROJECT_SOURCE_DIR}/thirdparty/nlohmann"
"${PROJECT_SOURCE_DIR}/dDashboardServer/auth"
"${PROJECT_SOURCE_DIR}/dDashboardServer/routes"
)
target_link_libraries(DashboardRoutes PRIVATE ${COMMON_LIBRARIES} dWeb dServer)

View File

@@ -0,0 +1,291 @@
#include "DashboardRoutes.h"
#include "ServerState.h"
#include "Web.h"
#include "HTTPContext.h"
#include "eHTTPMethod.h"
#include "json.hpp"
#include "Game.h"
#include "Database.h"
#include "Logger.h"
#include "inja.hpp"
#include "AuthMiddleware.h"
#include "RequireAuthMiddleware.h"
void RegisterDashboardRoutes() {
// GET / - Main dashboard page (requires authentication)
Game::web.RegisterHTTPRoute({
.path = "/",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Initialize inja environment
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
// Prepare data for template
nlohmann::json data = context.GetUserDataJson();
// Server status - merge with server state
nlohmann::json serverState = ServerState::GetServerStateJson();
data.merge_patch(serverState);
// Statistics
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount();
data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount();
// Render template
const std::string html = env.render_file("index.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Failed to render template\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /login - Login page (no authentication required)
Game::web.RegisterHTTPRoute({
.path = "/login",
.method = eHTTPMethod::GET,
.middleware = {},
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Initialize inja environment
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
// Render template with empty user data (not authenticated)
nlohmann::json data = context.GetUserDataJson();
const std::string html = env.render_file("login.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering login template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Failed to render login page\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /accounts/:id - View single account
Game::web.RegisterHTTPRoute({
.path = "/accounts/:id",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(0) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Extract account ID from URL path
const std::string path = context.path;
size_t lastSlash = path.rfind('/');
if (lastSlash == std::string::npos) {
reply.status = eHTTPStatusCode::NOT_FOUND;
reply.message = "<h1>404 - Account not found</h1>";
reply.contentType = eContentType::TEXT_HTML;
return;
}
std::string idStr = path.substr(lastSlash + 1);
uint32_t accountId = 0;
try {
accountId = std::stoul(idStr);
} catch (...) {
reply.status = eHTTPStatusCode::NOT_FOUND;
reply.message = "<h1>404 - Invalid account ID</h1>";
reply.contentType = eContentType::TEXT_HTML;
return;
}
// Permission check: GM 0 can only view own account, GM > 0 can view any account
if (context.gmLevel == 0) {
LOG("Regular user '%s' (GM level 0) is trying to access account ID %u", context.authenticatedUser.c_str(), accountId);
// Regular user - get their own account ID
auto currentUserInfo = Database::Get()->GetAccountInfo(context.authenticatedUser);
if (!currentUserInfo.has_value() || currentUserInfo->id != accountId) {
LOG("Permission denied: user '%s' cannot access account ID %u", context.authenticatedUser.c_str(), accountId);
reply.status = eHTTPStatusCode::FORBIDDEN;
reply.message = "<h1>403 - Forbidden</h1><p>You do not have permission to view this account.</p>";
reply.contentType = eContentType::TEXT_HTML;
return;
}
}
// Get account data from API
nlohmann::json account = Database::Get()->GetAccountById(accountId);
// Check if account was found
if (account.contains("error")) {
reply.status = eHTTPStatusCode::NOT_FOUND;
reply.message = "<h1>404 - Account not found</h1>";
reply.contentType = eContentType::TEXT_HTML;
return;
}
// Initialize inja environment
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
// Prepare data for template
nlohmann::json data = context.GetUserDataJson();
data["account"] = account;
// Render template
const std::string html = env.render_file("account-view.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering account view template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "<h1>500 - Server Error</h1>";
reply.contentType = eContentType::TEXT_HTML;
}
}
});
// GET /accounts - Accounts management page
Game::web.RegisterHTTPRoute({
.path = "/accounts",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
// Initialize inja environment
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
// Prepare data for template
nlohmann::json data = context.GetUserDataJson();
// Render template
const std::string html = env.render_file("accounts.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering accounts template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Failed to render accounts page\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /characters - Characters management page
Game::web.RegisterHTTPRoute({
.path = "/characters",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
nlohmann::json data = context.GetUserDataJson();
const std::string html = env.render_file("characters.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering characters template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Failed to render characters page\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /play_keys - Play keys management page
Game::web.RegisterHTTPRoute({
.path = "/play_keys",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
nlohmann::json data = context.GetUserDataJson();
const std::string html = env.render_file("play_keys.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering play_keys template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Failed to render play_keys page\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /properties - Properties management page
Game::web.RegisterHTTPRoute({
.path = "/properties",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
nlohmann::json data = context.GetUserDataJson();
const std::string html = env.render_file("properties.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering properties template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Failed to render properties page\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
// GET /bug_reports - Bug reports management page
Game::web.RegisterHTTPRoute({
.path = "/bug_reports",
.method = eHTTPMethod::GET,
.middleware = { std::make_shared<RequireAuthMiddleware>(1) },
.handle = [](HTTPReply& reply, const HTTPContext& context) {
try {
inja::Environment env{"dDashboardServer/templates/"};
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
nlohmann::json data = context.GetUserDataJson();
const std::string html = env.render_file("bug_reports.jinja2", data);
reply.status = eHTTPStatusCode::OK;
reply.message = html;
reply.contentType = eContentType::TEXT_HTML;
} catch (const std::exception& ex) {
LOG("Error rendering bug_reports template: %s", ex.what());
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "{\"error\":\"Failed to render bug_reports page\"}";
reply.contentType = eContentType::APPLICATION_JSON;
}
}
});
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include "json.hpp"
class HTTPContext;
void RegisterDashboardRoutes();

View File

@@ -0,0 +1,52 @@
#pragma once
#include <chrono>
#include <vector>
#include <string>
#include <cstdint>
#include "json.hpp"
struct ServerStatus {
bool online{false};
uint32_t players{0};
std::string version{};
std::chrono::steady_clock::time_point lastSeen{};
};
struct WorldInstanceInfo {
uint32_t mapID{0};
uint32_t instanceID{0};
uint32_t cloneID{0};
uint32_t players{0};
std::string ip{};
uint32_t port{0};
bool isPrivate{false};
};
namespace ServerState {
extern ServerStatus g_AuthStatus;
extern ServerStatus g_ChatStatus;
extern std::vector<WorldInstanceInfo> g_WorldInstances;
// Helper function to get all server state as JSON
inline nlohmann::json GetServerStateJson() {
nlohmann::json data;
data["auth"]["online"] = g_AuthStatus.online;
data["auth"]["players"] = g_AuthStatus.players;
data["chat"]["online"] = g_ChatStatus.online;
data["chat"]["players"] = g_ChatStatus.players;
data["worlds"] = nlohmann::json::array();
for (const auto& world : g_WorldInstances) {
data["worlds"].push_back({
{"mapID", world.mapID},
{"instanceID", world.instanceID},
{"cloneID", world.cloneID},
{"players", world.players},
{"isPrivate", world.isPrivate}
});
}
return data;
}
}

View File

@@ -0,0 +1,68 @@
#include "StaticRoutes.h"
#include "Web.h"
#include "HTTPContext.h"
#include "eHTTPMethod.h"
#include "Game.h"
#include "Logger.h"
#include <fstream>
#include <sstream>
namespace {
std::string ReadFileToString(const std::string& filePath) {
std::ifstream file(filePath);
if (!file.is_open()) {
LOG("Failed to open file: %s", filePath.c_str());
return "";
}
std::stringstream buffer{};
buffer << file.rdbuf();
return buffer.str();
}
eContentType GetContentType(const std::string& filePath) {
if (filePath.ends_with(".css")) {
return eContentType::TEXT_CSS;
} else if (filePath.ends_with(".js")) {
return eContentType::TEXT_JAVASCRIPT;
} else if (filePath.ends_with(".html")) {
return eContentType::TEXT_HTML;
} else if (filePath.ends_with(".png")) {
return eContentType::IMAGE_PNG;
} else if (filePath.ends_with(".jpg") || filePath.ends_with(".jpeg")) {
return eContentType::IMAGE_JPEG;
} else if (filePath.ends_with(".json")) {
return eContentType::APPLICATION_JSON;
}
return eContentType::TEXT_PLAIN;
}
void ServeStaticFile(const std::string& urlPath, const std::string& filePath) {
Game::web.RegisterHTTPRoute({
.path = urlPath,
.method = eHTTPMethod::GET,
.middleware = {},
.handle = [filePath](HTTPReply& reply, const HTTPContext& context) {
const std::string content = ReadFileToString(filePath);
if (content.empty()) {
reply.status = eHTTPStatusCode::NOT_FOUND;
reply.message = "{\"error\":\"File not found\"}";
reply.contentType = eContentType::APPLICATION_JSON;
} else {
reply.status = eHTTPStatusCode::OK;
reply.message = content;
reply.contentType = GetContentType(filePath);
}
}
});
}
}
void RegisterStaticRoutes() {
// Serve CSS files
ServeStaticFile("/css/dashboard.css", "dDashboardServer/static/css/dashboard.css");
ServeStaticFile("/css/login.css", "dDashboardServer/static/css/login.css");
// Serve JavaScript files
ServeStaticFile("/js/dashboard.js", "dDashboardServer/static/js/dashboard.js");
ServeStaticFile("/js/login.js", "dDashboardServer/static/js/login.js");
}

View File

@@ -0,0 +1,3 @@
#pragma once
void RegisterStaticRoutes();

View File

@@ -0,0 +1,35 @@
#include "WSRoutes.h"
#include "ServerState.h"
#include "Web.h"
#include "json.hpp"
#include "Game.h"
#include "Database.h"
#include "Logger.h"
void RegisterWSRoutes() {
// Register WebSocket subscriptions for real-time updates
Game::web.RegisterWSSubscription("dashboard_update");
Game::web.RegisterWSSubscription("server_status");
Game::web.RegisterWSSubscription("player_joined");
Game::web.RegisterWSSubscription("player_left");
// dashboard_update: Broadcasts complete dashboard data every 2 seconds
// Other subscriptions can be triggered by events from the master server
}
void BroadcastDashboardUpdate() {
// Get server state data (auth, chat, worlds) - mutex is acquired internally
nlohmann::json data = ServerState::GetServerStateJson();
// Add statistics
try {
data["stats"]["onlinePlayers"] = 0; // TODO: Get from server communication
data["stats"]["totalAccounts"] = Database::Get()->GetAccountCount();
data["stats"]["totalCharacters"] = Database::Get()->GetCharacterCount();
} catch (const std::exception& ex) {
LOG_DEBUG("Error getting stats: %s", ex.what());
}
// Broadcast to all connected WebSocket clients subscribed to "dashboard_update"
Game::web.SendWSMessage("dashboard_update", data);
}

View File

@@ -0,0 +1,4 @@
#pragma once
void RegisterWSRoutes();
void BroadcastDashboardUpdate();

View File

@@ -0,0 +1,495 @@
/* Minimal custom styling - mostly Bootstrap5 utilities */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
margin: 0;
padding: 0;
}
body.d-flex.bg-dark.text-white {
background-color: #0d0d0d;
color: #fff;
}
/* Sidebar adjustments */
.navbar.flex-column {
box-shadow: 0.125rem 0 0.25rem rgba(0, 0, 0, 0.075);
}
.navbar.flex-column .navbar-nav {
width: 100%;
}
.navbar.flex-column .nav-link {
padding: 0.75rem 1.25rem;
border-left: 3px solid transparent;
transition: all 0.3s ease;
}
.navbar.flex-column .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
border-left-color: #667eea;
padding-left: 1.5rem;
}
.navbar.flex-column .nav-link.active {
background-color: rgba(255, 255, 255, 0.1);
border-left-color: #667eea;
}
main {
display: flex;
flex-direction: column;
padding: 0;
min-height: 100vh;
}
/* Responsive design */
@media (max-width: 991.98px) {
body {
display: block !important;
}
main {
margin-left: 0 !important;
}
.navbar.flex-column {
width: 100% !important;
height: auto !important;
position: relative !important;
top: auto !important;
start: auto !important;
}
}
.navbar {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.username {
font-weight: 600;
color: #667eea;
font-size: 1.1em;
}
.logout-btn {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: 600;
transition: opacity 0.3s;
}
.logout-btn:hover {
opacity: 0.9;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.stat:last-child {
border-bottom: none;
}
.stat-label {
color: #666;
font-weight: 500;
}
.stat-value {
color: #333;
font-weight: bold;
font-size: 1.2em;
}
.status {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9em;
font-weight: bold;
}
.status.online {
background: #4caf50;
color: white;
}
.status.offline {
background: #f44336;
color: white;
}
.world-list {
max-height: 300px;
overflow-y: auto;
}
.world-item {
padding: 15px;
background: #f5f5f5;
border-radius: 5px;
margin-bottom: 10px;
}
.world-item h3 {
color: #333;
margin-bottom: 8px;
}
.world-detail {
color: #666;
font-size: 0.9em;
margin: 3px 0;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
/* Dark theme for data tables and containers */
/* Container margin for sidebar layout */
.account-view-container,
.accounts-container,
.characters-container,
.play-keys-container,
.properties-container,
.bug-reports-container {
margin-left: 280px;
padding: 20px;
}
/* Table card styling */
.table-card {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.table-header {
background-color: #222;
padding: 1.5rem;
border-bottom: 1px solid #333;
border-radius: 0.5rem 0.5rem 0 0;
}
.table-header h2 {
margin: 0;
color: #fff;
}
.table-body {
padding: 1.5rem;
}
/* Bootstrap card overrides for dark theme */
.card {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
margin-bottom: 1.5rem;
}
.card-header {
background-color: #222;
padding: 1.5rem;
border-bottom: 1px solid #333;
border-radius: 0.5rem 0.5rem 0 0;
}
.card-header h2 {
margin: 0;
color: #fff;
}
.card-body {
padding: 1.5rem;
}
/* Table styling */
.table-dark {
color: #fff;
}
.table-dark thead {
background-color: #2a2a2a;
}
.table-dark thead th {
border-bottom: 2px solid #444;
color: #aaa;
font-weight: 600;
text-transform: uppercase;
font-size: 0.875rem;
padding: 1rem;
}
.table-dark tbody td {
padding: 0.875rem 1rem;
border-bottom: 1px solid #333;
vertical-align: middle;
}
.table-dark tbody tr:hover {
background-color: #252525;
}
/* DataTables customization */
.dataTables_wrapper {
color: #fff;
}
.dataTables_wrapper .dataTables_filter input {
background-color: #2a2a2a;
color: #fff;
border: 1px solid #444;
border-radius: 0.25rem;
padding: 0.4rem 0.6rem;
}
.dataTables_wrapper .dataTables_filter input::placeholder {
color: #888;
}
.dataTables_wrapper .dataTables_info {
color: #aaa;
padding: 1rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
background: #2a2a2a;
color: #fff;
border: 1px solid #444;
margin: 0 2px;
padding: 0.4rem 0.8rem;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background: #3a3a3a;
border: 1px solid #555;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: #0d6efd;
border: 1px solid #0d6efd;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.dataTables_wrapper .dataTables_length select {
background-color: #2a2a2a;
color: #fff;
border: 1px solid #444;
padding: 0.4rem 0.6rem;
border-radius: 0.25rem;
}
/* Detail grid layout */
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.detail-item {
background-color: #0a0a0a;
padding: 1rem;
border-radius: 0.25rem;
border-left: 3px solid #0d6efd;
}
.detail-label {
color: #999;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
}
.detail-value {
color: #fff;
font-size: 1rem;
font-weight: 500;
}
/* Badge styling */
.badge {
display: inline-block;
padding: 0.35rem 0.6rem;
font-size: 0.8rem;
font-weight: 500;
border-radius: 0.25rem;
}
.badge-active {
background-color: #28a745;
color: #fff;
}
.badge-inactive {
background-color: #6c757d;
color: #fff;
}
.badge-banned {
background-color: #dc3545;
color: #fff;
}
.badge-locked {
background-color: #ffc107;
color: #000;
}
.badge-gm {
background-color: #17a2b8;
color: #fff;
}
.badge-approved {
background-color: #28a745;
color: #fff;
}
.badge-pending {
background-color: #ffc107;
color: #000;
}
/* Button styling */
.button-group {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
border: none;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.btn-primary {
background-color: #0d6efd;
color: #fff;
}
.btn-primary:hover {
background-color: #0b5ed7;
}
.btn-secondary {
background-color: #6c757d;
color: #fff;
}
.btn-secondary:hover {
background-color: #5c636a;
}
.btn-danger {
background-color: #dc3545;
color: #fff;
}
.btn-danger:hover {
background-color: #c82333;
}
.btn-info {
background-color: #17a2b8;
color: #fff;
}
.btn-info:hover {
background-color: #138496;
}
.btn-warning {
background-color: #ffc107;
color: #000;
}
.btn-warning:hover {
background-color: #e0a800;
}
/* Action buttons */
.account-actions {
display: flex;
gap: 0.5rem;
}
/* Utility classes */
.text-muted {
color: #999 !important;
}
.back-link {
color: #0d6efd;
text-decoration: none;
margin-bottom: 1rem;
display: inline-block;
}
.back-link:hover {
text-decoration: underline;
}
.report-preview {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-section {
margin-bottom: 1.5rem;
}
.search-input {
background-color: #2a2a2a;
border: 1px solid #444;
color: #fff;
padding: 0.5rem;
border-radius: 0.25rem;
}
.search-input::placeholder {
color: #888;
}

View File

@@ -0,0 +1,30 @@
/* Custom styling for login page on top of Bootstrap5 */
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
.card {
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2) !important;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
.btn-primary:hover {
background: linear-gradient(135deg, #5568d3 0%, #6a3f93 100%);
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
h1 {
color: #333;
font-weight: 600;
}

View File

@@ -0,0 +1,240 @@
let ws = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectDelay = 3000;
// Helper function to get cookie value
function getCookie(name) {
const nameEQ = name + '=';
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length));
}
}
return null;
}
// Helper function to delete cookie
function deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict`;
}
// Check authentication on page load
function checkAuthentication() {
// Check localStorage first (most secure)
let token = localStorage.getItem('dashboardToken');
// Fallback to cookie if localStorage empty
if (!token) {
token = getCookie('dashboardToken');
}
if (!token) {
// Redirect to login if no token
window.location.href = '/login';
return false;
}
// Verify token is valid (asynchronous)
fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token })
})
.then(res => {
if (!res.ok) {
console.error('Verify endpoint returned:', res.status);
throw new Error(`HTTP ${res.status}`);
}
return res.json();
})
.then(data => {
console.log('Token verification response:', data);
if (!data.valid) {
// Token is invalid/expired, delete cookies and redirect to login
console.log('Token verification failed, redirecting to login');
deleteCookie('dashboardToken');
deleteCookie('gmLevel');
localStorage.removeItem('dashboardToken');
window.location.href = '/login';
} else {
// Update UI with username
console.log('Token verified, user:', data.username);
const usernameElement = document.querySelector('.username');
if (usernameElement) {
usernameElement.textContent = data.username || 'User';
} else {
console.warn('Username element not found in DOM');
}
// Now that verification is complete, connect to WebSocket
setTimeout(() => {
console.log('Starting WebSocket connection');
connectWebSocket();
}, 100);
}
})
.catch(err => {
console.error('Token verification error:', err);
// Network error - log but don't redirect immediately
// This prevents redirect loops on network issues
});
return true;
}
// Get token from localStorage or cookie
function getAuthToken() {
let token = localStorage.getItem('dashboardToken');
if (!token) {
token = getCookie('dashboardToken');
}
console.log('getAuthToken called, token available:', !!token);
return token;
}
// Logout function
function logout() {
deleteCookie('dashboardToken');
deleteCookie('gmLevel');
localStorage.removeItem('dashboardToken');
window.location.href = '/login';
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const token = getAuthToken();
if (!token) {
console.error('No token available for WebSocket connection');
window.location.href = '/login';
return;
}
console.log(`WebSocket connection attempt ${reconnectAttempts + 1}/${maxReconnectAttempts}`);
// Connect to WebSocket without token in URL (token is in cookies)
const wsUrl = `${protocol}//${window.location.host}/ws`;
console.log(`Connecting to WebSocket: ${wsUrl}`);
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
reconnectAttempts = 0;
// Subscribe to dashboard updates
ws.send(JSON.stringify({
event: 'subscribe',
subscription: 'dashboard_update'
}));
document.getElementById('connection-status')?.remove();
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Handle subscription confirmation
if (data.subscribed) {
console.log('Subscribed to:', data.subscribed);
return;
}
// Handle dashboard updates
if (data.event === 'dashboard_update') {
updateDashboard(data);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
ws = null;
// Show connection status
showConnectionStatus('Disconnected - Attempting to reconnect...');
// Attempt to reconnect with exponential backoff
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const backoffDelay = reconnectDelay * Math.pow(2, reconnectAttempts - 1);
console.log(`Reconnecting in ${backoffDelay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
setTimeout(connectWebSocket, backoffDelay);
} else {
console.error('Max reconnection attempts reached');
showConnectionStatus('Connection lost - Reload page to reconnect');
}
};
} catch (error) {
console.error('Failed to create WebSocket:', error);
showConnectionStatus('Failed to connect - Reload page to retry');
}
}
function showConnectionStatus(message) {
let statusEl = document.getElementById('connection-status');
if (!statusEl) {
statusEl = document.createElement('div');
statusEl.id = 'connection-status';
statusEl.style.cssText = 'position: fixed; top: 10px; right: 10px; background: #f44336; color: white; padding: 10px 20px; border-radius: 4px; z-index: 1000;';
document.body.appendChild(statusEl);
}
statusEl.textContent = message;
}
function updateDashboard(data) {
// Update server status
if (data.auth) {
document.getElementById('auth-status').textContent = data.auth.online ? 'Online' : 'Offline';
document.getElementById('auth-status').className = 'status ' + (data.auth.online ? 'online' : 'offline');
}
if (data.chat) {
document.getElementById('chat-status').textContent = data.chat.online ? 'Online' : 'Offline';
document.getElementById('chat-status').className = 'status ' + (data.chat.online ? 'online' : 'offline');
}
// Update world instances
if (data.worlds) {
document.getElementById('world-count').textContent = data.worlds.length;
const worldList = document.getElementById('world-list');
if (data.worlds.length === 0) {
worldList.innerHTML = '<div class="loading">No active world instances</div>';
} else {
worldList.innerHTML = data.worlds.map(world => `
<div class="world-item">
<h3>Zone ${world.mapID} - Instance ${world.instanceID}</h3>
<div class="world-detail">Clone ID: ${world.cloneID}</div>
<div class="world-detail">Players: ${world.players}</div>
<div class="world-detail">Type: ${world.isPrivate ? 'Private' : 'Public'}</div>
</div>
`).join('');
}
}
// Update statistics
if (data.stats) {
if (data.stats.onlinePlayers !== undefined) {
document.getElementById('online-players').textContent = data.stats.onlinePlayers;
}
if (data.stats.totalAccounts !== undefined) {
document.getElementById('total-accounts').textContent = data.stats.totalAccounts;
}
if (data.stats.totalCharacters !== undefined) {
document.getElementById('total-characters').textContent = data.stats.totalCharacters;
}
}
}
// Connect on page load
connectWebSocket();

View File

@@ -0,0 +1,99 @@
// Check if user is already logged in
function checkExistingToken() {
const token = localStorage.getItem('dashboardToken');
if (token) {
verifyTokenAndRedirect(token);
}
}
function verifyTokenAndRedirect(token) {
fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token })
})
.then(res => res.json())
.then(data => {
if (data.valid) {
window.location.href = '/';
}
})
.catch(err => console.error('Token verification failed:', err));
}
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.textContent = message;
alert.className = 'alert';
if (type === 'error') {
alert.classList.add('alert-danger');
} else if (type === 'success') {
alert.classList.add('alert-success');
}
alert.style.display = 'block';
}
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', () => {
const loginForm = document.getElementById('loginForm');
if (!loginForm) {
console.error('Login form not found');
return;
}
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const rememberMe = document.getElementById('rememberMe').checked;
// Validate input
if (!username || !password) {
showAlert('Username and password are required', 'error');
return;
}
if (password.length > 40) {
showAlert('Password exceeds maximum length (40 characters)', 'error');
return;
}
// Show loading state
document.getElementById('loading').style.display = 'inline-block';
document.getElementById('loginBtn').disabled = true;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, rememberMe })
});
const data = await response.json();
if (data.success) {
// Store token in localStorage (also set as cookie for API calls)
localStorage.setItem('dashboardToken', data.token);
document.cookie = `dashboardToken=${data.token}; path=/; SameSite=Strict`;
showAlert('Login successful! Redirecting...', 'success');
// Redirect after a short delay (no token in URL)
setTimeout(() => {
window.location.href = '/';
}, 1000);
} else {
showAlert(data.message || 'Login failed', 'error');
document.getElementById('loading').style.display = 'none';
document.getElementById('loginBtn').disabled = false;
}
} catch (error) {
showAlert('Network error: ' + error.message, 'error');
document.getElementById('loading').style.display = 'none';
document.getElementById('loginBtn').disabled = false;
}
});
// Check existing token on page load
checkExistingToken();
});

View File

@@ -0,0 +1,137 @@
{% extends "base.jinja2" %}
{% block title %}Account - DarkflameServer{% endblock %}
{% block css %}{% endblock %}
{% block content %}
<div class="account-view-container">
<div class="container-fluid">
<a href="/accounts" class="back-link">← Back to Accounts</a>
<div class="card">
<div class="card-header">
<h2>Account #{{ account.id }} - {{ account.name }}</h2>
<p class="text-muted">View account details and manage settings</p>
</div>
<div class="card-body">
<div class="detail-grid">
<div class="detail-item">
<div class="detail-label">Account ID</div>
<div class="detail-value">{{ account.id }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Username</div>
<div class="detail-value">{{ account.name }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Created</div>
<div class="detail-value">{{ account.created_at }}</div>
</div>
<div class="detail-item">
<div class="detail-label">GM Level</div>
<div class="detail-value">
{% if account.gm_level > 0 %}
<span class="badge badge-gm">GM {{ account.gm_level }}</span>
{% else %}
<span class="badge badge-inactive">User</span>
{% endif %}
</div>
</div>
<div class="detail-item">
<div class="detail-label">Ban Status</div>
<div class="detail-value">
{% if account.banned %}
<span class="badge badge-banned">BANNED</span>
{% else %}
<span class="badge badge-active">Active</span>
{% endif %}
</div>
</div>
<div class="detail-item">
<div class="detail-label">Lock Status</div>
<div class="detail-value">
{% if account.locked %}
<span class="badge badge-locked">LOCKED</span>
{% else %}
<span class="badge badge-active">Unlocked</span>
{% endif %}
</div>
</div>
<div class="detail-item">
<div class="detail-label">Mute Expires</div>
<div class="detail-value">
{% if account.mute_expire > 0 %}
<span>{{ account.mute_expire }}</span>
{% else %}
<span class="text-muted">Not muted</span>
{% endif %}
</div>
</div>
</div>
<div class="button-group">
<button class="btn btn-primary" onclick="EditAccount()">Edit Account</button>
{% if not account.banned %}
<button class="btn btn-danger" onclick="BanAccount()">Ban Account</button>
{% else %}
<button class="btn btn-secondary" onclick="UnbanAccount()">Unban Account</button>
{% endif %}
{% if not account.locked %}
<button class="btn btn-danger" onclick="LockAccount()">Lock Account</button>
{% else %}
<button class="btn btn-secondary" onclick="UnlockAccount()">Unlock Account</button>
{% endif %}
</div>
</div>
</div>
<!-- TODO: Add modals for edit, ban, lock operations -->
<!-- TODO: Add character list for this account -->
<!-- TODO: Add login history -->
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function EditAccount() {
alert("Edit functionality coming soon");
// TODO: Open edit modal
}
function BanAccount() {
if (confirm("Are you sure you want to ban this account?")) {
alert("Ban functionality coming soon");
// TODO: Call ban API endpoint
}
}
function UnbanAccount() {
if (confirm("Are you sure you want to unban this account?")) {
alert("Unban functionality coming soon");
// TODO: Call unban API endpoint
}
}
function LockAccount() {
if (confirm("Are you sure you want to lock this account?")) {
alert("Lock functionality coming soon");
// TODO: Call lock API endpoint
}
}
function UnlockAccount() {
if (confirm("Are you sure you want to unlock this account?")) {
alert("Unlock functionality coming soon");
// TODO: Call unlock API endpoint
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,133 @@
{% extends "base.jinja2" %}
{% block title %}Accounts - DarkflameServer{% endblock %}
{% block css %}{% endblock %}
{% block content %}
<div class="accounts-container">
<div class="container-fluid">
<div class="table-card">
<div class="table-header">
<h2 class="mb-0">Accounts</h2>
<p class="text-muted">View and manage user accounts</p>
</div>
<div class="table-body">
<div class="table-responsive">
<table id="accountsTable" class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Banned</th>
<th>Locked</th>
<th>GM Level</th>
<th>Mute Expires</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data populated by DataTables -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// Initialize DataTable with server-side processing
$('#accountsTable').DataTable({
processing: true,
serverSide: true,
pageLength: 25,
lengthMenu: [10, 25, 50, 100],
ajax: {
url: '/api/tables/accounts',
type: 'POST',
contentType: 'application/json',
data: function(d) {
return JSON.stringify(d);
},
error: function(xhr, error, thrown) {
console.error('Error loading accounts:', error);
if (xhr.status === 401) {
window.location.href = '/login';
}
}
},
columns: [
{ data: 'id' },
{ data: 'name' },
{
data: 'banned',
render: function(data) {
return data ? '<span class="badge badge-banned">Banned</span>' : '<span class="badge bg-success">Active</span>';
}
},
{
data: 'locked',
render: function(data) {
return data ? '<span class="badge badge-locked">Locked</span>' : '<span class="badge bg-secondary">Unlocked</span>';
}
},
{
data: 'gm_level',
render: function(data) {
if (data === 0) return '-';
return '<span class="badge badge-gm">Level ' + data + '</span>';
}
},
{
data: 'mute_expire',
render: function(data) {
if (data === 0) return 'Not Muted';
const now = Math.floor(Date.now() / 1000);
const isMuted = data > now;
const date = new Date(data * 1000);
const dateStr = date.toLocaleString();
if (isMuted) {
return '<span class="badge bg-danger">Muted until ' + dateStr + '</span>';
} else {
return '<span class="badge bg-success">Expired ' + dateStr + '</span>';
}
}
},
{
data: 'created_at',
render: function(data) {
return data ? new Date(data).toLocaleString() : '-';
}
},
{
data: 'id',
render: function(data) {
return '<div class="account-actions">' +
'<button class="btn btn-sm btn-info" onclick="viewAccount(' + data + ')" title="View">👁️</button>' +
'<button class="btn btn-sm btn-warning" onclick="editAccount(' + data + ')" title="Edit">✏️</button>' +
'</div>';
},
orderable: false,
searchable: false
}
],
order: [[0, 'asc']],
stateSave: false
});
});
function viewAccount(id) {
window.location.href = '/accounts/' + id;
}
function editAccount(id) {
alert('Edit account: ' + id);
// TODO: Implement account edit modal
}
</script>
{% endblock %}

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}DarkflameServer{% endblock %}</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link href="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.3.7/b-3.2.6/fh-4.0.6/sc-2.4.3/datatables.min.css" rel="stylesheet" integrity="sha384-XMNDGLb5fN9IqhIrVXOAtGKcz4KCr+JSHXGZ1TDXQPDukbEAfmLPjHdCXhgK93fv" crossorigin="anonymous">
<link rel="stylesheet" href="/css/dashboard.css">
{% block css %}{% endblock %}
</head>
<body class="d-flex bg-dark text-white">
{% if username and username != "" %}
{% include "header.jinja2" %}
{% endif %}
<div class="container-fluid py-3">
{% block content_before %}{% endblock %}
{% block content %}{% endblock %}
{% block content_after %}{% endblock %}
</div>
<footer class="mt-5 pt-5 border-top border-secondary text-center pb-3">
{% block footer %}
<p class="text-muted small">DarkflameServer Dashboard &copy; 2024</p>
{% endblock %}
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.3.7/b-3.2.6/fh-4.0.6/sc-2.4.3/datatables.min.js" integrity="sha384-BPUXtS4tH3onFfu5m+dPbFfpLOXQwSWGwrsNWxOAAwqqJx6tJHhFkGF6uitrmEui" crossorigin="anonymous"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,97 @@
{% extends "base.jinja2" %}
{% block title %}Bug Reports - DarkflameServer{% endblock %}
{% block css %}{% endblock %}
{% block content %}
<div class="bug-reports-container">
<div class="table-card">
<div class="table-header">
<h2 class="mb-0">Bug Reports</h2>
<p class="text-muted">View and manage bug reports from players</p>
</div>
<div class="table-body">
<table id="bugReportsTable" class="table table-dark table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Player</th>
<th>Client Version</th>
<th>Submitted</th>
<th>Report Preview</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data populated by DataTables -->
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// Initialize DataTable with server-side processing
$('#bugReportsTable').DataTable({
processing: true,
serverSide: true,
pageLength: 25,
lengthMenu: [10, 25, 50, 100],
ajax: {
url: '/api/tables/bug_reports',
type: 'POST',
contentType: 'application/json',
data: function(d) {
return JSON.stringify(d);
}
},
columns: [
{ data: 'id' },
{ data: 'other_player_id' },
{ data: 'client_version' },
{
data: 'submitted',
render: function(data) {
return data ? new Date(data).toLocaleString() : '-';
}
},
{
data: 'body',
render: function(data) {
return '<span class="report-preview" title="' + (data || '') + '">' + (data || '-') + '</span>';
}
},
{
data: 'id',
render: function(data) {
return '<div class="account-actions">' +
'<button class="btn btn-sm btn-info" onclick="viewReport(' + data + ')" title="View">👁️</button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteReport(' + data + ')" title="Delete">🗑️</button>' +
'</div>';
},
orderable: false,
searchable: false
}
],
order: [[0, 'desc']],
stateSave: false
});
});
function viewReport(id) {
alert('View report: ' + id);
// TODO: Implement report view modal
}
function deleteReport(id) {
if (confirm('Are you sure you want to delete this report?')) {
alert('Delete report: ' + id);
// TODO: Implement report deletion
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,90 @@
{% extends "base.jinja2" %}
{% block title %}Characters - DarkflameServer{% endblock %}
{% block css %}{% endblock %}
{% block content %}
<div class="characters-container">
<div class="table-card">
<div class="table-header">
<h2 class="mb-0">Characters</h2>
<p class="text-muted">View and manage player characters</p>
</div>
<div class="table-body">
<table id="charactersTable" class="table table-dark table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Account</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data populated by DataTables -->
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// Initialize DataTable with server-side processing
$('#charactersTable').DataTable({
processing: true,
serverSide: true,
pageLength: 25,
lengthMenu: [10, 25, 50, 100],
ajax: {
url: '/api/tables/characters',
type: 'POST',
contentType: 'application/json',
data: function(d) {
return JSON.stringify(d);
}
},
columns: [
{ data: 'id' },
{ data: 'name' },
{ data: 'account_name' },
{
data: 'last_login',
render: function(data) {
if (data === 0) return 'Never';
const date = new Date(data * 1000);
return date.toLocaleString();
}
},
{
data: 'id',
render: function(data) {
return '<div class="account-actions">' +
'<button class="btn btn-sm btn-info" onclick="viewCharacter(' + data + ')" title="View">👁️</button>' +
'<button class="btn btn-sm btn-warning" onclick="editCharacter(' + data + ')" title="Edit">✏️</button>' +
'</div>';
},
orderable: false,
searchable: false
}
],
order: [[0, 'asc']],
stateSave: false
});
});
function viewCharacter(id) {
alert('View character: ' + id);
// TODO: Implement character view modal
}
function editCharacter(id) {
alert('Edit character: ' + id);
// TODO: Implement character edit modal
}
</script>
{% endblock %}

View File

@@ -0,0 +1,30 @@
{# Navigation #}
<nav class="navbar navbar-dark bg-dark flex-column" style="width: 280px; height: 100vh; position: fixed; left: 0; top: 0; overflow-y: auto;">
<div class="p-3">
<a class="navbar-brand fw-bold" href="/">🎮 DarkflameServer</a>
</div>
<ul class="navbar-nav flex-column w-100 flex-grow-1 p-3">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/accounts">Accounts</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/characters">Characters</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/play_keys">Play Keys</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/properties">Properties</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/bug_reports">Bug Reports</a>
</li>
<li class="nav-item mt-auto">
<a class="nav-link" href="#" id="logoutBtn">Logout</a>
</li>
</ul>
</nav>

View File

@@ -0,0 +1,35 @@
{% extends "base.jinja2" %}
{% block title %}Dashboard - DarkflameServer{% endblock %}
{% block content %}
<!-- Main Content -->
<main style="margin-left: 280px;">
<div class="container-fluid p-3 p-md-4">
<div class="row g-3">
{% include "server_status.jinja2" %}
{% include "statistics.jinja2" %}
</div>
{% include "world_instances.jinja2" %}
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="/js/dashboard.js"></script>
<script>
// Check authentication and initialize dashboard
document.addEventListener('DOMContentLoaded', () => {
// checkAuthentication is now async and calls connectWebSocket when ready
checkAuthentication();
// Setup logout button
document.getElementById('logoutBtn').addEventListener('click', (e) => {
e.preventDefault();
logout();
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends "base.jinja2" %}
{% block title %}Dashboard Login - DarkflameServer{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="/css/login.css">
{% endblock %}
{% block content %}
<div class="min-vh-100 d-flex align-items-center justify-content-center">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-lg border-0">
<div class="card-body p-5">
<h1 class="text-center mb-4">🎮 DarkflameServer</h1>
<div id="alert" class="alert" role="alert" style="display: none;"></div>
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required maxlength="40">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="rememberMe" name="rememberMe">
<label class="form-check-label" for="rememberMe">
Remember me for 30 days
</label>
</div>
<button type="submit" class="btn btn-primary w-100" id="loginBtn">
<span id="loading" class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true" style="display: none;"></span>
<span>Login</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/js/login.js"></script>
{% endblock %}

View File

@@ -0,0 +1,95 @@
{% extends "base.jinja2" %}
{% block title %}Play Keys - DarkflameServer{% endblock %}
{% block css %}{% endblock %}
{% block content %}
<div class="play-keys-container">
<div class="table-card">
<div class="table-header">
<h2 class="mb-0">Play Keys</h2>
<p class="text-muted">View and manage play keys</p>
</div>
<div class="table-body">
<table id="playKeysTable" class="table table-dark table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Key String</th>
<th>Uses Remaining</th>
<th>Created</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data populated by DataTables -->
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// Initialize DataTable with server-side processing
$('#playKeysTable').DataTable({
processing: true,
serverSide: true,
pageLength: 25,
lengthMenu: [10, 25, 50, 100],
ajax: {
url: '/api/tables/play_keys',
type: 'POST',
contentType: 'application/json',
data: function(d) {
return JSON.stringify(d);
}
},
columns: [
{ data: 'id' },
{ data: 'key_string' },
{ data: 'key_uses' },
{
data: 'created_at',
render: function(data) {
return data ? new Date(data).toLocaleString() : '-';
}
},
{
data: 'active',
render: function(data) {
return data ? '<span class="badge badge-active">Active</span>' : '<span class="badge badge-inactive">Inactive</span>';
}
},
{
data: 'id',
render: function(data) {
return '<div class="account-actions">' +
'<button class="btn btn-sm btn-info" onclick="viewKey(' + data + ')" title="View">👁️</button>' +
'<button class="btn btn-sm btn-warning" onclick="editKey(' + data + ')" title="Edit">✏️</button>' +
'</div>';
},
orderable: false,
searchable: false
}
],
order: [[0, 'asc']],
stateSave: false
});
});
function viewKey(id) {
alert('View key: ' + id);
// TODO: Implement key view modal
}
function editKey(id) {
alert('Edit key: ' + id);
// TODO: Implement key edit modal
}
</script>
{% endblock %}

View File

@@ -0,0 +1,92 @@
{% extends "base.jinja2" %}
{% block title %}Properties - DarkflameServer{% endblock %}
{% block css %}{% endblock %}
{% block content %}
<div class="properties-container">
<div class="table-card">
<div class="table-header">
<h2 class="mb-0">Properties</h2>
<p class="text-muted">View and manage player properties</p>
</div>
<div class="table-body">
<table id="propertiesTable" class="table table-dark table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Owner ID</th>
<th>Moderation Status</th>
<th>Reputation</th>
<th>Zone</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Data populated by DataTables -->
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
// Initialize DataTable with server-side processing
$('#propertiesTable').DataTable({
processing: true,
serverSide: true,
pageLength: 25,
lengthMenu: [10, 25, 50, 100],
ajax: {
url: '/api/tables/properties',
type: 'POST',
contentType: 'application/json',
data: function(d) {
return JSON.stringify(d);
}
},
columns: [
{ data: 'id' },
{ data: 'name' },
{ data: 'owner_id' },
{
data: 'mod_approved',
render: function(data) {
return data ? '<span class="badge badge-approved">Approved</span>' : '<span class="badge badge-pending">Pending</span>';
}
},
{ data: 'reputation' },
{ data: 'zone_id' },
{
data: 'id',
render: function(data) {
return '<div class="account-actions">' +
'<button class="btn btn-sm btn-info" onclick="viewProperty(' + data + ')" title="View">👁️</button>' +
'<button class="btn btn-sm btn-warning" onclick="editProperty(' + data + ')" title="Edit">✏️</button>' +
'</div>';
},
orderable: false,
searchable: false
}
],
order: [[0, 'asc']],
stateSave: false
});
});
function viewProperty(id) {
alert('View property: ' + id);
// TODO: Implement property view modal
}
function editProperty(id) {
alert('Edit property: ' + id);
// TODO: Implement property edit modal
}
</script>
{% endblock %}

View File

@@ -0,0 +1,29 @@
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-light">
<h5 class="mb-0">Server Status</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<span>Auth Server</span>
{% if auth.online %}
<span class="badge bg-success" id="auth-status">Online</span>
{% else %}
<span class="badge bg-danger" id="auth-status">Offline</span>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<span>Chat Server</span>
{% if chat.online %}
<span class="badge bg-success" id="chat-status">Online</span>
{% else %}
<span class="badge bg-danger" id="chat-status">Offline</span>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Active Worlds</span>
<span class="badge bg-primary" id="world-count">{{ length(worlds) }}</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-light">
<h5 class="mb-0">Statistics</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<span>Online Players</span>
<span class="badge bg-info" id="online-players">{{ stats.onlinePlayers }}</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<span>Total Accounts</span>
<span class="badge bg-info" id="total-accounts">{{ stats.totalAccounts }}</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<span>Total Characters</span>
<span class="badge bg-info" id="total-characters">{{ stats.totalCharacters }}</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,37 @@
<div class="card border-0 shadow-sm mt-4">
<div class="card-header bg-light">
<h5 class="mb-0">Active World Instances</h5>
</div>
<div class="card-body">
<div id="world-list">
{% if length(worlds) == 0 %}
<p class="text-muted text-center mb-0">No active world instances</p>
{% else %}
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead class="table-light">
<tr>
<th>Zone</th>
<th>Instance</th>
<th>Clone</th>
<th>Players</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for world in worlds %}
<tr>
<td>{{ world.mapID }}</td>
<td>{{ world.instanceID }}</td>
<td>{{ world.cloneID }}</td>
<td><span class="badge bg-secondary">{{ world.players }}</span></td>
<td>{% if world.isPrivate %}<span class="badge bg-warning">Private</span>{% else %}<span class="badge bg-primary">Public</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -1,6 +1,5 @@
#include "CDActivitiesTable.h"
void CDActivitiesTable::LoadValuesFromDatabase() {
// First, get the size of the table
uint32_t size = 0;
@@ -56,3 +55,13 @@ std::vector<CDActivities> CDActivitiesTable::Query(std::function<bool(CDActiviti
return data;
}
std::optional<const CDActivities> CDActivitiesTable::GetActivity(const uint32_t activityID) {
auto& entries = GetEntries();
for (const auto& entry : entries) {
if (entry.ActivityID == activityID) {
return entry;
}
}
return std::nullopt;
}

View File

@@ -2,6 +2,7 @@
// Custom Classes
#include "CDTable.h"
#include <optional>
struct CDActivities {
uint32_t ActivityID;
@@ -31,4 +32,5 @@ public:
// Queries the table with a custom "where" clause
std::vector<CDActivities> Query(std::function<bool(CDActivities)> predicate);
std::optional<const CDActivities> GetActivity(const uint32_t activityID);
};

View File

@@ -15,7 +15,7 @@ target_include_directories(dDatabaseCDClient PUBLIC "."
"${PROJECT_SOURCE_DIR}/dCommon"
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
)
target_link_libraries(dDatabaseCDClient PRIVATE sqlite3)
target_link_libraries(dDatabaseCDClient PRIVATE sqlite3 glm::glm)
if (${CDCLIENT_CACHE_ALL})
add_compile_definitions(dDatabaseCDClient PRIVATE CDCLIENT_CACHE_ALL=${CDCLIENT_CACHE_ALL})

View File

@@ -10,4 +10,5 @@ add_dependencies(dDatabase conncpp_dylib)
target_include_directories(dDatabase PUBLIC ".")
target_link_libraries(dDatabase
PUBLIC dDatabaseCDClient dDatabaseGame)
PUBLIC dDatabaseCDClient dDatabaseGame
PRIVATE glm::glm)

View File

@@ -29,7 +29,7 @@ target_include_directories(dDatabaseGame PUBLIC "."
target_link_libraries(dDatabaseGame
INTERFACE dCommon
PRIVATE sqlite3 MariaDB::ConnCpp)
PRIVATE sqlite3 MariaDB::ConnCpp glm::glm)
# Glob together all headers that need to be precompiled
file(

View File

@@ -48,7 +48,7 @@ public:
virtual void Commit() = 0;
virtual bool GetAutoCommit() = 0;
virtual void SetAutoCommit(bool value) = 0;
virtual void DeleteCharacter(const uint32_t characterId) = 0;
virtual void DeleteCharacter(const LWOOBJID characterId) = 0;
};
#endif //!__GAMEDATABASE__H__

View File

@@ -5,6 +5,7 @@
#include <optional>
#include <string>
#include <string_view>
#include "json.hpp"
enum class eGameMasterLevel : uint8_t;
@@ -14,6 +15,7 @@ public:
std::string bcryptPassword;
uint32_t id{};
uint32_t playKeyId{};
uint64_t muteExpire{};
bool banned{};
bool locked{};
eGameMasterLevel maxGmLevel{};
@@ -38,6 +40,30 @@ public:
virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0;
virtual uint32_t GetAccountCount() = 0;
// Login attempt tracking methods
// Record a failed login attempt
virtual void RecordFailedAttempt(const uint32_t accountId) = 0;
// Clear failed login attempts and update last login time
virtual void ClearFailedAttempts(const uint32_t accountId) = 0;
// Set account lockout
virtual void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) = 0;
// Check if account is locked out
virtual bool IsLockedOut(const uint32_t accountId) = 0;
// Get failed attempt count
virtual uint8_t GetFailedAttempts(const uint32_t accountId) = 0;
// Get paginated list of accounts with optional search/filtering for DataTables
// Returns a JSON object with the account data and metadata
virtual nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
// Get a single account by ID
// Returns a JSON object with the account details
virtual nlohmann::json GetAccountById(uint32_t accountId) = 0;
};
#endif //!__IACCOUNTS__H__

View File

@@ -14,7 +14,7 @@ enum class eActivityType : uint32_t {
class IActivityLog {
public:
// Update the activity log for the given account.
virtual void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) = 0;
virtual void UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) = 0;
};
#endif //!__IACTIVITYLOG__H__

View File

@@ -9,7 +9,7 @@ class IBehaviors {
public:
struct Info {
LWOOBJID behaviorId{};
uint32_t characterId{};
LWOOBJID characterId{};
std::string behaviorInfo;
};

View File

@@ -2,6 +2,7 @@
#define __IBUGREPORTS__H__
#include <cstdint>
#include <string>
#include <string_view>
class IBugReports {
@@ -11,10 +12,14 @@ public:
std::string clientVersion;
std::string otherPlayer;
std::string selection;
uint32_t characterId{};
LWOOBJID characterId{};
};
// Add a new bug report to the database.
virtual void InsertNewBugReport(const Info& info) = 0;
// Get paginated list of bug reports with optional search/filtering for DataTables
// Returns a JSON-formatted string with the bug report data and metadata
virtual std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
};
#endif //!__IBUGREPORTS__H__

View File

@@ -14,7 +14,7 @@ public:
struct Info {
std::string name;
std::string pendingName;
uint32_t id{};
LWOOBJID id{};
uint32_t accountId{};
bool needsRename{};
LWOCLONEID cloneId{};
@@ -25,25 +25,28 @@ public:
virtual std::vector<std::string> GetApprovedCharacterNames() = 0;
// Get the character info for the given character id.
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const uint32_t charId) = 0;
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const LWOOBJID charId) = 0;
// Get the character info for the given character name.
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const std::string_view name) = 0;
// Get the character ids for the given account.
virtual std::vector<uint32_t> GetAccountCharacterIds(const uint32_t accountId) = 0;
virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
// Get the total number of characters in the database.
virtual uint32_t GetCharacterCount() = 0;
// Insert a new character into the database.
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;
// Set the name of the given character.
virtual void SetCharacterName(const uint32_t characterId, const std::string_view name) = 0;
virtual void SetCharacterName(const LWOOBJID characterId, const std::string_view name) = 0;
// Set the pending name of the given character.
virtual void SetPendingCharacterName(const uint32_t characterId, const std::string_view name) = 0;
virtual void SetPendingCharacterName(const LWOOBJID characterId, const std::string_view name) = 0;
// Updates the given character ids last login to be right now.
virtual void UpdateLastLoggedInCharacter(const uint32_t characterId) = 0;
virtual void UpdateLastLoggedInCharacter(const LWOOBJID characterId) = 0;
virtual bool IsNameInUse(const std::string_view name) = 0;
};

View File

@@ -8,13 +8,17 @@
class ICharXml {
public:
// Get the character xml for the given character id.
virtual std::string GetCharacterXml(const uint32_t charId) = 0;
virtual std::string GetCharacterXml(const LWOOBJID charId) = 0;
// Update the character xml for the given character id.
virtual void UpdateCharacterXml(const uint32_t charId, const std::string_view lxfml) = 0;
virtual void UpdateCharacterXml(const LWOOBJID charId, const std::string_view lxfml) = 0;
// Insert the character xml for the given character id.
virtual void InsertCharacterXml(const uint32_t characterId, const std::string_view lxfml) = 0;
virtual void InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) = 0;
// Get paginated list of characters with optional search/filtering for DataTables
// Returns a JSON-formatted string with the character data and metadata
virtual std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
};
#endif //!__ICHARXML__H__

View File

@@ -8,7 +8,7 @@ class ICommandLog {
public:
// Insert a new slash command log entry.
virtual void InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) = 0;
virtual void InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) = 0;
};
#endif //!__ICOMMANDLOG__H__

View File

@@ -8,25 +8,25 @@
class IFriends {
public:
struct BestFriendStatus {
uint32_t playerCharacterId{};
uint32_t friendCharacterId{};
LWOOBJID playerCharacterId{};
LWOOBJID friendCharacterId{};
uint32_t bestFriendStatus{};
};
// Get the friends list for the given character id.
virtual std::vector<FriendData> GetFriendsList(const uint32_t charId) = 0;
virtual std::vector<FriendData> GetFriendsList(const LWOOBJID charId) = 0;
// Get the best friend status for the given player and friend character ids.
virtual std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
virtual std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
// Set the best friend status for the given player and friend character ids.
virtual void SetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId, const uint32_t bestFriendStatus) = 0;
virtual void SetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId, const uint32_t bestFriendStatus) = 0;
// Add a friend to the given character id.
virtual void AddFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
virtual void AddFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
// Remove a friend from the given character id.
virtual void RemoveFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
virtual void RemoveFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
};
#endif //!__IFRIENDS__H__

View File

@@ -9,12 +9,12 @@ class IIgnoreList {
public:
struct Info {
std::string name;
uint32_t id;
LWOOBJID id;
};
virtual std::vector<Info> GetIgnoreList(const uint32_t playerId) = 0;
virtual void AddIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) = 0;
virtual void RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) = 0;
virtual std::vector<Info> GetIgnoreList(const LWOOBJID playerId) = 0;
virtual void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) = 0;
virtual void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) = 0;
};
#endif //!__IIGNORELIST__H__

View File

@@ -5,12 +5,13 @@
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
class ILeaderboard {
public:
struct Entry {
uint32_t charId{};
LWOOBJID charId{};
uint32_t lastPlayedTimestamp{};
float primaryScore{};
float secondaryScore{};
@@ -36,12 +37,12 @@ public:
virtual std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) = 0;
virtual std::vector<ILeaderboard::Entry> GetAgsLeaderboard(const uint32_t activityId) = 0;
virtual std::optional<Score> GetPlayerScore(const uint32_t playerId, const uint32_t gameId) = 0;
virtual std::optional<Score> GetPlayerScore(const LWOOBJID playerId, const uint32_t gameId) = 0;
virtual void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
virtual void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
virtual void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) = 0;
virtual void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) = 0;
virtual void SaveScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) = 0;
virtual void UpdateScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) = 0;
virtual void IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) = 0;
virtual void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) = 0;
};
#endif //!__ILEADERBOARD__H__

View File

@@ -16,13 +16,13 @@ public:
virtual void InsertNewMail(const MailInfo& mail) = 0;
// Get the mail for the given character id.
virtual std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) = 0;
virtual std::vector<MailInfo> GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) = 0;
// Get the mail for the given mail id.
virtual std::optional<MailInfo> GetMail(const uint64_t mailId) = 0;
// Get the number of unread mail for the given character id.
virtual uint32_t GetUnreadMailCount(const uint32_t characterId) = 0;
virtual uint32_t GetUnreadMailCount(const LWOOBJID characterId) = 0;
// Mark the given mail as read.
virtual void MarkMailRead(const uint64_t mailId) = 0;

View File

@@ -6,14 +6,19 @@
class IObjectIdTracker {
public:
// Only the first 48 bits of the ids are the id, the last 16 bits are reserved for flags.
struct Range {
uint64_t minID{}; // Only the first 48 bits are the id, the last 16 bits are reserved for flags.
uint64_t maxID{}; // Only the first 48 bits are the id, the last 16 bits are reserved for flags.
};
// Get the current persistent id.
virtual std::optional<uint32_t> GetCurrentPersistentId() = 0;
virtual std::optional<uint64_t> GetCurrentPersistentId() = 0;
// Insert the default persistent id.
virtual void InsertDefaultPersistentId() = 0;
// Update the persistent id.
virtual void UpdatePersistentId(const uint32_t newId) = 0;
virtual Range GetPersistentIdRange() = 0;
};
#endif //!__IOBJECTIDTRACKER__H__

View File

@@ -3,6 +3,8 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
class IPlayKeys {
public:
@@ -10,6 +12,10 @@ public:
// Optional of bool may seem pointless, however the optional indicates if the playkey exists
// and the bool indicates if the playkey is active.
virtual std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) = 0;
// Get paginated list of play keys with optional search/filtering for DataTables
// Returns a JSON-formatted string with the play key data and metadata
virtual std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
};
#endif //!__IPLAYKEYS__H__

View File

@@ -13,7 +13,7 @@ public:
std::string description;
std::string rejectionReason;
LWOOBJID id{};
uint32_t ownerId{};
LWOOBJID ownerId{};
LWOCLONEID cloneId{};
int32_t privacyOption{};
uint32_t modApproved{};
@@ -27,7 +27,7 @@ public:
uint32_t mapId{};
std::string searchString;
ePropertySortType sortChoice{};
uint32_t playerId{};
LWOOBJID playerId{};
uint32_t numResults{};
uint32_t startIndex{};
uint32_t playerSort{};
@@ -39,6 +39,9 @@ public:
std::vector<IProperty::Info> entries;
};
// Get the property info for the given property id.
virtual std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) = 0;
// Get the property info for the given property id.
virtual std::optional<IProperty::Info> GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) = 0;
@@ -61,5 +64,9 @@ public:
// Insert a new property into the database.
virtual void InsertNewProperty(const IProperty::Info& info, const uint32_t templateId, const LWOZONEID& zoneId) = 0;
// Get paginated list of properties with optional search/filtering for DataTables
// Returns a JSON-formatted string with the property data and metadata
virtual std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
};
#endif //!__IPROPERTY__H__

View File

@@ -13,19 +13,19 @@ public:
}
NiPoint3 position;
NiQuaternion rotation;
NiQuaternion rotation = QuatUtils::IDENTITY;
LWOOBJID id{};
LOT lot{};
uint32_t ugcId{};
LWOOBJID ugcId{};
std::array<LWOOBJID, 5> behaviors{};
};
// Inserts a new UGC model into the database.
virtual void InsertNewUgcModel(
std::stringstream& sd0Data,
const uint32_t blueprintId,
const uint64_t blueprintId,
const uint32_t accountId,
const uint32_t characterId) = 0;
const LWOOBJID characterId) = 0;
// Get the property models for the given property id.
virtual std::vector<IPropertyContents::Model> GetPropertyModels(const LWOOBJID& propertyId) = 0;
@@ -45,6 +45,6 @@ public:
virtual void RemoveModel(const LWOOBJID& modelId) = 0;
// Gets a model by ID
virtual Model GetModel(const LWOOBJID modelID) = 0;
virtual std::optional<Model> GetModel(const LWOOBJID modelID) = 0;
};
#endif //!__IPROPERTIESCONTENTS__H__

View File

@@ -29,5 +29,7 @@ public:
// Inserts a new UGC model into the database.
virtual void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) = 0;
virtual std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) = 0;
};
#endif //!__IUGC__H__

View File

@@ -7,7 +7,7 @@
class IUgcModularBuild {
public:
virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) = 0;
virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) = 0;
virtual void DeleteUgcBuild(const LWOOBJID bigId) = 0;
};

View File

@@ -100,7 +100,7 @@ void MySQLDatabase::SetAutoCommit(bool value) {
con->setAutoCommit(value);
}
void MySQLDatabase::DeleteCharacter(const uint32_t characterId) {
void MySQLDatabase::DeleteCharacter(const LWOOBJID characterId) {
ExecuteDelete("DELETE FROM charxml WHERE id=? LIMIT 1;", characterId);
ExecuteDelete("DELETE FROM command_log WHERE character_id=?;", characterId);
ExecuteDelete("DELETE FROM friends WHERE player_id=? OR friend_id=?;", characterId, characterId);

View File

@@ -40,31 +40,32 @@ public:
std::vector<std::string> GetApprovedCharacterNames() override;
std::vector<FriendData> GetFriendsList(uint32_t charID) override;
std::vector<FriendData> GetFriendsList(LWOOBJID charID) override;
std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) override;
void SetBestFriendStatus(const uint32_t playerAccountId, const uint32_t friendAccountId, const uint32_t bestFriendStatus) override;
void AddFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override;
void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override;
void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override;
std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) override;
void SetBestFriendStatus(const LWOOBJID playerAccountId, const LWOOBJID friendAccountId, const uint32_t bestFriendStatus) override;
void AddFriend(const LWOOBJID playerAccountId, const LWOOBJID friendAccountId) override;
void RemoveFriend(const LWOOBJID playerAccountId, const LWOOBJID friendAccountId) override;
void UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) override;
void DeleteUgcModelData(const LWOOBJID& modelId) override;
void UpdateUgcModelData(const LWOOBJID& modelId, std::stringstream& lxfml) override;
std::vector<IUgc::Model> GetAllUgcModels() override;
void CreateMigrationHistoryTable() override;
bool IsMigrationRun(const std::string_view str) override;
void InsertMigration(const std::string_view str) override;
std::optional<ICharInfo::Info> GetCharacterInfo(const uint32_t charId) override;
std::optional<ICharInfo::Info> GetCharacterInfo(const LWOOBJID charId) override;
std::optional<ICharInfo::Info> GetCharacterInfo(const std::string_view charId) override;
std::string GetCharacterXml(const uint32_t accountId) override;
void UpdateCharacterXml(const uint32_t characterId, const std::string_view lxfml) override;
std::string GetCharacterXml(const LWOOBJID accountId) override;
void UpdateCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) override;
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
void InsertNewCharacter(const ICharInfo::Info info) override;
void InsertCharacterXml(const uint32_t accountId, const std::string_view lxfml) override;
std::vector<uint32_t> GetAccountCharacterIds(uint32_t accountId) override;
void DeleteCharacter(const uint32_t characterId) override;
void SetCharacterName(const uint32_t characterId, const std::string_view name) override;
void SetPendingCharacterName(const uint32_t characterId, const std::string_view name) override;
void UpdateLastLoggedInCharacter(const uint32_t characterId) override;
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
std::vector<LWOOBJID> GetAccountCharacterIds(LWOOBJID accountId) override;
void DeleteCharacter(const LWOOBJID characterId) override;
void SetCharacterName(const LWOOBJID characterId, const std::string_view name) override;
void SetPendingCharacterName(const LWOOBJID characterId, const std::string_view name) override;
void UpdateLastLoggedInCharacter(const LWOOBJID characterId) override;
void SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) override;
std::optional<IPetNames::Info> GetPetNameInfo(const LWOOBJID& petId) override;
std::optional<IProperty::Info> GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) override;
@@ -79,34 +80,36 @@ public:
void RemoveModel(const LWOOBJID& modelId) override;
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
void InsertNewBugReport(const IBugReports::Info& info) override;
std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
void InsertNewMail(const MailInfo& mail) override;
void InsertNewUgcModel(
std::stringstream& sd0Data,
const uint32_t blueprintId,
const uint64_t blueprintId,
const uint32_t accountId,
const uint32_t characterId) override;
std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
const LWOOBJID characterId) override;
std::vector<MailInfo> GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) override;
std::optional<MailInfo> GetMail(const uint64_t mailId) override;
uint32_t GetUnreadMailCount(const uint32_t characterId) override;
uint32_t GetUnreadMailCount(const LWOOBJID characterId) override;
void MarkMailRead(const uint64_t mailId) override;
void DeleteMail(const uint64_t mailId) override;
void ClaimMailItem(const uint64_t mailId) override;
void InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) override;
void InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) override;
void UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) override;
void UpdateAccountBan(const uint32_t accountId, const bool banned) override;
void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override;
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override;
void SetMasterInfo(const IServers::MasterInfo& info) override;
std::optional<uint32_t> GetCurrentPersistentId() override;
std::optional<uint64_t> GetCurrentPersistentId() override;
IObjectIdTracker::Range GetPersistentIdRange() override;
void InsertDefaultPersistentId() override;
void UpdatePersistentId(const uint32_t id) override;
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
std::vector<IUgc::Model> GetUgcModels(const LWOOBJID& propertyId) override;
void AddIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) override;
void RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) override;
std::vector<IIgnoreList::Info> GetIgnoreList(const uint32_t playerId) override;
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
std::vector<IIgnoreList::Info> GetIgnoreList(const LWOOBJID playerId) override;
void InsertRewardCode(const uint32_t account_id, const uint32_t reward_code) override;
std::vector<uint32_t> GetRewardCodesByAccountID(const uint32_t account_id) override;
void AddBehavior(const IBehaviors::Info& info) override;
@@ -118,16 +121,27 @@ public:
std::vector<ILeaderboard::Entry> GetAscendingLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> GetNsLeaderboard(const uint32_t activityId) override;
std::vector<ILeaderboard::Entry> GetAgsLeaderboard(const uint32_t activityId) override;
void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override;
void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override;
std::optional<ILeaderboard::Score> GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override;
void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override;
void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) override;
void SaveScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) override;
void UpdateScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) override;
std::optional<ILeaderboard::Score> GetPlayerScore(const LWOOBJID playerId, const uint32_t gameId) override;
void IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) override;
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
void DeleteUgcBuild(const LWOOBJID bigId) override;
uint32_t GetAccountCount() override;
uint32_t GetCharacterCount() override;
void RecordFailedAttempt(const uint32_t accountId) override;
void ClearFailedAttempts(const uint32_t accountId) override;
void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) override;
bool IsLockedOut(const uint32_t accountId) override;
uint8_t GetFailedAttempts(const uint32_t accountId) override;
nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
nlohmann::json GetAccountById(uint32_t accountId) override;
bool IsNameInUse(const std::string_view name) override;
IPropertyContents::Model GetModel(const LWOOBJID modelID) override;
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;
std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override;
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override;
std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
private:
@@ -168,91 +182,91 @@ private:
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::string_view param) {
// LOG("%s", param.data());
LOG_DEBUG("%s", param.data());
stmt->setString(index, param.data());
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const char* param) {
// LOG("%s", param);
LOG_DEBUG("%s", param);
stmt->setString(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::string param) {
// LOG("%s", param.c_str());
LOG_DEBUG("%s", param.c_str());
stmt->setString(index, param.c_str());
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int8_t param) {
// LOG("%u", param);
LOG_DEBUG("%u", param);
stmt->setByte(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint8_t param) {
// LOG("%d", param);
LOG_DEBUG("%d", param);
stmt->setByte(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int16_t param) {
// LOG("%u", param);
LOG_DEBUG("%u", param);
stmt->setShort(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint16_t param) {
// LOG("%d", param);
LOG_DEBUG("%d", param);
stmt->setShort(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint32_t param) {
// LOG("%u", param);
LOG_DEBUG("%u", param);
stmt->setUInt(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int32_t param) {
// LOG("%d", param);
LOG_DEBUG("%d", param);
stmt->setInt(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int64_t param) {
// LOG("%llu", param);
LOG_DEBUG("%llu", param);
stmt->setInt64(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint64_t param) {
// LOG("%llu", param);
LOG_DEBUG("%llu", param);
stmt->setUInt64(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const float param) {
// LOG("%f", param);
LOG_DEBUG("%f", param);
stmt->setFloat(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const double param) {
// LOG("%f", param);
LOG_DEBUG("%f", param);
stmt->setDouble(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const bool param) {
// LOG("%d", param);
LOG_DEBUG("%s", param ? "true" : "false");
stmt->setBoolean(index, param);
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::istream* param) {
// LOG("Blob");
LOG_DEBUG("Blob");
// This is the one time you will ever see me use const_cast.
stmt->setBlob(index, const_cast<std::istream*>(param));
}
@@ -260,10 +274,21 @@ inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::istr
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::optional<uint32_t> param) {
if (param) {
// LOG("%d", param.value());
LOG_DEBUG("%d", param.value());
stmt->setInt(index, param.value());
} else {
// LOG("Null");
LOG_DEBUG("Null");
stmt->setNull(index, sql::DataType::SQLNULL);
}
}
template<>
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::optional<LWOOBJID> param) {
if (param) {
LOG_DEBUG("%d", param.value());
stmt->setInt64(index, param.value());
} else {
LOG_DEBUG("Null");
stmt->setNull(index, sql::DataType::SQLNULL);
}
}

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