mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-06-14 02:34:20 +00:00
Compare commits
1 Commits
web-dashbo
...
limit-user
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277b415196 |
29
.github/copilot-instructions.md
vendored
29
.github/copilot-instructions.md
vendored
@@ -1,29 +0,0 @@
|
||||
# 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.
|
||||
@@ -19,7 +19,6 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Export the compile commands for debuggi
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0063 NEW) # Set CMAKE visibility policy to NEW on project and subprojects
|
||||
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) # Set C and C++ symbol visibility to hide inlined functions
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
set(FETCHCONTENT_QUIET FALSE) # GLM takes a long time to clone, this will at least show _something_ while its downloading
|
||||
|
||||
# Read variables from file
|
||||
FILE(READ "${CMAKE_SOURCE_DIR}/CMakeVariables.txt" variables)
|
||||
@@ -110,8 +109,6 @@ 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)
|
||||
@@ -128,7 +125,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" "dashboardconfig.ini" "blocklist.dcf")
|
||||
set(RESOURCE_FILES "sharedconfig.ini" "authconfig.ini" "chatconfig.ini" "worldconfig.ini" "masterconfig.ini" "blocklist.dcf")
|
||||
message(STATUS "Checking resource file integrity")
|
||||
|
||||
include(Utils)
|
||||
@@ -309,7 +306,7 @@ add_subdirectory(dServer)
|
||||
add_subdirectory(dWeb)
|
||||
|
||||
# Create a list of common libraries shared between all binaries
|
||||
set(COMMON_LIBRARIES glm::glm "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
|
||||
set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
|
||||
|
||||
# Add platform specific common libraries
|
||||
if(UNIX)
|
||||
@@ -324,7 +321,6 @@ 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(
|
||||
|
||||
@@ -6,8 +6,6 @@ FetchContent_Declare(
|
||||
googletest
|
||||
GIT_REPOSITORY https://github.com/google/googletest.git
|
||||
GIT_TAG release-1.12.1
|
||||
GIT_PROGRESS TRUE
|
||||
GIT_SHALLOW 1
|
||||
)
|
||||
|
||||
# For Windows: Prevent overriding the parent project's compiler/linker settings
|
||||
|
||||
@@ -52,7 +52,6 @@ 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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
set(DCHATFILTER_SOURCES "dChatFilter.cpp")
|
||||
|
||||
add_library(dChatFilter STATIC ${DCHATFILTER_SOURCES})
|
||||
target_link_libraries(dChatFilter dDatabase glm::glm)
|
||||
target_link_libraries(dChatFilter dDatabase)
|
||||
|
||||
@@ -14,6 +14,6 @@ add_compile_definitions(ChatServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}
|
||||
add_library(dChatServer ${DCHATSERVER_SOURCES})
|
||||
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer" "${PROJECT_SOURCE_DIR}/dChatFilter")
|
||||
|
||||
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter glm::glm)
|
||||
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter)
|
||||
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose dWeb)
|
||||
|
||||
|
||||
@@ -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(playerId);
|
||||
auto ignoreList = Database::Get()->GetIgnoreList(static_cast<uint32_t>(playerId));
|
||||
if (ignoreList.empty()) {
|
||||
LOG_DEBUG("Player %llu has no ignores", playerId);
|
||||
return;
|
||||
@@ -43,6 +43,7 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +114,9 @@ void ChatIgnoreList::AddIgnore(Packet* packet) {
|
||||
}
|
||||
|
||||
if (ignoredPlayerId != LWOOBJID_EMPTY) {
|
||||
Database::Get()->AddIgnore(playerId, ignoredPlayerId);
|
||||
Database::Get()->AddIgnore(static_cast<uint32_t>(playerId), static_cast<uint32_t>(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());
|
||||
@@ -155,7 +157,7 @@ void ChatIgnoreList::RemoveIgnore(Packet* packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
Database::Get()->RemoveIgnore(playerId, toRemove->playerId);
|
||||
Database::Get()->RemoveIgnore(static_cast<uint32_t>(playerId), static_cast<uint32_t>(toRemove->playerId));
|
||||
receiver.ignoredPlayers.erase(toRemove, receiver.ignoredPlayers.end());
|
||||
|
||||
CBITSTREAM;
|
||||
|
||||
@@ -35,6 +35,7 @@ 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
|
||||
@@ -160,7 +161,9 @@ 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
|
||||
@@ -315,6 +318,7 @@ void ChatPacketHandler::HandleRemoveFriend(Packet* packet) {
|
||||
}
|
||||
|
||||
// Convert friendID to LWOOBJID
|
||||
GeneralUtils::SetBit(friendID, eObjectBits::PERSISTENT);
|
||||
GeneralUtils::SetBit(friendID, eObjectBits::CHARACTER);
|
||||
|
||||
Database::Get()->RemoveFriend(playerID, friendID);
|
||||
|
||||
@@ -59,7 +59,6 @@ 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:
|
||||
|
||||
@@ -202,11 +201,8 @@ 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;
|
||||
}
|
||||
|
||||
@@ -19,24 +19,23 @@
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "dChatFilter.h"
|
||||
#include "TeamContainer.h"
|
||||
#include "HTTPContext.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = Game::playerContainer;
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleHTTPTeamsRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = TeamContainer::GetTeamContainer();
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleHTTPAnnounceRequest(HTTPReply& reply, const HTTPContext& context) {
|
||||
auto data = GeneralUtils::TryParse<json>(context.body);
|
||||
void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
|
||||
auto data = GeneralUtils::TryParse<json>(body);
|
||||
if (!data) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
@@ -97,21 +96,18 @@ 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
|
||||
});
|
||||
|
||||
|
||||
@@ -477,7 +477,7 @@ TeamData* TeamContainer::CreateLocalTeam(std::vector<LWOOBJID> members) {
|
||||
}
|
||||
}
|
||||
|
||||
newTeam->lootFlag = 0;
|
||||
newTeam->lootFlag = 1;
|
||||
|
||||
TeamStatusUpdate(newTeam);
|
||||
|
||||
|
||||
@@ -374,21 +374,6 @@ 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.
|
||||
|
||||
@@ -54,8 +54,6 @@ elseif (WIN32)
|
||||
zlib
|
||||
URL https://github.com/madler/zlib/archive/refs/tags/v1.2.11.zip
|
||||
URL_HASH MD5=9d6a627693163bbbf3f26403a3a0b0b1
|
||||
GIT_PROGRESS TRUE
|
||||
GIT_SHALLOW 1
|
||||
)
|
||||
|
||||
# Disable warning about no project version.
|
||||
@@ -76,6 +74,5 @@ else ()
|
||||
endif ()
|
||||
|
||||
target_link_libraries(dCommon
|
||||
PUBLIC glm::glm
|
||||
PRIVATE ZLIB::ZLIB bcrypt tinyxml2
|
||||
INTERFACE dDatabase)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// C++
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
@@ -19,9 +19,6 @@
|
||||
#include "dPlatforms.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "DluAssert.h"
|
||||
|
||||
#include <glm/ext/vector_float3.hpp>
|
||||
|
||||
enum eInventoryType : uint32_t;
|
||||
enum class eObjectBits : size_t;
|
||||
@@ -247,7 +244,7 @@ namespace GeneralUtils {
|
||||
* @returns An std::optional containing the desired NiPoint3 if it can be constructed from the string parameters
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] std::optional<T> TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ) {
|
||||
[[nodiscard]] std::optional<NiPoint3> TryParse(const std::string_view strX, const std::string_view strY, const std::string_view strZ) {
|
||||
const auto x = TryParse<float>(strX);
|
||||
if (!x) return std::nullopt;
|
||||
|
||||
@@ -255,7 +252,7 @@ namespace GeneralUtils {
|
||||
if (!y) return std::nullopt;
|
||||
|
||||
const auto z = TryParse<float>(strZ);
|
||||
return z ? std::make_optional<T>(x.value(), y.value(), z.value()) : std::nullopt;
|
||||
return z ? std::make_optional<NiPoint3>(x.value(), y.value(), z.value()) : std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,8 +261,8 @@ namespace GeneralUtils {
|
||||
* @returns An std::optional containing the desired NiPoint3 if it can be constructed from the string parameters
|
||||
*/
|
||||
template <typename T>
|
||||
[[nodiscard]] std::optional<T> TryParse(const std::span<const std::string> str) {
|
||||
return (str.size() == 3) ? TryParse<T>(str[0], str[1], str[2]) : std::nullopt;
|
||||
[[nodiscard]] std::optional<NiPoint3> TryParse(const std::span<const std::string> str) {
|
||||
return (str.size() == 3) ? TryParse<NiPoint3>(str[0], str[1], str[2]) : std::nullopt;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
@@ -306,7 +303,7 @@ namespace GeneralUtils {
|
||||
template<typename Container>
|
||||
inline Container::value_type GetRandomElement(const Container& container) {
|
||||
DluAssert(!container.empty());
|
||||
return container[GenerateRandomNumber<typename Container::size_type>(0, container.size() - 1)];
|
||||
return container[GenerateRandomNumber<typename Container::value_type>(0, container.size() - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,17 +96,3 @@ 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);
|
||||
}
|
||||
|
||||
@@ -32,19 +32,6 @@ 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:
|
||||
|
||||
@@ -5,43 +5,13 @@
|
||||
#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;
|
||||
// Use length-based parsing to avoid expensive string copy
|
||||
const auto err = doc.Parse(data.data(), data.size());
|
||||
const auto err = doc.Parse(data.data());
|
||||
if (err != tinyxml2::XML_SUCCESS) {
|
||||
LOG("Failed to parse xml %s.", StringifiedEnum::ToString(err).data());
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@@ -50,6 +20,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -78,19 +49,16 @@ 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) 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 (split.size() < 12) {
|
||||
LOG("Not enough in the split?");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value();
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value();
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value();
|
||||
if (x < lowest.x) lowest.x = x;
|
||||
if (y < lowest.y) lowest.y = y;
|
||||
if (z < lowest.z) lowest.z = z;
|
||||
|
||||
if (highest.x < x) highest.x = x;
|
||||
@@ -119,19 +87,13 @@ 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 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;
|
||||
auto x = GeneralUtils::TryParse<float>(split[9]).value() - newRootPos.x + curPosition.x;
|
||||
auto y = GeneralUtils::TryParse<float>(split[10]).value() - newRootPos.y + curPosition.y;
|
||||
auto z = GeneralUtils::TryParse<float>(split[11]).value() - newRootPos.z + curPosition.z;
|
||||
std::stringstream stream;
|
||||
for (int i = 0; i < 9; i++) {
|
||||
stream << split[i];
|
||||
@@ -166,345 +128,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "NiPoint3.h"
|
||||
|
||||
@@ -19,7 +18,6 @@ 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);
|
||||
|
||||
@@ -6,14 +6,10 @@
|
||||
\brief Defines a point in space in XYZ coordinates
|
||||
*/
|
||||
|
||||
|
||||
class NiPoint3;
|
||||
class NiQuaternion;
|
||||
typedef NiPoint3 Vector3; //!< The Vector3 class is technically the NiPoint3 class, but typedef'd for clarity in some cases
|
||||
|
||||
#include <glm/ext/vector_float3.hpp>
|
||||
|
||||
#include "NiQuaternion.h"
|
||||
|
||||
//! A custom class the defines a point in space
|
||||
class NiPoint3 {
|
||||
public:
|
||||
@@ -25,12 +21,6 @@ public:
|
||||
//! Initializer
|
||||
constexpr NiPoint3() = default;
|
||||
|
||||
constexpr NiPoint3(const glm::vec3& vec) noexcept
|
||||
: x{ vec.x }
|
||||
, y{ vec.y }
|
||||
, z{ vec.z } {
|
||||
}
|
||||
|
||||
//! Initializer
|
||||
/*!
|
||||
\param x The x coordinate
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#endif
|
||||
|
||||
#include "NiQuaternion.h"
|
||||
#include <glm/ext/quaternion_float.hpp>
|
||||
|
||||
// MARK: Getters / Setters
|
||||
|
||||
|
||||
@@ -3,18 +3,37 @@
|
||||
// C++
|
||||
#include <cmath>
|
||||
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
// MARK: Member Functions
|
||||
|
||||
Vector3 QuatUtils::Euler(const NiQuaternion& quat) {
|
||||
return glm::eulerAngles(quat);
|
||||
Vector3 NiQuaternion::GetEulerAngles() const {
|
||||
Vector3 angles;
|
||||
|
||||
// roll (x-axis rotation)
|
||||
const float sinr_cosp = 2 * (w * x + y * z);
|
||||
const float cosr_cosp = 1 - 2 * (x * x + y * y);
|
||||
angles.x = std::atan2(sinr_cosp, cosr_cosp);
|
||||
|
||||
// pitch (y-axis rotation)
|
||||
const float sinp = 2 * (w * y - z * x);
|
||||
|
||||
if (std::abs(sinp) >= 1) {
|
||||
angles.y = std::copysign(3.14 / 2, sinp); // use 90 degrees if out of range
|
||||
} else {
|
||||
angles.y = std::asin(sinp);
|
||||
}
|
||||
|
||||
// yaw (z-axis rotation)
|
||||
const float siny_cosp = 2 * (w * z + x * y);
|
||||
const float cosy_cosp = 1 - 2 * (y * y + z * z);
|
||||
angles.z = std::atan2(siny_cosp, cosy_cosp);
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
// MARK: Helper Functions
|
||||
|
||||
//! Look from a specific point in space to another point in space (Y-locked)
|
||||
NiQuaternion QuatUtils::LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
NiQuaternion NiQuaternion::LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
//To make sure we don't orient around the X/Z axis:
|
||||
NiPoint3 source = sourcePoint;
|
||||
NiPoint3 dest = destPoint;
|
||||
@@ -32,11 +51,11 @@ NiQuaternion QuatUtils::LookAt(const NiPoint3& sourcePoint, const NiPoint3& dest
|
||||
NiPoint3 vecB = vecA.CrossProduct(posZ);
|
||||
|
||||
if (vecB.DotProduct(forwardVector) < 0) rotAngle = -rotAngle;
|
||||
return glm::angleAxis(rotAngle, glm::vec3{vecA.x, vecA.y, vecA.z});
|
||||
return NiQuaternion::CreateFromAxisAngle(vecA, rotAngle);
|
||||
}
|
||||
|
||||
//! Look from a specific point in space to another point in space
|
||||
NiQuaternion QuatUtils::LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
NiQuaternion NiQuaternion::LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint) {
|
||||
NiPoint3 forwardVector = NiPoint3(destPoint - sourcePoint).Unitize();
|
||||
|
||||
NiPoint3 posZ = NiPoint3Constant::UNIT_Z;
|
||||
@@ -48,26 +67,37 @@ NiQuaternion QuatUtils::LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoin
|
||||
NiPoint3 vecB = vecA.CrossProduct(posZ);
|
||||
|
||||
if (vecB.DotProduct(forwardVector) < 0) rotAngle = -rotAngle;
|
||||
return glm::angleAxis(rotAngle, glm::vec3{vecA.x, vecA.y, vecA.z});
|
||||
return NiQuaternion::CreateFromAxisAngle(vecA, rotAngle);
|
||||
}
|
||||
|
||||
//! Creates a Quaternion from a specific axis and angle relative to that axis
|
||||
NiQuaternion QuatUtils::AxisAngle(const Vector3& axis, float angle) {
|
||||
return glm::angleAxis(angle, glm::vec3(axis.x, axis.y, axis.z));
|
||||
NiQuaternion NiQuaternion::CreateFromAxisAngle(const Vector3& axis, float angle) {
|
||||
float halfAngle = angle * 0.5f;
|
||||
float s = static_cast<float>(sin(halfAngle));
|
||||
|
||||
NiQuaternion q;
|
||||
q.x = axis.GetX() * s;
|
||||
q.y = axis.GetY() * s;
|
||||
q.z = axis.GetZ() * s;
|
||||
q.w = static_cast<float>(cos(halfAngle));
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
NiQuaternion QuatUtils::FromEuler(const NiPoint3& eulerAngles) {
|
||||
return glm::quat(glm::vec3(eulerAngles.x, eulerAngles.y, eulerAngles.z));
|
||||
}
|
||||
NiQuaternion NiQuaternion::FromEulerAngles(const NiPoint3& eulerAngles) {
|
||||
// Abbreviations for the various angular functions
|
||||
float cy = cos(eulerAngles.z * 0.5);
|
||||
float sy = sin(eulerAngles.z * 0.5);
|
||||
float cp = cos(eulerAngles.y * 0.5);
|
||||
float sp = sin(eulerAngles.y * 0.5);
|
||||
float cr = cos(eulerAngles.x * 0.5);
|
||||
float sr = sin(eulerAngles.x * 0.5);
|
||||
|
||||
Vector3 QuatUtils::Forward(const NiQuaternion& quat) {
|
||||
return quat * glm::vec3(0, 0, 1);
|
||||
}
|
||||
NiQuaternion q;
|
||||
q.w = cr * cp * cy + sr * sp * sy;
|
||||
q.x = sr * cp * cy - cr * sp * sy;
|
||||
q.y = cr * sp * cy + sr * cp * sy;
|
||||
q.z = cr * cp * sy - sr * sp * cy;
|
||||
|
||||
Vector3 QuatUtils::Up(const NiQuaternion& quat) {
|
||||
return quat * glm::vec3(0, 1, 0);
|
||||
}
|
||||
|
||||
Vector3 QuatUtils::Right(const NiQuaternion& quat) {
|
||||
return quat * glm::vec3(1, 0, 0);
|
||||
return q;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,158 @@
|
||||
#ifndef NIQUATERNION_H
|
||||
#define NIQUATERNION_H
|
||||
#ifndef __NIQUATERNION_H__
|
||||
#define __NIQUATERNION_H__
|
||||
|
||||
// Custom Classes
|
||||
#include "NiPoint3.h"
|
||||
|
||||
#define GLM_FORCE_QUAT_DATA_WXYZ
|
||||
/*!
|
||||
\file NiQuaternion.hpp
|
||||
\brief Defines a quaternion in space in WXYZ coordinates
|
||||
*/
|
||||
|
||||
#include <glm/ext/quaternion_float.hpp>
|
||||
class NiQuaternion;
|
||||
typedef NiQuaternion Quaternion; //!< A typedef for a shorthand version of NiQuaternion
|
||||
|
||||
using Quaternion = glm::quat;
|
||||
using NiQuaternion = Quaternion;
|
||||
//! A class that defines a rotation in space
|
||||
class NiQuaternion {
|
||||
public:
|
||||
float w{ 1 }; //!< The w coordinate
|
||||
float x{ 0 }; //!< The x coordinate
|
||||
float y{ 0 }; //!< The y coordinate
|
||||
float z{ 0 }; //!< The z coordinate
|
||||
|
||||
namespace QuatUtils {
|
||||
constexpr NiQuaternion IDENTITY = glm::identity<NiQuaternion>();
|
||||
Vector3 Forward(const NiQuaternion& quat);
|
||||
Vector3 Up(const NiQuaternion& quat);
|
||||
Vector3 Right(const NiQuaternion& quat);
|
||||
NiQuaternion LookAt(const NiPoint3& from, const NiPoint3& to);
|
||||
NiQuaternion LookAtUnlocked(const NiPoint3& from, const NiPoint3& to);
|
||||
Vector3 Euler(const NiQuaternion& quat);
|
||||
NiQuaternion AxisAngle(const Vector3& axis, float angle);
|
||||
NiQuaternion FromEuler(const NiPoint3& eulerAngles);
|
||||
constexpr float PI_OVER_180 = glm::pi<float>() / 180.0f;
|
||||
|
||||
//! The initializer
|
||||
constexpr NiQuaternion() = default;
|
||||
|
||||
//! The initializer
|
||||
/*!
|
||||
\param w The w coordinate
|
||||
\param x The x coordinate
|
||||
\param y The y coordinate
|
||||
\param z The z coordinate
|
||||
*/
|
||||
constexpr NiQuaternion(const float w, const float x, const float y, const float z) noexcept
|
||||
: w{ w }
|
||||
, x{ x }
|
||||
, y{ y }
|
||||
, z{ z } {
|
||||
}
|
||||
|
||||
// MARK: Setters / Getters
|
||||
|
||||
//! Gets the W coordinate
|
||||
/*!
|
||||
\return The w coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetW() const noexcept;
|
||||
|
||||
//! Sets the W coordinate
|
||||
/*!
|
||||
\param w The w coordinate
|
||||
*/
|
||||
constexpr void SetW(const float w) noexcept;
|
||||
|
||||
//! Gets the X coordinate
|
||||
/*!
|
||||
\return The x coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetX() const noexcept;
|
||||
|
||||
//! Sets the X coordinate
|
||||
/*!
|
||||
\param x The x coordinate
|
||||
*/
|
||||
constexpr void SetX(const float x) noexcept;
|
||||
|
||||
//! Gets the Y coordinate
|
||||
/*!
|
||||
\return The y coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetY() const noexcept;
|
||||
|
||||
//! Sets the Y coordinate
|
||||
/*!
|
||||
\param y The y coordinate
|
||||
*/
|
||||
constexpr void SetY(const float y) noexcept;
|
||||
|
||||
//! Gets the Z coordinate
|
||||
/*!
|
||||
\return The z coordinate
|
||||
*/
|
||||
[[nodiscard]] constexpr float GetZ() const noexcept;
|
||||
|
||||
//! Sets the Z coordinate
|
||||
/*!
|
||||
\param z The z coordinate
|
||||
*/
|
||||
constexpr void SetZ(const float z) noexcept;
|
||||
|
||||
// MARK: Member Functions
|
||||
|
||||
//! Returns the forward vector from the quaternion
|
||||
/*!
|
||||
\return The forward vector of the quaternion
|
||||
*/
|
||||
[[nodiscard]] constexpr Vector3 GetForwardVector() const noexcept;
|
||||
|
||||
//! Returns the up vector from the quaternion
|
||||
/*!
|
||||
\return The up vector fo the quaternion
|
||||
*/
|
||||
[[nodiscard]] constexpr Vector3 GetUpVector() const noexcept;
|
||||
|
||||
//! Returns the right vector from the quaternion
|
||||
/*!
|
||||
\return The right vector of the quaternion
|
||||
*/
|
||||
[[nodiscard]] constexpr Vector3 GetRightVector() const noexcept;
|
||||
|
||||
[[nodiscard]] Vector3 GetEulerAngles() const;
|
||||
|
||||
// MARK: Operators
|
||||
|
||||
//! Operator to check for equality
|
||||
constexpr bool operator==(const NiQuaternion& rot) const noexcept;
|
||||
|
||||
//! Operator to check for inequality
|
||||
constexpr bool operator!=(const NiQuaternion& rot) const noexcept;
|
||||
|
||||
// MARK: Helper Functions
|
||||
|
||||
//! Look from a specific point in space to another point in space (Y-locked)
|
||||
/*!
|
||||
\param sourcePoint The source location
|
||||
\param destPoint The destination location
|
||||
\return The Quaternion with the rotation towards the destination
|
||||
*/
|
||||
[[nodiscard]] static NiQuaternion LookAt(const NiPoint3& sourcePoint, const NiPoint3& destPoint);
|
||||
|
||||
//! Look from a specific point in space to another point in space
|
||||
/*!
|
||||
\param sourcePoint The source location
|
||||
\param destPoint The destination location
|
||||
\return The Quaternion with the rotation towards the destination
|
||||
*/
|
||||
[[nodiscard]] static NiQuaternion LookAtUnlocked(const NiPoint3& sourcePoint, const NiPoint3& destPoint);
|
||||
|
||||
//! Creates a Quaternion from a specific axis and angle relative to that axis
|
||||
/*!
|
||||
\param axis The axis that is used
|
||||
\param angle The angle relative to this axis
|
||||
\return A quaternion created from the axis and angle
|
||||
*/
|
||||
[[nodiscard]] static NiQuaternion CreateFromAxisAngle(const Vector3& axis, float angle);
|
||||
|
||||
[[nodiscard]] static NiQuaternion FromEulerAngles(const NiPoint3& eulerAngles);
|
||||
};
|
||||
|
||||
#endif // !NIQUATERNION_H
|
||||
// Static Variables
|
||||
namespace NiQuaternionConstant {
|
||||
constexpr NiQuaternion IDENTITY(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Include constexpr and inline function definitions in a seperate file for readability
|
||||
#include "NiQuaternion.inl"
|
||||
|
||||
#endif // !__NIQUATERNION_H__
|
||||
|
||||
75
dCommon/NiQuaternion.inl
Normal file
75
dCommon/NiQuaternion.inl
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
#ifndef __NIQUATERNION_H__
|
||||
#error "This should only be included inline in NiQuaternion.h: Do not include directly!"
|
||||
#endif
|
||||
|
||||
// MARK: Setters / Getters
|
||||
|
||||
//! Gets the W coordinate
|
||||
constexpr float NiQuaternion::GetW() const noexcept {
|
||||
return this->w;
|
||||
}
|
||||
|
||||
//! Sets the W coordinate
|
||||
constexpr void NiQuaternion::SetW(const float w) noexcept {
|
||||
this->w = w;
|
||||
}
|
||||
|
||||
//! Gets the X coordinate
|
||||
constexpr float NiQuaternion::GetX() const noexcept {
|
||||
return this->x;
|
||||
}
|
||||
|
||||
//! Sets the X coordinate
|
||||
constexpr void NiQuaternion::SetX(const float x) noexcept {
|
||||
this->x = x;
|
||||
}
|
||||
|
||||
//! Gets the Y coordinate
|
||||
constexpr float NiQuaternion::GetY() const noexcept {
|
||||
return this->y;
|
||||
}
|
||||
|
||||
//! Sets the Y coordinate
|
||||
constexpr void NiQuaternion::SetY(const float y) noexcept {
|
||||
this->y = y;
|
||||
}
|
||||
|
||||
//! Gets the Z coordinate
|
||||
constexpr float NiQuaternion::GetZ() const noexcept {
|
||||
return this->z;
|
||||
}
|
||||
|
||||
//! Sets the Z coordinate
|
||||
constexpr void NiQuaternion::SetZ(const float z) noexcept {
|
||||
this->z = z;
|
||||
}
|
||||
|
||||
// MARK: Member Functions
|
||||
|
||||
//! Returns the forward vector from the quaternion
|
||||
constexpr Vector3 NiQuaternion::GetForwardVector() const noexcept {
|
||||
return Vector3(2 * (x * z + w * y), 2 * (y * z - w * x), 1 - 2 * (x * x + y * y));
|
||||
}
|
||||
|
||||
//! Returns the up vector from the quaternion
|
||||
constexpr Vector3 NiQuaternion::GetUpVector() const noexcept {
|
||||
return Vector3(2 * (x * y - w * z), 1 - 2 * (x * x + z * z), 2 * (y * z + w * x));
|
||||
}
|
||||
|
||||
//! Returns the right vector from the quaternion
|
||||
constexpr Vector3 NiQuaternion::GetRightVector() const noexcept {
|
||||
return Vector3(1 - 2 * (y * y + z * z), 2 * (x * y + w * z), 2 * (x * z - w * y));
|
||||
}
|
||||
|
||||
// MARK: Operators
|
||||
|
||||
//! Operator to check for equality
|
||||
constexpr bool NiQuaternion::operator==(const NiQuaternion& rot) const noexcept {
|
||||
return rot.x == this->x && rot.y == this->y && rot.z == this->z && rot.w == this->w;
|
||||
}
|
||||
|
||||
//! Operator to check for inequality
|
||||
constexpr bool NiQuaternion::operator!=(const NiQuaternion& rot) const noexcept {
|
||||
return !(*this == rot);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ struct LocalSpaceInfo {
|
||||
|
||||
struct PositionUpdate {
|
||||
NiPoint3 position = NiPoint3Constant::ZERO;
|
||||
NiQuaternion rotation = QuatUtils::IDENTITY;
|
||||
NiQuaternion rotation = NiQuaternionConstant::IDENTITY;
|
||||
bool onGround = false;
|
||||
bool onRail = false;
|
||||
NiPoint3 velocity = NiPoint3Constant::ZERO;
|
||||
|
||||
@@ -81,9 +81,6 @@ public:
|
||||
[[nodiscard]]
|
||||
AssetStream GetFile(const char* name) const;
|
||||
|
||||
[[nodiscard]]
|
||||
AssetStream GetFile(const std::string& name) const { return GetFile(name.c_str()); };
|
||||
|
||||
private:
|
||||
void LoadPackIndex();
|
||||
|
||||
|
||||
@@ -47,8 +47,6 @@ 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) {
|
||||
@@ -60,18 +58,6 @@ const std::string& dConfig::GetValue(std::string key) {
|
||||
return this->m_ConfigValues[key];
|
||||
}
|
||||
|
||||
void dConfig::AddConfigHandler(std::function<void()> handler) {
|
||||
m_ConfigHandlers.push_back(handler);
|
||||
}
|
||||
|
||||
void dConfig::LogSettings() const {
|
||||
LOG("Configuration settings:");
|
||||
for (const auto& [key, value] : m_ConfigValues) {
|
||||
const auto& valueLog = key.find("password") != std::string::npos ? "<HIDDEN>" : value;
|
||||
LOG(" %s = %s", key.c_str(), valueLog.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void dConfig::ProcessLine(const std::string& line) {
|
||||
auto splitLoc = line.find('=');
|
||||
auto key = line.substr(0, splitLoc);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
@@ -31,15 +29,10 @@ public:
|
||||
* Reloads the config file to reset values
|
||||
*/
|
||||
void ReloadConfig();
|
||||
|
||||
// Adds a function to be called when the config is (re)loaded
|
||||
void AddConfigHandler(std::function<void()> handler);
|
||||
void LogSettings() const;
|
||||
|
||||
private:
|
||||
void ProcessLine(const std::string& line);
|
||||
|
||||
private:
|
||||
std::map<std::string, std::string> m_ConfigValues;
|
||||
std::vector<std::function<void()>> m_ConfigHandlers;
|
||||
std::string m_ConfigFilePath;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
namespace MessageType {
|
||||
enum class Master : uint32_t {
|
||||
REQUEST_ZONE_TRANSFER = 1,
|
||||
REQUEST_PERSISTENT_ID = 1,
|
||||
REQUEST_PERSISTENT_ID_RESPONSE,
|
||||
REQUEST_ZONE_TRANSFER,
|
||||
REQUEST_ZONE_TRANSFER_RESPONSE,
|
||||
SERVER_INFO,
|
||||
REQUEST_SESSION_KEY,
|
||||
@@ -27,8 +29,6 @@ namespace MessageType {
|
||||
AFFIRM_TRANSFER_REQUEST,
|
||||
AFFIRM_TRANSFER_RESPONSE,
|
||||
|
||||
NEW_SESSION_ALERT,
|
||||
|
||||
REQUEST_SERVER_LIST
|
||||
NEW_SESSION_ALERT
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ enum class ServiceType : uint16_t {
|
||||
COMMON = 0,
|
||||
AUTH,
|
||||
CHAT,
|
||||
DASHBOARD,
|
||||
WORLD,
|
||||
WORLD = 4,
|
||||
CLIENT,
|
||||
MASTER,
|
||||
UNKNOWN
|
||||
|
||||
@@ -18,9 +18,7 @@ enum class eCharacterVersion : uint32_t {
|
||||
SPEED_BASE,
|
||||
// Fixes nexus force explorer missions
|
||||
NJ_JAYMISSIONS,
|
||||
NEXUS_FORCE_EXPLORER, // Fixes pet ids in player inventories
|
||||
PET_IDS, // Fixes pet ids in player inventories
|
||||
UP_TO_DATE, // will become INVENTORY_PERSISTENT_IDS
|
||||
UP_TO_DATE, // will become NEXUS_FORCE_EXPLORER
|
||||
};
|
||||
|
||||
#endif //!__ECHARACTERVERSION__H__
|
||||
|
||||
@@ -50,10 +50,7 @@ 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,
|
||||
|
||||
// The mission is failed (don't know where this is used)
|
||||
FAILED = 16,
|
||||
COMPLETE_READY_TO_COMPLETE = 12
|
||||
};
|
||||
|
||||
#endif //!__MISSIONSTATE__H__
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#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__
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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"
|
||||
)
|
||||
@@ -1,203 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#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__
|
||||
@@ -1,28 +0,0 @@
|
||||
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)
|
||||
@@ -1,144 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#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);
|
||||
};
|
||||
@@ -1,186 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#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
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#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__
|
||||
@@ -1,443 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterAPIRoutes();
|
||||
@@ -1,102 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Web.h"
|
||||
|
||||
/**
|
||||
* Register authentication routes
|
||||
* /api/auth/login - POST login endpoint
|
||||
* /api/auth/verify - POST verify token endpoint
|
||||
*/
|
||||
void RegisterAuthRoutes();
|
||||
@@ -1,30 +0,0 @@
|
||||
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)
|
||||
@@ -1,291 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
class HTTPContext;
|
||||
|
||||
void RegisterDashboardRoutes();
|
||||
@@ -1,52 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#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");
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterStaticRoutes();
|
||||
@@ -1,35 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
void RegisterWSRoutes();
|
||||
void BroadcastDashboardUpdate();
|
||||
@@ -1,495 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
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();
|
||||
@@ -1,99 +0,0 @@
|
||||
// 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();
|
||||
});
|
||||
@@ -1,137 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,133 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,34 +0,0 @@
|
||||
<!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 © 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>
|
||||
@@ -1,97 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,90 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,30 +0,0 @@
|
||||
{# 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>
|
||||
@@ -1,35 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,53 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,95 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,92 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,29 +0,0 @@
|
||||
<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>
|
||||
@@ -1,21 +0,0 @@
|
||||
<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>
|
||||
@@ -1,37 +0,0 @@
|
||||
<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>
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "CDActivitiesTable.h"
|
||||
|
||||
|
||||
void CDActivitiesTable::LoadValuesFromDatabase() {
|
||||
// First, get the size of the table
|
||||
uint32_t size = 0;
|
||||
@@ -55,13 +56,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
// Custom Classes
|
||||
#include "CDTable.h"
|
||||
#include <optional>
|
||||
|
||||
struct CDActivities {
|
||||
uint32_t ActivityID;
|
||||
@@ -32,5 +31,4 @@ 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);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ target_include_directories(dDatabaseCDClient PUBLIC "."
|
||||
"${PROJECT_SOURCE_DIR}/dCommon"
|
||||
"${PROJECT_SOURCE_DIR}/dCommon/dEnums"
|
||||
)
|
||||
target_link_libraries(dDatabaseCDClient PRIVATE sqlite3 glm::glm)
|
||||
target_link_libraries(dDatabaseCDClient PRIVATE sqlite3)
|
||||
|
||||
if (${CDCLIENT_CACHE_ALL})
|
||||
add_compile_definitions(dDatabaseCDClient PRIVATE CDCLIENT_CACHE_ALL=${CDCLIENT_CACHE_ALL})
|
||||
|
||||
@@ -10,5 +10,4 @@ add_dependencies(dDatabase conncpp_dylib)
|
||||
|
||||
target_include_directories(dDatabase PUBLIC ".")
|
||||
target_link_libraries(dDatabase
|
||||
PUBLIC dDatabaseCDClient dDatabaseGame
|
||||
PRIVATE glm::glm)
|
||||
PUBLIC dDatabaseCDClient dDatabaseGame)
|
||||
|
||||
@@ -29,7 +29,7 @@ target_include_directories(dDatabaseGame PUBLIC "."
|
||||
|
||||
target_link_libraries(dDatabaseGame
|
||||
INTERFACE dCommon
|
||||
PRIVATE sqlite3 MariaDB::ConnCpp glm::glm)
|
||||
PRIVATE sqlite3 MariaDB::ConnCpp)
|
||||
|
||||
# Glob together all headers that need to be precompiled
|
||||
file(
|
||||
|
||||
@@ -48,7 +48,7 @@ public:
|
||||
virtual void Commit() = 0;
|
||||
virtual bool GetAutoCommit() = 0;
|
||||
virtual void SetAutoCommit(bool value) = 0;
|
||||
virtual void DeleteCharacter(const LWOOBJID characterId) = 0;
|
||||
virtual void DeleteCharacter(const uint32_t characterId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__GAMEDATABASE__H__
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "json.hpp"
|
||||
|
||||
enum class eGameMasterLevel : uint8_t;
|
||||
|
||||
@@ -15,7 +14,6 @@ public:
|
||||
std::string bcryptPassword;
|
||||
uint32_t id{};
|
||||
uint32_t playKeyId{};
|
||||
uint64_t muteExpire{};
|
||||
bool banned{};
|
||||
bool locked{};
|
||||
eGameMasterLevel maxGmLevel{};
|
||||
@@ -40,30 +38,6 @@ 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__
|
||||
|
||||
@@ -14,7 +14,7 @@ enum class eActivityType : uint32_t {
|
||||
class IActivityLog {
|
||||
public:
|
||||
// Update the activity log for the given account.
|
||||
virtual void UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) = 0;
|
||||
virtual void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IACTIVITYLOG__H__
|
||||
|
||||
@@ -9,7 +9,7 @@ class IBehaviors {
|
||||
public:
|
||||
struct Info {
|
||||
LWOOBJID behaviorId{};
|
||||
LWOOBJID characterId{};
|
||||
uint32_t characterId{};
|
||||
std::string behaviorInfo;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#define __IBUGREPORTS__H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class IBugReports {
|
||||
@@ -12,14 +11,10 @@ public:
|
||||
std::string clientVersion;
|
||||
std::string otherPlayer;
|
||||
std::string selection;
|
||||
LWOOBJID characterId{};
|
||||
uint32_t 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__
|
||||
|
||||
@@ -14,7 +14,7 @@ public:
|
||||
struct Info {
|
||||
std::string name;
|
||||
std::string pendingName;
|
||||
LWOOBJID id{};
|
||||
uint32_t id{};
|
||||
uint32_t accountId{};
|
||||
bool needsRename{};
|
||||
LWOCLONEID cloneId{};
|
||||
@@ -25,28 +25,25 @@ public:
|
||||
virtual std::vector<std::string> GetApprovedCharacterNames() = 0;
|
||||
|
||||
// Get the character info for the given character id.
|
||||
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const LWOOBJID charId) = 0;
|
||||
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const uint32_t 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<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
|
||||
|
||||
// Get the total number of characters in the database.
|
||||
virtual uint32_t GetCharacterCount() = 0;
|
||||
virtual std::vector<uint32_t> GetAccountCharacterIds(const uint32_t accountId) = 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 LWOOBJID characterId, const std::string_view name) = 0;
|
||||
virtual void SetCharacterName(const uint32_t characterId, const std::string_view name) = 0;
|
||||
|
||||
// Set the pending name of the given character.
|
||||
virtual void SetPendingCharacterName(const LWOOBJID characterId, const std::string_view name) = 0;
|
||||
virtual void SetPendingCharacterName(const uint32_t characterId, const std::string_view name) = 0;
|
||||
|
||||
// Updates the given character ids last login to be right now.
|
||||
virtual void UpdateLastLoggedInCharacter(const LWOOBJID characterId) = 0;
|
||||
virtual void UpdateLastLoggedInCharacter(const uint32_t characterId) = 0;
|
||||
|
||||
virtual bool IsNameInUse(const std::string_view name) = 0;
|
||||
};
|
||||
|
||||
@@ -8,17 +8,13 @@
|
||||
class ICharXml {
|
||||
public:
|
||||
// Get the character xml for the given character id.
|
||||
virtual std::string GetCharacterXml(const LWOOBJID charId) = 0;
|
||||
virtual std::string GetCharacterXml(const uint32_t charId) = 0;
|
||||
|
||||
// Update the character xml for the given character id.
|
||||
virtual void UpdateCharacterXml(const LWOOBJID charId, const std::string_view lxfml) = 0;
|
||||
virtual void UpdateCharacterXml(const uint32_t charId, const std::string_view lxfml) = 0;
|
||||
|
||||
// Insert the character xml for the given character id.
|
||||
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;
|
||||
virtual void InsertCharacterXml(const uint32_t characterId, const std::string_view lxfml) = 0;
|
||||
};
|
||||
|
||||
#endif //!__ICHARXML__H__
|
||||
|
||||
@@ -8,7 +8,7 @@ class ICommandLog {
|
||||
public:
|
||||
|
||||
// Insert a new slash command log entry.
|
||||
virtual void InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) = 0;
|
||||
virtual void InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) = 0;
|
||||
};
|
||||
|
||||
#endif //!__ICOMMANDLOG__H__
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
class IFriends {
|
||||
public:
|
||||
struct BestFriendStatus {
|
||||
LWOOBJID playerCharacterId{};
|
||||
LWOOBJID friendCharacterId{};
|
||||
uint32_t playerCharacterId{};
|
||||
uint32_t friendCharacterId{};
|
||||
uint32_t bestFriendStatus{};
|
||||
};
|
||||
|
||||
// Get the friends list for the given character id.
|
||||
virtual std::vector<FriendData> GetFriendsList(const LWOOBJID charId) = 0;
|
||||
virtual std::vector<FriendData> GetFriendsList(const uint32_t charId) = 0;
|
||||
|
||||
// Get the best friend status for the given player and friend character ids.
|
||||
virtual std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
|
||||
virtual std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
|
||||
|
||||
// Set the best friend status for the given player and friend character ids.
|
||||
virtual void SetBestFriendStatus(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId, const uint32_t bestFriendStatus) = 0;
|
||||
virtual void SetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId, const uint32_t bestFriendStatus) = 0;
|
||||
|
||||
// Add a friend to the given character id.
|
||||
virtual void AddFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
|
||||
virtual void AddFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
|
||||
|
||||
// Remove a friend from the given character id.
|
||||
virtual void RemoveFriend(const LWOOBJID playerCharacterId, const LWOOBJID friendCharacterId) = 0;
|
||||
virtual void RemoveFriend(const uint32_t playerCharacterId, const uint32_t friendCharacterId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IFRIENDS__H__
|
||||
|
||||
@@ -9,12 +9,12 @@ class IIgnoreList {
|
||||
public:
|
||||
struct Info {
|
||||
std::string name;
|
||||
LWOOBJID id;
|
||||
uint32_t id;
|
||||
};
|
||||
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
#endif //!__IIGNORELIST__H__
|
||||
|
||||
@@ -5,13 +5,12 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "dCommonVars.h"
|
||||
|
||||
class ILeaderboard {
|
||||
public:
|
||||
|
||||
struct Entry {
|
||||
LWOOBJID charId{};
|
||||
uint32_t charId{};
|
||||
uint32_t lastPlayedTimestamp{};
|
||||
float primaryScore{};
|
||||
float secondaryScore{};
|
||||
@@ -37,12 +36,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 LWOOBJID playerId, const uint32_t gameId) = 0;
|
||||
virtual std::optional<Score> GetPlayerScore(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;
|
||||
virtual void SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
|
||||
virtual void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) = 0;
|
||||
virtual void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) = 0;
|
||||
virtual void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__ILEADERBOARD__H__
|
||||
|
||||
@@ -16,13 +16,16 @@ public:
|
||||
virtual void InsertNewMail(const MailInfo& mail) = 0;
|
||||
|
||||
// Get the mail for the given character id.
|
||||
virtual std::vector<MailInfo> GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) = 0;
|
||||
virtual std::vector<MailInfo> GetMailForPlayer(const uint32_t 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 LWOOBJID characterId) = 0;
|
||||
virtual uint32_t GetUnreadMailCount(const uint32_t characterId) = 0;
|
||||
|
||||
// Get the number of mail for a given character id.
|
||||
virtual uint32_t GetMailCount(const uint32_t characterId) = 0;
|
||||
|
||||
// Mark the given mail as read.
|
||||
virtual void MarkMailRead(const uint64_t mailId) = 0;
|
||||
|
||||
@@ -6,19 +6,14 @@
|
||||
|
||||
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<uint64_t> GetCurrentPersistentId() = 0;
|
||||
virtual std::optional<uint32_t> GetCurrentPersistentId() = 0;
|
||||
|
||||
// Insert the default persistent id.
|
||||
virtual void InsertDefaultPersistentId() = 0;
|
||||
|
||||
virtual Range GetPersistentIdRange() = 0;
|
||||
// Update the persistent id.
|
||||
virtual void UpdatePersistentId(const uint32_t newId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IOBJECTIDTRACKER__H__
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class IPlayKeys {
|
||||
public:
|
||||
@@ -12,10 +10,6 @@ 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__
|
||||
|
||||
@@ -13,7 +13,7 @@ public:
|
||||
std::string description;
|
||||
std::string rejectionReason;
|
||||
LWOOBJID id{};
|
||||
LWOOBJID ownerId{};
|
||||
uint32_t ownerId{};
|
||||
LWOCLONEID cloneId{};
|
||||
int32_t privacyOption{};
|
||||
uint32_t modApproved{};
|
||||
@@ -27,7 +27,7 @@ public:
|
||||
uint32_t mapId{};
|
||||
std::string searchString;
|
||||
ePropertySortType sortChoice{};
|
||||
LWOOBJID playerId{};
|
||||
uint32_t playerId{};
|
||||
uint32_t numResults{};
|
||||
uint32_t startIndex{};
|
||||
uint32_t playerSort{};
|
||||
@@ -39,9 +39,6 @@ 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;
|
||||
|
||||
@@ -64,9 +61,5 @@ 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__
|
||||
|
||||
@@ -13,19 +13,19 @@ public:
|
||||
}
|
||||
|
||||
NiPoint3 position;
|
||||
NiQuaternion rotation = QuatUtils::IDENTITY;
|
||||
NiQuaternion rotation;
|
||||
LWOOBJID id{};
|
||||
LOT lot{};
|
||||
LWOOBJID ugcId{};
|
||||
uint32_t ugcId{};
|
||||
std::array<LWOOBJID, 5> behaviors{};
|
||||
};
|
||||
|
||||
// Inserts a new UGC model into the database.
|
||||
virtual void InsertNewUgcModel(
|
||||
std::stringstream& sd0Data,
|
||||
const uint64_t blueprintId,
|
||||
const uint32_t blueprintId,
|
||||
const uint32_t accountId,
|
||||
const LWOOBJID characterId) = 0;
|
||||
const uint32_t 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 std::optional<Model> GetModel(const LWOOBJID modelID) = 0;
|
||||
virtual Model GetModel(const LWOOBJID modelID) = 0;
|
||||
};
|
||||
#endif //!__IPROPERTIESCONTENTS__H__
|
||||
|
||||
@@ -29,7 +29,5 @@ 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__
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
class IUgcModularBuild {
|
||||
public:
|
||||
virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) = 0;
|
||||
virtual void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) = 0;
|
||||
virtual void DeleteUgcBuild(const LWOOBJID bigId) = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ void MySQLDatabase::SetAutoCommit(bool value) {
|
||||
con->setAutoCommit(value);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteCharacter(const LWOOBJID characterId) {
|
||||
void MySQLDatabase::DeleteCharacter(const uint32_t 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);
|
||||
|
||||
@@ -40,32 +40,31 @@ public:
|
||||
|
||||
std::vector<std::string> GetApprovedCharacterNames() override;
|
||||
|
||||
std::vector<FriendData> GetFriendsList(LWOOBJID charID) override;
|
||||
std::vector<FriendData> GetFriendsList(uint32_t charID) 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;
|
||||
std::optional<IFriends::BestFriendStatus> GetBestFriendStatus(const uint32_t playerCharacterId, const uint32_t friendCharacterId) override;
|
||||
void SetBestFriendStatus(const uint32_t playerAccountId, const uint32_t friendAccountId, const uint32_t bestFriendStatus) override;
|
||||
void AddFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override;
|
||||
void RemoveFriend(const uint32_t playerAccountId, const uint32_t friendAccountId) override;
|
||||
void UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) override;
|
||||
void DeleteUgcModelData(const LWOOBJID& modelId) override;
|
||||
void UpdateUgcModelData(const LWOOBJID& modelId, std::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 LWOOBJID charId) override;
|
||||
std::optional<ICharInfo::Info> GetCharacterInfo(const uint32_t charId) override;
|
||||
std::optional<ICharInfo::Info> GetCharacterInfo(const std::string_view charId) override;
|
||||
std::string GetCharacterXml(const LWOOBJID accountId) override;
|
||||
void UpdateCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) override;
|
||||
std::string GetCharacterXml(const uint32_t accountId) override;
|
||||
void UpdateCharacterXml(const uint32_t characterId, const std::string_view lxfml) override;
|
||||
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
|
||||
void InsertNewCharacter(const ICharInfo::Info info) override;
|
||||
void InsertCharacterXml(const 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 InsertCharacterXml(const uint32_t accountId, const std::string_view lxfml) override;
|
||||
std::vector<uint32_t> GetAccountCharacterIds(uint32_t accountId) override;
|
||||
void DeleteCharacter(const uint32_t characterId) override;
|
||||
void SetCharacterName(const uint32_t characterId, const std::string_view name) override;
|
||||
void SetPendingCharacterName(const uint32_t characterId, const std::string_view name) override;
|
||||
void UpdateLastLoggedInCharacter(const uint32_t characterId) override;
|
||||
void SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) override;
|
||||
std::optional<IPetNames::Info> GetPetNameInfo(const LWOOBJID& petId) override;
|
||||
std::optional<IProperty::Info> GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) override;
|
||||
@@ -80,36 +79,35 @@ 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 uint64_t blueprintId,
|
||||
const uint32_t blueprintId,
|
||||
const uint32_t accountId,
|
||||
const LWOOBJID characterId) override;
|
||||
std::vector<MailInfo> GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) override;
|
||||
const uint32_t characterId) override;
|
||||
std::vector<MailInfo> GetMailForPlayer(const uint32_t characterId, const uint32_t numberOfMail) override;
|
||||
std::optional<MailInfo> GetMail(const uint64_t mailId) override;
|
||||
uint32_t GetUnreadMailCount(const LWOOBJID characterId) override;
|
||||
uint32_t GetUnreadMailCount(const uint32_t characterId) override;
|
||||
uint32_t GetMailCount(const uint32_t characterId) override;
|
||||
void MarkMailRead(const uint64_t mailId) override;
|
||||
void DeleteMail(const uint64_t mailId) override;
|
||||
void ClaimMailItem(const uint64_t mailId) override;
|
||||
void InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) override;
|
||||
void InsertSlashCommandUsage(const uint32_t characterId, const std::string_view command) override;
|
||||
void UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) override;
|
||||
void UpdateAccountBan(const uint32_t accountId, const bool banned) override;
|
||||
void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override;
|
||||
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override;
|
||||
void SetMasterInfo(const IServers::MasterInfo& info) override;
|
||||
std::optional<uint64_t> GetCurrentPersistentId() override;
|
||||
IObjectIdTracker::Range GetPersistentIdRange() override;
|
||||
std::optional<uint32_t> GetCurrentPersistentId() override;
|
||||
void InsertDefaultPersistentId() override;
|
||||
void UpdatePersistentId(const uint32_t id) override;
|
||||
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
|
||||
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
|
||||
std::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 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 AddIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) override;
|
||||
void RemoveIgnore(const uint32_t playerId, const uint32_t ignoredPlayerId) override;
|
||||
std::vector<IIgnoreList::Info> GetIgnoreList(const uint32_t playerId) override;
|
||||
void InsertRewardCode(const uint32_t account_id, const uint32_t reward_code) override;
|
||||
std::vector<uint32_t> GetRewardCodesByAccountID(const uint32_t account_id) override;
|
||||
void AddBehavior(const IBehaviors::Info& info) override;
|
||||
@@ -121,27 +119,16 @@ 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 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 SaveScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override;
|
||||
void UpdateScore(const uint32_t playerId, const uint32_t gameId, const Score& score) override;
|
||||
std::optional<ILeaderboard::Score> GetPlayerScore(const uint32_t playerId, const uint32_t gameId) override;
|
||||
void IncrementNumWins(const uint32_t playerId, const uint32_t gameId) override;
|
||||
void IncrementTimesPlayed(const uint32_t playerId, const uint32_t gameId) override;
|
||||
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<uint32_t> characterId) override;
|
||||
void DeleteUgcBuild(const LWOOBJID bigId) override;
|
||||
uint32_t GetAccountCount() override;
|
||||
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;
|
||||
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;
|
||||
IPropertyContents::Model GetModel(const LWOOBJID modelID) override;
|
||||
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
|
||||
private:
|
||||
|
||||
@@ -182,91 +169,91 @@ private:
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::string_view param) {
|
||||
LOG_DEBUG("%s", param.data());
|
||||
// LOG("%s", param.data());
|
||||
stmt->setString(index, param.data());
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const char* param) {
|
||||
LOG_DEBUG("%s", param);
|
||||
// LOG("%s", param);
|
||||
stmt->setString(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::string param) {
|
||||
LOG_DEBUG("%s", param.c_str());
|
||||
// LOG("%s", param.c_str());
|
||||
stmt->setString(index, param.c_str());
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int8_t param) {
|
||||
LOG_DEBUG("%u", param);
|
||||
// LOG("%u", param);
|
||||
stmt->setByte(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint8_t param) {
|
||||
LOG_DEBUG("%d", param);
|
||||
// LOG("%d", param);
|
||||
stmt->setByte(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int16_t param) {
|
||||
LOG_DEBUG("%u", param);
|
||||
// LOG("%u", param);
|
||||
stmt->setShort(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint16_t param) {
|
||||
LOG_DEBUG("%d", param);
|
||||
// LOG("%d", param);
|
||||
stmt->setShort(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint32_t param) {
|
||||
LOG_DEBUG("%u", param);
|
||||
// LOG("%u", param);
|
||||
stmt->setUInt(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int32_t param) {
|
||||
LOG_DEBUG("%d", param);
|
||||
// LOG("%d", param);
|
||||
stmt->setInt(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const int64_t param) {
|
||||
LOG_DEBUG("%llu", param);
|
||||
// LOG("%llu", param);
|
||||
stmt->setInt64(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const uint64_t param) {
|
||||
LOG_DEBUG("%llu", param);
|
||||
// LOG("%llu", param);
|
||||
stmt->setUInt64(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const float param) {
|
||||
LOG_DEBUG("%f", param);
|
||||
// LOG("%f", param);
|
||||
stmt->setFloat(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const double param) {
|
||||
LOG_DEBUG("%f", param);
|
||||
// LOG("%f", param);
|
||||
stmt->setDouble(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const bool param) {
|
||||
LOG_DEBUG("%s", param ? "true" : "false");
|
||||
// LOG("%d", param);
|
||||
stmt->setBoolean(index, param);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::istream* param) {
|
||||
LOG_DEBUG("Blob");
|
||||
// LOG("Blob");
|
||||
// This is the one time you will ever see me use const_cast.
|
||||
stmt->setBlob(index, const_cast<std::istream*>(param));
|
||||
}
|
||||
@@ -274,21 +261,10 @@ 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_DEBUG("%d", param.value());
|
||||
// LOG("%d", param.value());
|
||||
stmt->setInt(index, param.value());
|
||||
} else {
|
||||
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");
|
||||
// LOG("Null");
|
||||
stmt->setNull(index, sql::DataType::SQLNULL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#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);
|
||||
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level FROM accounts WHERE name = ? LIMIT 1;", username);
|
||||
|
||||
if (!result->next()) {
|
||||
return std::nullopt;
|
||||
@@ -17,7 +16,6 @@ std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_v
|
||||
toReturn.banned = result->getBoolean("banned");
|
||||
toReturn.locked = result->getBoolean("locked");
|
||||
toReturn.playKeyId = result->getUInt("play_key_id");
|
||||
toReturn.muteExpire = result->getUInt64("mute_expire");
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
@@ -46,142 +44,3 @@ 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"}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
void MySQLDatabase::UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) {
|
||||
void MySQLDatabase::UpdateActivityLog(const uint32_t characterId, const eActivityType activityType, const LWOMAPID mapId) {
|
||||
ExecuteInsert("INSERT INTO activity_log (character_id, activity, time, map_id) VALUES (?, ?, ?, ?);",
|
||||
characterId, static_cast<uint32_t>(activityType), static_cast<uint32_t>(time(NULL)), mapId);
|
||||
}
|
||||
|
||||
@@ -4,77 +4,3 @@ 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();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user