Compare commits

...

59 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
348 changed files with 15119 additions and 2022 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

@@ -110,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)
@@ -126,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)
@@ -322,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

@@ -7,6 +7,7 @@ FetchContent_Declare(
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

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

View File

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

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

View File

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

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

@@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
}
}
newTeam->lootFlag = 1;
newTeam->lootFlag = 0;
TeamStatusUpdate(newTeam);

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

@@ -55,6 +55,7 @@ elseif (WIN32)
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.

View File

@@ -3,7 +3,7 @@
// C++
#include <charconv>
#include <cstdint>
#include <cmath>
#include <cmath>
#include <ctime>
#include <functional>
#include <optional>
@@ -19,6 +19,7 @@
#include "dPlatforms.h"
#include "Game.h"
#include "Logger.h"
#include "DluAssert.h"
#include <glm/ext/vector_float3.hpp>
@@ -305,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

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

@@ -5,7 +5,8 @@ enum class ServiceType : uint16_t {
COMMON = 0,
AUTH,
CHAT,
WORLD = 4,
DASHBOARD,
WORLD,
CLIENT,
MASTER,
UNKNOWN

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

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

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

@@ -16,16 +16,16 @@ public:
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);
}
}

View File

@@ -1,6 +1,7 @@
#include "MySQLDatabase.h"
#include "eGameMasterLevel.h"
#include "json.hpp"
std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_view username) {
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire FROM accounts WHERE name = ? LIMIT 1;", username);
@@ -45,3 +46,142 @@ uint32_t MySQLDatabase::GetAccountCount() {
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
return res->next() ? res->getUInt("count") : 0;
}
void MySQLDatabase::RecordFailedAttempt(const uint32_t accountId) {
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
}
void MySQLDatabase::ClearFailedAttempts(const uint32_t accountId) {
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = NOW() WHERE id = ?;", accountId);
}
void MySQLDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
ExecuteUpdate("UPDATE accounts SET lockout_time = FROM_UNIXTIME(?) WHERE id = ?;", lockoutUntil, accountId);
}
bool MySQLDatabase::IsLockedOut(const uint32_t accountId) {
auto result = ExecuteSelect("SELECT lockout_time FROM accounts WHERE id = ?;", accountId);
if (!result->next()) {
return false;
}
// If lockout_time is set and in the future, account is locked
const char* lockoutTime = result->getString("lockout_time").c_str();
if (lockoutTime == nullptr || strlen(lockoutTime) == 0) {
return false;
}
// Simplified check - if lockout_time exists and is not null, it's locked
return true;
}
uint8_t MySQLDatabase::GetFailedAttempts(const uint32_t accountId) {
auto result = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
if (!result->next()) {
return 0;
}
return result->getUInt("failed_attempts");
}
nlohmann::json MySQLDatabase::GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
// Build base query
std::string baseQuery = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts";
std::string whereClause;
std::string orderClause;
// Add search filter if provided
if (!search.empty()) {
whereClause = " WHERE name LIKE CONCAT('%', ?, '%')";
}
// Map column indices to database columns
std::string orderColumnName = "id";
switch (orderColumn) {
case 0: orderColumnName = "id"; break;
case 1: orderColumnName = "name"; break;
case 2: orderColumnName = "banned"; break;
case 3: orderColumnName = "locked"; break;
case 4: orderColumnName = "gm_level"; break;
case 5: orderColumnName = "mute_expire"; break;
case 6: orderColumnName = "created_at"; break;
default: orderColumnName = "id";
}
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
// Build the main query
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
// Get total count
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
auto totalCountResult = ExecuteSelect(totalCountQuery);
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
// Get filtered count
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE name LIKE CONCAT('%', ?, '%');";
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
}
// Execute main query
std::unique_ptr<sql::ResultSet> result;
if (!search.empty()) {
result = ExecuteSelect(mainQuery, search, start, length);
} else {
result = ExecuteSelect(mainQuery, start, length);
}
// Build response JSON
nlohmann::json accountsArray = nlohmann::json::array();
while (result->next()) {
nlohmann::json account = {
{"id", result->getUInt("id")},
{"name", result->getString("name")},
{"banned", result->getBoolean("banned")},
{"locked", result->getBoolean("locked")},
{"gm_level", result->getInt("gm_level")},
{"mute_expire", result->getUInt64("mute_expire")},
{"created_at", result->getString("created_at")}
};
accountsArray.push_back(account);
}
nlohmann::json response = {
{"draw", 1},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", accountsArray}
};
return response;
}
nlohmann::json MySQLDatabase::GetAccountById(uint32_t accountId) {
try {
const std::string query = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts WHERE id = ?;";
auto result = ExecuteSelect(query, accountId);
if (!result->next()) {
return nlohmann::json{{"error", "Account not found"}};
}
nlohmann::json account = {
{"id", result->getUInt("id")},
{"name", result->getString("name")},
{"banned", result->getBoolean("banned")},
{"locked", result->getBoolean("locked")},
{"gm_level", result->getInt("gm_level")},
{"mute_expire", result->getUInt64("mute_expire")},
{"created_at", result->getString("created_at")}
};
return account;
} catch (const sql::SQLException& e) {
LOG_DEBUG("SQL Error: %s", e.what());
return nlohmann::json{{"error", "Database error"}};
}
}

View File

@@ -1,6 +1,6 @@
#include "MySQLDatabase.h"
void MySQLDatabase::UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) {
void MySQLDatabase::UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) {
ExecuteInsert("INSERT INTO activity_log (character_id, activity, time, map_id) VALUES (?, ?, ?, ?);",
characterId, static_cast<uint32_t>(activityType), static_cast<uint32_t>(time(NULL)), mapId);
}

View File

@@ -4,3 +4,77 @@ void MySQLDatabase::InsertNewBugReport(const IBugReports::Info& info) {
ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)",
info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId);
}
#include "json.hpp"
std::string MySQLDatabase::GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
// Build base query
std::string baseQuery = "SELECT id, body, client_version, other_player_id, selection, submitted FROM bug_reports";
std::string whereClause;
std::string orderClause;
// Add search filter if provided
if (!search.empty()) {
whereClause = " WHERE body LIKE CONCAT('%', ?, '%') OR other_player_id LIKE CONCAT('%', ?, '%')";
}
// Map column indices to database columns
std::string orderColumnName = "id";
switch (orderColumn) {
case 0: orderColumnName = "id"; break;
case 1: orderColumnName = "other_player_id"; break;
case 2: orderColumnName = "client_version"; break;
case 3: orderColumnName = "submitted"; break;
default: orderColumnName = "id";
}
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
// Build the main query
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
// Get total count
std::string totalCountQuery = "SELECT COUNT(*) as count FROM bug_reports;";
auto totalCountResult = ExecuteSelect(totalCountQuery);
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
// Get filtered count
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM bug_reports WHERE body LIKE CONCAT('%', ?, '%') OR other_player_id LIKE CONCAT('%', ?, '%');";
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search, search);
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
}
// Execute main query
std::unique_ptr<sql::ResultSet> result;
if (!search.empty()) {
result = ExecuteSelect(mainQuery, search, search, start, length);
} else {
result = ExecuteSelect(mainQuery, start, length);
}
// Build response JSON
nlohmann::json reportsArray = nlohmann::json::array();
while (result->next()) {
nlohmann::json report = {
{"id", result->getUInt("id")},
{"other_player_id", result->getString("other_player_id")},
{"client_version", result->getString("client_version")},
{"selection", result->getString("selection")},
{"submitted", result->getString("submitted")},
{"body", result->getString("body")}
};
reportsArray.push_back(report);
}
nlohmann::json response = {
{"draw", 0},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", reportsArray}
};
return response.dump();
}

View File

@@ -19,7 +19,7 @@ std::optional<ICharInfo::Info> CharInfoFromQueryResult(std::unique_ptr<sql::Resu
ICharInfo::Info toReturn;
toReturn.id = stmt->getUInt("id");
toReturn.id = stmt->getInt64("id");
toReturn.name = stmt->getString("name").c_str();
toReturn.pendingName = stmt->getString("pending_name").c_str();
toReturn.needsRename = stmt->getBoolean("needs_rename");
@@ -30,7 +30,7 @@ std::optional<ICharInfo::Info> CharInfoFromQueryResult(std::unique_ptr<sql::Resu
return toReturn;
}
std::optional<ICharInfo::Info> MySQLDatabase::GetCharacterInfo(const uint32_t charId) {
std::optional<ICharInfo::Info> MySQLDatabase::GetCharacterInfo(const LWOOBJID charId) {
return CharInfoFromQueryResult(
ExecuteSelect("SELECT name, pending_name, needs_rename, prop_clone_id, permission_map, id, account_id FROM charinfo WHERE id = ? LIMIT 1;", charId)
);
@@ -42,18 +42,23 @@ std::optional<ICharInfo::Info> MySQLDatabase::GetCharacterInfo(const std::string
);
}
std::vector<uint32_t> MySQLDatabase::GetAccountCharacterIds(const uint32_t accountId) {
std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accountId) {
auto result = ExecuteSelect("SELECT id FROM charinfo WHERE account_id = ? ORDER BY last_login DESC LIMIT 4;", accountId);
std::vector<uint32_t> toReturn;
std::vector<LWOOBJID> toReturn;
toReturn.reserve(result->rowsCount());
while (result->next()) {
toReturn.push_back(result->getUInt("id"));
toReturn.push_back(result->getInt64("id"));
}
return toReturn;
}
uint32_t MySQLDatabase::GetCharacterCount() {
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
return res->next() ? res->getUInt("count") : 0;
}
void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",
@@ -65,15 +70,15 @@ void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
static_cast<uint32_t>(time(NULL)));
}
void MySQLDatabase::SetCharacterName(const uint32_t characterId, const std::string_view name) {
void MySQLDatabase::SetCharacterName(const LWOOBJID characterId, const std::string_view name) {
ExecuteUpdate("UPDATE charinfo SET name = ?, pending_name = '', needs_rename = 0, last_login = ? WHERE id = ? LIMIT 1;", name, static_cast<uint32_t>(time(NULL)), characterId);
}
void MySQLDatabase::SetPendingCharacterName(const uint32_t characterId, const std::string_view name) {
void MySQLDatabase::SetPendingCharacterName(const LWOOBJID characterId, const std::string_view name) {
ExecuteUpdate("UPDATE charinfo SET pending_name = ?, needs_rename = 0, last_login = ? WHERE id = ? LIMIT 1", name, static_cast<uint32_t>(time(NULL)), characterId);
}
void MySQLDatabase::UpdateLastLoggedInCharacter(const uint32_t characterId) {
void MySQLDatabase::UpdateLastLoggedInCharacter(const LWOOBJID characterId) {
ExecuteUpdate("UPDATE charinfo SET last_login = ? WHERE id = ? LIMIT 1", static_cast<uint32_t>(time(NULL)), characterId);
}

View File

@@ -1,6 +1,6 @@
#include "MySQLDatabase.h"
std::string MySQLDatabase::GetCharacterXml(const uint32_t charId) {
std::string MySQLDatabase::GetCharacterXml(const LWOOBJID charId) {
auto result = ExecuteSelect("SELECT xml_data FROM charxml WHERE id = ? LIMIT 1;", charId);
if (!result->next()) {
@@ -10,10 +10,82 @@ std::string MySQLDatabase::GetCharacterXml(const uint32_t charId) {
return result->getString("xml_data").c_str();
}
void MySQLDatabase::UpdateCharacterXml(const uint32_t charId, const std::string_view lxfml) {
void MySQLDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string_view lxfml) {
ExecuteUpdate("UPDATE charxml SET xml_data = ? WHERE id = ?;", lxfml, charId);
}
void MySQLDatabase::InsertCharacterXml(const uint32_t characterId, const std::string_view lxfml) {
void MySQLDatabase::InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) {
ExecuteInsert("INSERT INTO `charxml` (`id`, `xml_data`) VALUES (?,?)", characterId, lxfml);
}
#include "json.hpp"
std::string MySQLDatabase::GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
// Build base query
std::string baseQuery = "SELECT c.id, c.name, c.account_id, c.last_login, a.name as account_name FROM charinfo c JOIN accounts a ON c.account_id = a.id";
std::string whereClause;
std::string orderClause;
// Add search filter if provided
if (!search.empty()) {
whereClause = " WHERE c.name LIKE CONCAT('%', ?, '%')";
}
// Map column indices to database columns
std::string orderColumnName = "c.id";
switch (orderColumn) {
case 0: orderColumnName = "c.id"; break;
case 1: orderColumnName = "c.name"; break;
case 2: orderColumnName = "a.name"; break;
case 3: orderColumnName = "c.last_login"; break;
default: orderColumnName = "c.id";
}
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
// Build the main query
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
// Get total count
std::string totalCountQuery = "SELECT COUNT(*) as count FROM charinfo;";
auto totalCountResult = ExecuteSelect(totalCountQuery);
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
// Get filtered count
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM charinfo WHERE name LIKE CONCAT('%', ?, '%');";
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
}
// Execute main query
std::unique_ptr<sql::ResultSet> result;
if (!search.empty()) {
result = ExecuteSelect(mainQuery, search, start, length);
} else {
result = ExecuteSelect(mainQuery, start, length);
}
// Build response JSON
nlohmann::json charactersArray = nlohmann::json::array();
while (result->next()) {
nlohmann::json character = {
{"id", result->getUInt64("id")},
{"name", result->getString("name")},
{"account_name", result->getString("account_name")},
{"last_login", result->getUInt64("last_login")}
};
charactersArray.push_back(character);
}
nlohmann::json response = {
{"draw", 0},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", charactersArray}
};
return response.dump();
}

View File

@@ -1,5 +1,5 @@
#include "MySQLDatabase.h"
void MySQLDatabase::InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) {
void MySQLDatabase::InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) {
ExecuteInsert("INSERT INTO command_log (character_id, command) VALUES (?, ?);", characterId, command);
}

View File

@@ -1,6 +1,6 @@
#include "MySQLDatabase.h"
std::vector<FriendData> MySQLDatabase::GetFriendsList(const uint32_t charId) {
std::vector<FriendData> MySQLDatabase::GetFriendsList(const LWOOBJID charId) {
auto friendsList = ExecuteSelect(
R"QUERY(
SELECT fr.requested_player AS player, best_friend AS bff, ci.name AS name FROM
@@ -19,7 +19,7 @@ std::vector<FriendData> MySQLDatabase::GetFriendsList(const uint32_t charId) {
while (friendsList->next()) {
FriendData fd;
fd.friendID = friendsList->getUInt("player");
fd.friendID = friendsList->getUInt64("player");
fd.isBestFriend = friendsList->getInt("bff") == 3; // 0 = friends, 1 = left_requested, 2 = right_requested, 3 = both_accepted - are now bffs
fd.friendName = friendsList->getString("name").c_str();
@@ -29,7 +29,7 @@ std::vector<FriendData> MySQLDatabase::GetFriendsList(const uint32_t charId) {
return toReturn;
}
std::optional<IFriends::BestFriendStatus> MySQLDatabase::GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) {
std::optional<IFriends::BestFriendStatus> MySQLDatabase::GetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) {
auto result = ExecuteSelect("SELECT * FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;",
playerCharacterId,
friendCharacterId,
@@ -42,14 +42,14 @@ std::optional<IFriends::BestFriendStatus> MySQLDatabase::GetBestFriendStatus(con
}
IFriends::BestFriendStatus toReturn;
toReturn.playerCharacterId = result->getUInt("player_id");
toReturn.friendCharacterId = result->getUInt("friend_id");
toReturn.playerCharacterId = result->getUInt64("player_id");
toReturn.friendCharacterId = result->getUInt64("friend_id");
toReturn.bestFriendStatus = result->getUInt("best_friend");
return toReturn;
}
void MySQLDatabase::SetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId, const uint32_t bestFriendStatus) {
void MySQLDatabase::SetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId, const uint32_t bestFriendStatus) {
ExecuteUpdate("UPDATE friends SET best_friend = ? WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;",
bestFriendStatus,
playerCharacterId,
@@ -59,11 +59,11 @@ void MySQLDatabase::SetBestFriendStatus(const uint32_t playerCharacterId, const
);
}
void MySQLDatabase::AddFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) {
void MySQLDatabase::AddFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) {
ExecuteInsert("INSERT IGNORE INTO friends (player_id, friend_id, best_friend) VALUES (?, ?, 0);", playerCharacterId, friendCharacterId);
}
void MySQLDatabase::RemoveFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) {
void MySQLDatabase::RemoveFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) {
ExecuteDelete("DELETE FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?) LIMIT 1;",
playerCharacterId,
friendCharacterId,

View File

@@ -1,22 +1,22 @@
#include "MySQLDatabase.h"
std::vector<IIgnoreList::Info> MySQLDatabase::GetIgnoreList(const uint32_t playerId) {
std::vector<IIgnoreList::Info> MySQLDatabase::GetIgnoreList(const LWOOBJID playerId) {
auto result = ExecuteSelect("SELECT ci.name AS name, il.ignored_player_id AS ignore_id FROM ignore_list AS il JOIN charinfo AS ci ON il.ignored_player_id = ci.id WHERE il.player_id = ?", playerId);
std::vector<IIgnoreList::Info> ignoreList;
ignoreList.reserve(result->rowsCount());
while (result->next()) {
ignoreList.push_back(IIgnoreList::Info{ result->getString("name").c_str(), result->getUInt("ignore_id") });
ignoreList.push_back(IIgnoreList::Info{ result->getString("name").c_str(), result->getInt64("ignore_id") });
}
return ignoreList;
}
void MySQLDatabase::AddIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) {
void MySQLDatabase::AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) {
ExecuteInsert("INSERT IGNORE INTO ignore_list (player_id, ignored_player_id) VALUES (?, ?)", playerId, ignoredPlayerId);
}
void MySQLDatabase::RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) {
void MySQLDatabase::RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) {
ExecuteDelete("DELETE FROM ignore_list WHERE player_id = ? AND ignored_player_id = ?", playerId, ignoredPlayerId);
}

View File

@@ -21,7 +21,7 @@ std::vector<ILeaderboard::Entry> ProcessQuery(UniqueResultSet& rows) {
while (rows->next()) {
auto& entry = entries.emplace_back();
entry.charId = rows->getUInt("character_id");
entry.charId = rows->getUInt64("character_id");
entry.lastPlayedTimestamp = rows->getUInt("lp_unix");
entry.primaryScore = rows->getFloat("primaryScore");
entry.secondaryScore = rows->getFloat("secondaryScore");
@@ -58,21 +58,21 @@ std::vector<ILeaderboard::Entry> MySQLDatabase::GetNsLeaderboard(const uint32_t
return ProcessQuery(leaderboard);
}
void MySQLDatabase::SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) {
void MySQLDatabase::SaveScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) {
ExecuteInsert("INSERT leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, character_id = ?, game_id = ?;",
score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId);
}
void MySQLDatabase::UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) {
void MySQLDatabase::UpdateScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) {
ExecuteInsert("UPDATE leaderboard SET primaryScore = ?, secondaryScore = ?, tertiaryScore = ?, timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;",
score.primaryScore, score.secondaryScore, score.tertiaryScore, playerId, gameId);
}
void MySQLDatabase::IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) {
void MySQLDatabase::IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) {
ExecuteUpdate("UPDATE leaderboard SET timesPlayed = timesPlayed + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId);
}
std::optional<ILeaderboard::Score> MySQLDatabase::GetPlayerScore(const uint32_t playerId, const uint32_t gameId) {
std::optional<ILeaderboard::Score> MySQLDatabase::GetPlayerScore(const LWOOBJID playerId, const uint32_t gameId) {
std::optional<ILeaderboard::Score> toReturn = std::nullopt;
auto res = ExecuteSelect("SELECT * FROM leaderboard WHERE character_id = ? AND game_id = ?;", playerId, gameId);
if (res->next()) {
@@ -86,6 +86,6 @@ std::optional<ILeaderboard::Score> MySQLDatabase::GetPlayerScore(const uint32_t
return toReturn;
}
void MySQLDatabase::IncrementNumWins(const uint32_t playerId, const uint32_t gameId) {
void MySQLDatabase::IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) {
ExecuteUpdate("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId);
}

View File

@@ -19,7 +19,7 @@ void MySQLDatabase::InsertNewMail(const MailInfo& mail) {
mail.itemCount);
}
std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) {
std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) {
auto res = ExecuteSelect(
"SELECT id, subject, body, sender_name, attachment_id, attachment_lot, attachment_subkey, attachment_count, was_read, time_sent"
" FROM mail WHERE receiver_id=? limit ?;",
@@ -61,7 +61,7 @@ std::optional<MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) {
return toReturn;
}
uint32_t MySQLDatabase::GetUnreadMailCount(const uint32_t characterId) {
uint32_t MySQLDatabase::GetUnreadMailCount(const LWOOBJID characterId) {
auto res = ExecuteSelect("SELECT COUNT(*) AS number_unread FROM mail WHERE receiver_id=? AND was_read=0;", characterId);
if (!res->next()) {

View File

@@ -1,17 +1,42 @@
#include "MySQLDatabase.h"
std::optional<uint32_t> MySQLDatabase::GetCurrentPersistentId() {
std::optional<uint64_t> MySQLDatabase::GetCurrentPersistentId() {
auto result = ExecuteSelect("SELECT last_object_id FROM object_id_tracker");
if (!result->next()) {
return std::nullopt;
}
return result->getUInt("last_object_id");
return result->getUInt64("last_object_id");
}
void MySQLDatabase::InsertDefaultPersistentId() {
ExecuteInsert("INSERT INTO object_id_tracker VALUES (1);");
}
void MySQLDatabase::UpdatePersistentId(const uint32_t newId) {
ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = ?;", newId);
IObjectIdTracker::Range MySQLDatabase::GetPersistentIdRange() {
IObjectIdTracker::Range range;
auto prevCommit = GetAutoCommit();
SetAutoCommit(false);
// THIS MUST ABSOLUTELY NOT FAIL. These IDs are expected to be unique. As such a transactional select is used to safely get a range
// of IDs that will never be used again. A separate feature could track unused IDs and recycle them, but that is not implemented.
ExecuteCustomQuery("START TRANSACTION;");
// 200 seems like a good range to reserve at once. Only way this would be noticable is if a player
// added hundreds of items at once.
// Doing the update first ensures that all other connections are blocked from accessing this table until we commit.
auto result = ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = last_object_id + 200;");
// If no rows were updated, it means the table is empty, so we need to insert the default row first.
if (result == 0) {
InsertDefaultPersistentId();
result = ExecuteUpdate("UPDATE object_id_tracker SET last_object_id = last_object_id + 200;");
}
// At this point all connections are waiting on us to finish the transaction, so we can safely select the ID we just set.
auto selectRes = ExecuteSelect("SELECT last_object_id FROM object_id_tracker;");
selectRes->next();
range.maxID = selectRes->getUInt64("last_object_id");
range.minID = range.maxID - 199;
ExecuteCustomQuery("COMMIT;");
SetAutoCommit(prevCommit);
return range;
}

View File

@@ -9,3 +9,77 @@ std::optional<bool> MySQLDatabase::IsPlaykeyActive(const int32_t playkeyId) {
return keyCheckRes->getBoolean("active");
}
#include "json.hpp"
std::string MySQLDatabase::GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
// Build base query
std::string baseQuery = "SELECT id, key_string, key_uses, created_at, active FROM play_keys";
std::string whereClause;
std::string orderClause;
// Add search filter if provided
if (!search.empty()) {
whereClause = " WHERE key_string LIKE CONCAT('%', ?, '%')";
}
// Map column indices to database columns
std::string orderColumnName = "id";
switch (orderColumn) {
case 0: orderColumnName = "id"; break;
case 1: orderColumnName = "key_string"; break;
case 2: orderColumnName = "key_uses"; break;
case 3: orderColumnName = "created_at"; break;
case 4: orderColumnName = "active"; break;
default: orderColumnName = "id";
}
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
// Build the main query
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
// Get total count
std::string totalCountQuery = "SELECT COUNT(*) as count FROM play_keys;";
auto totalCountResult = ExecuteSelect(totalCountQuery);
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
// Get filtered count
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM play_keys WHERE key_string LIKE CONCAT('%', ?, '%');";
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
}
// Execute main query
std::unique_ptr<sql::ResultSet> result;
if (!search.empty()) {
result = ExecuteSelect(mainQuery, search, start, length);
} else {
result = ExecuteSelect(mainQuery, start, length);
}
// Build response JSON
nlohmann::json keysArray = nlohmann::json::array();
while (result->next()) {
nlohmann::json key = {
{"id", result->getUInt("id")},
{"key_string", result->getString("key_string")},
{"key_uses", result->getUInt("key_uses")},
{"created_at", result->getString("created_at")},
{"active", result->getBoolean("active")}
};
keysArray.push_back(key);
}
nlohmann::json response = {
{"draw", 0},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", keysArray}
};
return response.dump();
}

View File

@@ -1,6 +1,23 @@
#include "MySQLDatabase.h"
#include "ePropertySortType.h"
IProperty::Info ReadPropertyInfo(UniqueResultSet& result) {
IProperty::Info info;
info.id = result->getUInt64("id");
info.ownerId = result->getInt64("owner_id");
info.cloneId = result->getUInt64("clone_id");
info.name = result->getString("name").c_str();
info.description = result->getString("description").c_str();
info.privacyOption = result->getInt("privacy_option");
info.rejectionReason = result->getString("rejection_reason").c_str();
info.lastUpdatedTime = result->getUInt("last_updated");
info.claimedTime = result->getUInt("time_claimed");
info.reputation = result->getUInt("reputation");
info.modApproved = result->getUInt("mod_approved");
info.performanceCost = result->getFloat("performance_cost");
return info;
}
std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(const IProperty::PropertyLookup& params) {
std::optional<IProperty::PropertyEntranceResult> result;
std::string query;
@@ -117,19 +134,7 @@ std::optional<IProperty::PropertyEntranceResult> MySQLDatabase::GetProperties(co
while (properties->next()) {
if (!result) result = IProperty::PropertyEntranceResult();
auto& entry = result->entries.emplace_back();
entry.id = properties->getUInt64("id");
entry.ownerId = properties->getUInt64("owner_id");
entry.cloneId = properties->getUInt64("clone_id");
entry.name = properties->getString("name").c_str();
entry.description = properties->getString("description").c_str();
entry.privacyOption = properties->getInt("privacy_option");
entry.rejectionReason = properties->getString("rejection_reason").c_str();
entry.lastUpdatedTime = properties->getUInt("last_updated");
entry.claimedTime = properties->getUInt("time_claimed");
entry.reputation = properties->getUInt("reputation");
entry.modApproved = properties->getUInt("mod_approved");
entry.performanceCost = properties->getFloat("performance_cost");
result->entries.push_back(ReadPropertyInfo(properties));
}
return result;
@@ -144,21 +149,7 @@ std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOMAPID map
return std::nullopt;
}
IProperty::Info toReturn;
toReturn.id = propertyEntry->getUInt64("id");
toReturn.ownerId = propertyEntry->getUInt64("owner_id");
toReturn.cloneId = propertyEntry->getUInt64("clone_id");
toReturn.name = propertyEntry->getString("name").c_str();
toReturn.description = propertyEntry->getString("description").c_str();
toReturn.privacyOption = propertyEntry->getInt("privacy_option");
toReturn.rejectionReason = propertyEntry->getString("rejection_reason").c_str();
toReturn.lastUpdatedTime = propertyEntry->getUInt("last_updated");
toReturn.claimedTime = propertyEntry->getUInt("time_claimed");
toReturn.reputation = propertyEntry->getUInt("reputation");
toReturn.modApproved = propertyEntry->getUInt("mod_approved");
toReturn.performanceCost = propertyEntry->getFloat("performance_cost");
return toReturn;
return ReadPropertyInfo(propertyEntry);
}
void MySQLDatabase::UpdatePropertyModerationInfo(const IProperty::Info& info) {
@@ -195,3 +186,91 @@ void MySQLDatabase::InsertNewProperty(const IProperty::Info& info, const uint32_
zoneId.GetMapID()
);
}
std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOOBJID id) {
auto propertyEntry = ExecuteSelect(
"SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost "
"FROM properties WHERE id = ?;", id);
if (!propertyEntry->next()) {
return std::nullopt;
}
return ReadPropertyInfo(propertyEntry);
}
#include "json.hpp"
std::string MySQLDatabase::GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
// Build base query
std::string baseQuery = "SELECT id, owner_id, name, mod_approved, reputation, zone_id FROM properties";
std::string whereClause;
std::string orderClause;
// Add search filter if provided
if (!search.empty()) {
whereClause = " WHERE name LIKE CONCAT('%', ?, '%')";
}
// Map column indices to database columns
std::string orderColumnName = "id";
switch (orderColumn) {
case 0: orderColumnName = "id"; break;
case 1: orderColumnName = "name"; break;
case 2: orderColumnName = "owner_id"; break;
case 3: orderColumnName = "mod_approved"; break;
case 4: orderColumnName = "reputation"; break;
case 5: orderColumnName = "zone_id"; break;
default: orderColumnName = "id";
}
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
// Build the main query
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
// Get total count
std::string totalCountQuery = "SELECT COUNT(*) as count FROM properties;";
auto totalCountResult = ExecuteSelect(totalCountQuery);
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
// Get filtered count
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM properties WHERE name LIKE CONCAT('%', ?, '%');";
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search);
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
}
// Execute main query
std::unique_ptr<sql::ResultSet> result;
if (!search.empty()) {
result = ExecuteSelect(mainQuery, search, start, length);
} else {
result = ExecuteSelect(mainQuery, start, length);
}
// Build response JSON
nlohmann::json propertiesArray = nlohmann::json::array();
while (result->next()) {
nlohmann::json property = {
{"id", result->getUInt64("id")},
{"owner_id", result->getUInt64("owner_id")},
{"name", result->getString("name")},
{"mod_approved", result->getBoolean("mod_approved")},
{"reputation", result->getUInt64("reputation")},
{"zone_id", result->getUInt("zone_id")}
};
propertiesArray.push_back(property);
}
nlohmann::json response = {
{"draw", 0},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", propertiesArray}
};
return response.dump();
}

View File

@@ -64,26 +64,27 @@ void MySQLDatabase::RemoveModel(const LWOOBJID& modelId) {
ExecuteDelete("DELETE FROM properties_contents WHERE id = ?;", modelId);
}
IPropertyContents::Model MySQLDatabase::GetModel(const LWOOBJID modelID) {
std::optional<IPropertyContents::Model> MySQLDatabase::GetModel(const LWOOBJID modelID) {
auto result = ExecuteSelect("SELECT * FROM properties_contents WHERE id = ?", modelID);
IPropertyContents::Model model{};
std::optional<IPropertyContents::Model> model = std::nullopt;
while (result->next()) {
model.id = result->getUInt64("id");
model.lot = static_cast<LOT>(result->getUInt("lot"));
model.position.x = result->getFloat("x");
model.position.y = result->getFloat("y");
model.position.z = result->getFloat("z");
model.rotation.w = result->getFloat("rw");
model.rotation.x = result->getFloat("rx");
model.rotation.y = result->getFloat("ry");
model.rotation.z = result->getFloat("rz");
model.ugcId = result->getUInt64("ugc_id");
model.behaviors[0] = result->getUInt64("behavior_1");
model.behaviors[1] = result->getUInt64("behavior_2");
model.behaviors[2] = result->getUInt64("behavior_3");
model.behaviors[3] = result->getUInt64("behavior_4");
model.behaviors[4] = result->getUInt64("behavior_5");
model = IPropertyContents::Model{};
model->id = result->getUInt64("id");
model->lot = static_cast<LOT>(result->getUInt("lot"));
model->position.x = result->getFloat("x");
model->position.y = result->getFloat("y");
model->position.z = result->getFloat("z");
model->rotation.w = result->getFloat("rw");
model->rotation.x = result->getFloat("rx");
model->rotation.y = result->getFloat("ry");
model->rotation.z = result->getFloat("rz");
model->ugcId = result->getUInt64("ugc_id");
model->behaviors[0] = result->getUInt64("behavior_1");
model->behaviors[1] = result->getUInt64("behavior_2");
model->behaviors[2] = result->getUInt64("behavior_3");
model->behaviors[3] = result->getUInt64("behavior_4");
model->behaviors[4] = result->getUInt64("behavior_5");
}
return model;

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