mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2025-12-17 12:04:27 -06:00
Compare commits
18 Commits
EmosewaMC-
...
waypointpa
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eeacbf746 | |||
|
|
2f315d9288 | ||
|
|
6ae1c7a376 | ||
|
|
c19ee04c8a | ||
|
|
2858345269 | ||
|
|
37e14979a4 | ||
|
|
b509fd4f10 | ||
|
|
820c0f0083 | ||
|
|
68eb20966f | ||
|
|
92155a3cb4 | ||
|
|
437362cce6 | ||
|
|
34665f6f5c | ||
|
|
32487dcd5f | ||
|
|
891b176b4f | ||
|
|
e42df5b02e | ||
| 61921cfb62 | |||
|
|
91f6b2bf81 | ||
|
|
01917841cb |
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -16,7 +16,10 @@ body:
|
||||
I have validated that this issue is not a syntax error of either MySQL or SQLite.
|
||||
required: true
|
||||
- label: >
|
||||
I have pulled the latest version of the main branch of DarkflameServer and have confirmed that the issue exists there.
|
||||
I have downloaded/pulled the latest version of the main branch of DarkflameServer and have confirmed that the issue exists there.
|
||||
required: true
|
||||
- label: >
|
||||
I have verified that my boot.cfg is configured as per the [README](https://github.com/DarkflameUniverse/DarkflameServer?tab=readme-ov-file#allowing-a-user-to-connect-to-your-server).
|
||||
required: true
|
||||
- type: input
|
||||
id: server-version
|
||||
|
||||
@@ -235,6 +235,8 @@ include_directories(
|
||||
|
||||
"dNet"
|
||||
|
||||
"dWeb"
|
||||
|
||||
"tests"
|
||||
"tests/dCommonTests"
|
||||
"tests/dGameTests"
|
||||
@@ -301,6 +303,7 @@ add_subdirectory(dZoneManager)
|
||||
add_subdirectory(dNavigation)
|
||||
add_subdirectory(dPhysics)
|
||||
add_subdirectory(dServer)
|
||||
add_subdirectory(dWeb)
|
||||
|
||||
# Create a list of common libraries shared between all binaries
|
||||
set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum")
|
||||
|
||||
@@ -324,13 +324,15 @@ While a character has a gmlevel of anything but `0`, some gameplay behavior will
|
||||
Some changes to the client `boot.cfg` file are needed to play on your server.
|
||||
|
||||
## Allowing a user to connect to your server
|
||||
**ALL OF THESE CHANGES ARE REQUIRED. PLEASE FULLY READ THIS SECTION**
|
||||
|
||||
To connect to a server follow these steps:
|
||||
* In the client directory, locate `boot.cfg`
|
||||
* Open it in a text editor and locate where it says `AUTHSERVERIP=0:`
|
||||
* Replace the contents after to `:` and the following `,` with what you configured as the server's public facing IP. For example `AUTHSERVERIP=0:localhost` for locally hosted servers
|
||||
* Next locate the line `UGCUSE3DSERVICES=7:`
|
||||
* Open `boot.cfg` in a text editor and locate the line `UGCUSE3DSERVICES=7:`
|
||||
* Ensure the number after the 7 is a `0`
|
||||
* Alternatively, remove the line with `UGCUSE3DSERVICES` altogether
|
||||
* Next locate where it says `AUTHSERVERIP=0:`
|
||||
* Replace the contents after to `:` and the following `,` with what you configured as the server's public facing IP. For example `AUTHSERVERIP=0:localhost` for locally hosted servers
|
||||
* Launch `legouniverse.exe`, through `wine` if on a Unix-like operating system
|
||||
* Note that if you are on WSL2, you will need to configure the public IP in the server and client to be the IP of the WSL2 instance and not localhost, which can be found by running `ifconfig` in the terminal. Windows defaults to WSL1, so this will not apply to most users.
|
||||
As an example, here is what the boot.cfg is required to contain for a server with the ip 12.34.56.78
|
||||
|
||||
@@ -105,7 +105,7 @@ void dChatFilter::ExportWordlistToDCF(const std::string& filepath, bool allowLis
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList) {
|
||||
std::set<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList) {
|
||||
if (gmLevel > eGameMasterLevel::FORUM_MODERATOR) return { }; //If anything but a forum mod, return true.
|
||||
if (message.empty()) return { };
|
||||
if (!allowList && m_DeniedWords.empty()) return { { 0, message.length() } };
|
||||
@@ -114,7 +114,7 @@ std::vector<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::
|
||||
std::string segment;
|
||||
std::regex reg("(!*|\\?*|\\;*|\\.*|\\,*)");
|
||||
|
||||
std::vector<std::pair<uint8_t, uint8_t>> listOfBadSegments = std::vector<std::pair<uint8_t, uint8_t>>();
|
||||
std::set<std::pair<uint8_t, uint8_t>> listOfBadSegments;
|
||||
|
||||
uint32_t position = 0;
|
||||
|
||||
@@ -127,17 +127,17 @@ std::vector<std::pair<uint8_t, uint8_t>> dChatFilter::IsSentenceOkay(const std::
|
||||
size_t hash = CalculateHash(segment);
|
||||
|
||||
if (std::find(m_UserUnapprovedWordCache.begin(), m_UserUnapprovedWordCache.end(), hash) != m_UserUnapprovedWordCache.end() && allowList) {
|
||||
listOfBadSegments.emplace_back(position, originalSegment.length());
|
||||
listOfBadSegments.emplace(position, originalSegment.length());
|
||||
}
|
||||
|
||||
if (std::find(m_ApprovedWords.begin(), m_ApprovedWords.end(), hash) == m_ApprovedWords.end() && allowList) {
|
||||
m_UserUnapprovedWordCache.push_back(hash);
|
||||
listOfBadSegments.emplace_back(position, originalSegment.length());
|
||||
listOfBadSegments.emplace(position, originalSegment.length());
|
||||
}
|
||||
|
||||
if (std::find(m_DeniedWords.begin(), m_DeniedWords.end(), hash) != m_DeniedWords.end() && !allowList) {
|
||||
m_UserUnapprovedWordCache.push_back(hash);
|
||||
listOfBadSegments.emplace_back(position, originalSegment.length());
|
||||
listOfBadSegments.emplace(position, originalSegment.length());
|
||||
}
|
||||
|
||||
position += originalSegment.length() + 1;
|
||||
|
||||
@@ -24,7 +24,7 @@ public:
|
||||
void ReadWordlistPlaintext(const std::string& filepath, bool allowList);
|
||||
bool ReadWordlistDCF(const std::string& filepath, bool allowList);
|
||||
void ExportWordlistToDCF(const std::string& filepath, bool allowList);
|
||||
std::vector<std::pair<uint8_t, uint8_t>> IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList = true);
|
||||
std::set<std::pair<uint8_t, uint8_t>> IsSentenceOkay(const std::string& message, eGameMasterLevel gmLevel, bool allowList = true);
|
||||
|
||||
private:
|
||||
bool m_DontGenerateDCF;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
set(DCHATSERVER_SOURCES
|
||||
"ChatIgnoreList.cpp"
|
||||
"ChatPacketHandler.cpp"
|
||||
"ChatWebAPI.cpp"
|
||||
"JSONUtils.cpp"
|
||||
"ChatJSONUtils.cpp"
|
||||
"ChatWeb.cpp"
|
||||
"PlayerContainer.cpp"
|
||||
"TeamContainer.cpp"
|
||||
)
|
||||
|
||||
add_executable(ChatServer "ChatServer.cpp")
|
||||
target_include_directories(ChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dChatFilter")
|
||||
target_include_directories(ChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dChatFilter" "${PROJECT_SOURCE_DIR}/dWeb")
|
||||
add_compile_definitions(ChatServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}\"")
|
||||
|
||||
add_library(dChatServer ${DCHATSERVER_SOURCES})
|
||||
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer")
|
||||
target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer" "${PROJECT_SOURCE_DIR}/dChatFilter")
|
||||
|
||||
target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter)
|
||||
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose)
|
||||
target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose dWeb)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "JSONUtils.h"
|
||||
#include "ChatJSONUtils.h"
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
@@ -47,16 +47,3 @@ void TeamContainer::to_json(json& data, const TeamContainer::Data& teamContainer
|
||||
data.push_back(*teamData);
|
||||
}
|
||||
}
|
||||
|
||||
std::string JSONUtils::CheckRequiredData(const json& data, const std::vector<std::string>& requiredData) {
|
||||
json check;
|
||||
check["error"] = json::array();
|
||||
for (const auto& required : requiredData) {
|
||||
if (!data.contains(required)) {
|
||||
check["error"].push_back("Missing Parameter: " + required);
|
||||
} else if (data[required] == "") {
|
||||
check["error"].push_back("Empty Parameter: " + required);
|
||||
}
|
||||
}
|
||||
return check["error"].empty() ? "" : check.dump();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#ifndef __JSONUTILS_H__
|
||||
#define __JSONUTILS_H__
|
||||
#ifndef __CHATJSONUTILS_H__
|
||||
#define __CHATJSONUTILS_H__
|
||||
|
||||
#include "json_fwd.hpp"
|
||||
#include "PlayerContainer.h"
|
||||
@@ -15,9 +15,4 @@ namespace TeamContainer {
|
||||
void to_json(nlohmann::json& data, const TeamContainer::Data& teamData);
|
||||
};
|
||||
|
||||
namespace JSONUtils {
|
||||
// check required data for reqeust
|
||||
std::string CheckRequiredData(const nlohmann::json& data, const std::vector<std::string>& requiredData);
|
||||
}
|
||||
|
||||
#endif // __JSONUTILS_H__
|
||||
#endif // !__CHATJSONUTILS_H__
|
||||
@@ -29,7 +29,7 @@
|
||||
#include "RakNetDefines.h"
|
||||
#include "MessageIdentifiers.h"
|
||||
|
||||
#include "ChatWebAPI.h"
|
||||
#include "ChatWeb.h"
|
||||
|
||||
namespace Game {
|
||||
Logger* logger = nullptr;
|
||||
@@ -93,17 +93,18 @@ int main(int argc, char** argv) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// seyup the chat api web server
|
||||
bool web_server_enabled = Game::config->GetValue("web_server_enabled") == "1";
|
||||
ChatWebAPI chatwebapi;
|
||||
if (web_server_enabled && !chatwebapi.Startup()) {
|
||||
// if we want the web api and it fails to start, exit
|
||||
// setup the chat api web server
|
||||
const uint32_t web_server_port = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("web_server_port")).value_or(2005);
|
||||
if (Game::config->GetValue("web_server_enabled") == "1" && !Game::web.Startup("localhost", web_server_port)) {
|
||||
// if we want the web server and it fails to start, exit
|
||||
LOG("Failed to start web server, shutting down.");
|
||||
Database::Destroy("ChatServer");
|
||||
delete Game::logger;
|
||||
delete Game::config;
|
||||
return EXIT_FAILURE;
|
||||
};
|
||||
}
|
||||
|
||||
if (Game::web.IsEnabled()) ChatWeb::RegisterRoutes();
|
||||
|
||||
//Find out the master's IP:
|
||||
std::string masterIP;
|
||||
@@ -167,10 +168,8 @@ int main(int argc, char** argv) {
|
||||
packet = nullptr;
|
||||
}
|
||||
|
||||
//Check and handle web requests:
|
||||
if (web_server_enabled) {
|
||||
chatwebapi.ReceiveRequests();
|
||||
}
|
||||
// Check and handle web requests:
|
||||
if (Game::web.IsEnabled()) Game::web.ReceiveRequests();
|
||||
|
||||
//Push our log every 30s:
|
||||
if (framesSinceLastFlush >= logFlushTime) {
|
||||
|
||||
133
dChatServer/ChatWeb.cpp
Normal file
133
dChatServer/ChatWeb.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "ChatWeb.h"
|
||||
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "json.hpp"
|
||||
#include "dCommonVars.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "dServer.h"
|
||||
#include "dConfig.h"
|
||||
#include "PlayerContainer.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "ChatPackets.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "Database.h"
|
||||
#include "ChatJSONUtils.h"
|
||||
#include "JSONUtils.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "dChatFilter.h"
|
||||
#include "TeamContainer.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = Game::playerContainer;
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
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, std::string body) {
|
||||
auto data = GeneralUtils::TryParse<json>(body);
|
||||
if (!data) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& good_data = data.value();
|
||||
auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" });
|
||||
if (!check.empty()) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = check;
|
||||
} else {
|
||||
|
||||
ChatPackets::Announcement announcement;
|
||||
announcement.title = good_data["title"];
|
||||
announcement.message = good_data["message"];
|
||||
announcement.Broadcast();
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = "{\"status\":\"Announcement Sent\"}";
|
||||
}
|
||||
}
|
||||
|
||||
void HandleWSChat(mg_connection* connection, json data) {
|
||||
auto check = JSONUtils::CheckRequiredData(data, { "user", "message", "gmlevel", "zone" });
|
||||
if (!check.empty()) {
|
||||
LOG_DEBUG("Received invalid websocket message: %s", check.c_str());
|
||||
} else {
|
||||
const auto user = data["user"].get<std::string>();
|
||||
const auto message = data["message"].get<std::string>();
|
||||
const auto gmlevel = GeneralUtils::TryParse<eGameMasterLevel>(data["gmlevel"].get<std::string>()).value_or(eGameMasterLevel::CIVILIAN);
|
||||
const auto zone = data["zone"].get<uint32_t>();
|
||||
|
||||
const auto filter_check = Game::chatFilter->IsSentenceOkay(message, gmlevel);
|
||||
if (!filter_check.empty()) {
|
||||
LOG_DEBUG("Chat message \"%s\" from %s was not allowed", message.c_str(), user.c_str());
|
||||
data["error"] = "Chat message blocked by filter";
|
||||
data["filtered"] = json::array();
|
||||
for (const auto& [start, len] : filter_check) {
|
||||
data["filtered"].push_back(message.substr(start, len));
|
||||
}
|
||||
mg_ws_send(connection, data.dump().c_str(), data.dump().size(), WEBSOCKET_OP_TEXT);
|
||||
return;
|
||||
}
|
||||
LOG("%s: %s", user.c_str(), message.c_str());
|
||||
|
||||
// TODO: Implement chat message handling from websocket message
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
namespace ChatWeb {
|
||||
void RegisterRoutes() {
|
||||
|
||||
// REST API v1 routes
|
||||
|
||||
std::string v1_route = "/api/v1/";
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandleHTTPPlayersRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "teams",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandleHTTPTeamsRequest
|
||||
});
|
||||
|
||||
Game::web.RegisterHTTPRoute({
|
||||
.path = v1_route + "announce",
|
||||
.method = eHTTPMethod::POST,
|
||||
.handle = HandleHTTPAnnounceRequest
|
||||
});
|
||||
|
||||
// WebSocket Events Handlers
|
||||
|
||||
// Game::web.RegisterWSEvent({
|
||||
// .name = "chat",
|
||||
// .handle = HandleWSChat
|
||||
// });
|
||||
|
||||
// WebSocket subscriptions
|
||||
|
||||
Game::web.RegisterWSSubscription("player");
|
||||
}
|
||||
|
||||
void SendWSPlayerUpdate(const PlayerData& player, eActivityType activityType) {
|
||||
json data;
|
||||
data["player_data"] = player;
|
||||
data["update_type"] = magic_enum::enum_name(activityType);
|
||||
Game::web.SendWSMessage("player", data);
|
||||
}
|
||||
}
|
||||
|
||||
19
dChatServer/ChatWeb.h
Normal file
19
dChatServer/ChatWeb.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef __CHATWEB_H__
|
||||
#define __CHATWEB_H__
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
#include "Web.h"
|
||||
#include "PlayerContainer.h"
|
||||
#include "IActivityLog.h"
|
||||
#include "ChatPacketHandler.h"
|
||||
|
||||
namespace ChatWeb {
|
||||
void RegisterRoutes();
|
||||
void SendWSPlayerUpdate(const PlayerData& player, eActivityType activityType);
|
||||
};
|
||||
|
||||
|
||||
#endif // __CHATWEB_H__
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
#include "ChatWebAPI.h"
|
||||
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
#include "json.hpp"
|
||||
#include "dCommonVars.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "dServer.h"
|
||||
#include "dConfig.h"
|
||||
#include "PlayerContainer.h"
|
||||
#include "JSONUtils.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "ChatPackets.h"
|
||||
#include "StringifiedEnum.h"
|
||||
#include "Database.h"
|
||||
|
||||
#ifdef DARKFLAME_PLATFORM_WIN32
|
||||
#pragma push_macro("DELETE")
|
||||
#undef DELETE
|
||||
#endif
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
typedef struct mg_connection mg_connection;
|
||||
typedef struct mg_http_message mg_http_message;
|
||||
|
||||
namespace {
|
||||
const char* json_content_type = "Content-Type: application/json\r\n";
|
||||
std::map<std::pair<eHTTPMethod, std::string>, WebAPIHTTPRoute> Routes{};
|
||||
}
|
||||
|
||||
bool ValidateAuthentication(const mg_http_message* http_msg) {
|
||||
// TO DO: This is just a placeholder for now
|
||||
// use tokens or something at a later point if we want to implement authentication
|
||||
// bit using the listen bind address to limit external access is good enough to start with
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ValidateJSON(std::optional<json> data, HTTPReply& reply) {
|
||||
if (!data) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid JSON\"}";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HandlePlayersRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = Game::playerContainer;
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleTeamsRequest(HTTPReply& reply, std::string body) {
|
||||
const json data = TeamContainer::GetTeamContainer();
|
||||
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
|
||||
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
|
||||
}
|
||||
|
||||
void HandleAnnounceRequest(HTTPReply& reply, std::string body) {
|
||||
auto data = GeneralUtils::TryParse<json>(body);
|
||||
if (!ValidateJSON(data, reply)) return;
|
||||
|
||||
const auto& good_data = data.value();
|
||||
auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" });
|
||||
if (!check.empty()) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = check;
|
||||
} else {
|
||||
|
||||
ChatPackets::Announcement announcement;
|
||||
announcement.title = good_data["title"];
|
||||
announcement.message = good_data["message"];
|
||||
announcement.Send();
|
||||
|
||||
reply.status = eHTTPStatusCode::OK;
|
||||
reply.message = "{\"status\":\"Announcement Sent\"}";
|
||||
}
|
||||
}
|
||||
|
||||
void HandleInvalidRoute(HTTPReply& reply) {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "{\"error\":\"Invalid Route\"}";
|
||||
}
|
||||
|
||||
void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) {
|
||||
HTTPReply reply;
|
||||
|
||||
if (!http_msg) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid Request\"}";
|
||||
} else if (ValidateAuthentication(http_msg)) {
|
||||
|
||||
// convert method from cstring to std string
|
||||
std::string method_string(http_msg->method.buf, http_msg->method.len);
|
||||
// get mehtod from mg to enum
|
||||
const eHTTPMethod method = magic_enum::enum_cast<eHTTPMethod>(method_string).value_or(eHTTPMethod::INVALID);
|
||||
|
||||
// convert uri from cstring to std string
|
||||
std::string uri(http_msg->uri.buf, http_msg->uri.len);
|
||||
std::transform(uri.begin(), uri.end(), uri.begin(), ::tolower);
|
||||
|
||||
// convert body from cstring to std string
|
||||
std::string body(http_msg->body.buf, http_msg->body.len);
|
||||
|
||||
|
||||
const auto routeItr = Routes.find({ method, uri });
|
||||
|
||||
if (routeItr != Routes.end()) {
|
||||
const auto& [_, route] = *routeItr;
|
||||
route.handle(reply, body);
|
||||
} else HandleInvalidRoute(reply);
|
||||
} else {
|
||||
reply.status = eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.message = "{\"error\":\"Unauthorized\"}";
|
||||
}
|
||||
mg_http_reply(connection, static_cast<int>(reply.status), json_content_type, reply.message.c_str());
|
||||
}
|
||||
|
||||
|
||||
void HandleRequests(mg_connection* connection, int request, void* request_data) {
|
||||
switch (request) {
|
||||
case MG_EV_HTTP_MSG:
|
||||
HandleHTTPMessage(connection, static_cast<mg_http_message*>(request_data));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatWebAPI::RegisterHTTPRoutes(WebAPIHTTPRoute route) {
|
||||
auto [_, success] = Routes.try_emplace({ route.method, route.path }, route);
|
||||
if (!success) {
|
||||
LOG_DEBUG("Failed to register route %s", route.path.c_str());
|
||||
} else {
|
||||
LOG_DEBUG("Registered route %s", route.path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ChatWebAPI::ChatWebAPI() {
|
||||
mg_log_set(MG_LL_NONE);
|
||||
mg_mgr_init(&mgr); // Initialize event manager
|
||||
}
|
||||
|
||||
ChatWebAPI::~ChatWebAPI() {
|
||||
mg_mgr_free(&mgr);
|
||||
}
|
||||
|
||||
bool ChatWebAPI::Startup() {
|
||||
// Make listen address
|
||||
// std::string listen_ip = Game::config->GetValue("web_server_listen_ip");
|
||||
// if (listen_ip == "localhost") listen_ip = "127.0.0.1";
|
||||
|
||||
const std::string& listen_port = Game::config->GetValue("web_server_listen_port");
|
||||
// const std::string& listen_address = "http://" + listen_ip + ":" + listen_port;
|
||||
const std::string& listen_address = "http://localhost:" + listen_port;
|
||||
LOG("Starting web server on %s", listen_address.c_str());
|
||||
|
||||
// Create HTTP listener
|
||||
if (!mg_http_listen(&mgr, listen_address.c_str(), HandleRequests, NULL)) {
|
||||
LOG("Failed to create web server listener on %s", listen_port.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Register routes
|
||||
|
||||
// API v1 routes
|
||||
std::string v1_route = "/api/v1/";
|
||||
RegisterHTTPRoutes({
|
||||
.path = v1_route + "players",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandlePlayersRequest
|
||||
});
|
||||
|
||||
RegisterHTTPRoutes({
|
||||
.path = v1_route + "teams",
|
||||
.method = eHTTPMethod::GET,
|
||||
.handle = HandleTeamsRequest
|
||||
});
|
||||
|
||||
RegisterHTTPRoutes({
|
||||
.path = v1_route + "announce",
|
||||
.method = eHTTPMethod::POST,
|
||||
.handle = HandleAnnounceRequest
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatWebAPI::ReceiveRequests() {
|
||||
mg_mgr_poll(&mgr, 15);
|
||||
}
|
||||
|
||||
#ifdef DARKFLAME_PLATFORM_WIN32
|
||||
#pragma pop_macro("DELETE")
|
||||
#endif
|
||||
@@ -1,36 +0,0 @@
|
||||
#ifndef __CHATWEBAPI_H__
|
||||
#define __CHATWEBAPI_H__
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
#include "mongoose.h"
|
||||
#include "eHTTPStatusCode.h"
|
||||
|
||||
enum class eHTTPMethod;
|
||||
|
||||
typedef struct mg_mgr mg_mgr;
|
||||
|
||||
struct HTTPReply {
|
||||
eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND;
|
||||
std::string message = "{\"error\":\"Not Found\"}";
|
||||
};
|
||||
|
||||
struct WebAPIHTTPRoute {
|
||||
std::string path;
|
||||
eHTTPMethod method;
|
||||
std::function<void(HTTPReply&, const std::string&)> handle;
|
||||
};
|
||||
|
||||
class ChatWebAPI {
|
||||
public:
|
||||
ChatWebAPI();
|
||||
~ChatWebAPI();
|
||||
void ReceiveRequests();
|
||||
void RegisterHTTPRoutes(WebAPIHTTPRoute route);
|
||||
bool Startup();
|
||||
private:
|
||||
mg_mgr mgr;
|
||||
|
||||
};
|
||||
|
||||
#endif // __CHATWEBAPI_H__
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "ChatPackets.h"
|
||||
#include "dConfig.h"
|
||||
#include "MessageType/Chat.h"
|
||||
#include "ChatWeb.h"
|
||||
#include "TeamContainer.h"
|
||||
|
||||
void PlayerContainer::Initialize() {
|
||||
@@ -59,8 +60,9 @@ void PlayerContainer::InsertPlayer(Packet* packet) {
|
||||
m_PlayerCount++;
|
||||
|
||||
LOG("Added user: %s (%llu), zone: %i", data.playerName.c_str(), data.playerID, data.zoneID.GetMapID());
|
||||
ChatWeb::SendWSPlayerUpdate(data, isLogin ? eActivityType::PlayerLoggedIn : eActivityType::PlayerChangedZone);
|
||||
|
||||
Database::Get()->UpdateActivityLog(data.playerID, eActivityType::PlayerLoggedIn, data.zoneID.GetMapID());
|
||||
Database::Get()->UpdateActivityLog(data.playerID, isLogin ? eActivityType::PlayerLoggedIn : eActivityType::PlayerChangedZone, data.zoneID.GetMapID());
|
||||
m_PlayersToRemove.erase(playerId);
|
||||
}
|
||||
|
||||
@@ -114,6 +116,8 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
|
||||
}
|
||||
}
|
||||
|
||||
ChatWeb::SendWSPlayerUpdate(player, eActivityType::PlayerLoggedOut);
|
||||
|
||||
m_PlayerCount--;
|
||||
LOG("Removed user: %llu", playerID);
|
||||
m_Players.erase(playerID);
|
||||
|
||||
@@ -16,6 +16,7 @@ set(DCOMMON_SOURCES
|
||||
"BrickByBrickFix.cpp"
|
||||
"BinaryPathFinder.cpp"
|
||||
"FdbToSqlite.cpp"
|
||||
"JSONUtils.cpp"
|
||||
"TinyXmlUtils.cpp"
|
||||
"Sd0.cpp"
|
||||
"Lxfml.cpp"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <assert.h>
|
||||
|
||||
#ifdef _DEBUG
|
||||
# define DluAssert(expression) do { assert(expression) } while(0)
|
||||
# define DluAssert(expression) do { assert(expression); } while(0)
|
||||
#else
|
||||
# define DluAssert(expression)
|
||||
#endif
|
||||
|
||||
17
dCommon/JSONUtils.cpp
Normal file
17
dCommon/JSONUtils.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "JSONUtils.h"
|
||||
#include "json.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
std::string JSONUtils::CheckRequiredData(const json& data, const std::vector<std::string>& requiredData) {
|
||||
json check;
|
||||
check["error"] = json::array();
|
||||
for (const auto& required : requiredData) {
|
||||
if (!data.contains(required)) {
|
||||
check["error"].push_back("Missing Parameter: " + required);
|
||||
} else if (data[required] == "") {
|
||||
check["error"].push_back("Empty Parameter: " + required);
|
||||
}
|
||||
}
|
||||
return check["error"].empty() ? "" : check.dump();
|
||||
}
|
||||
11
dCommon/JSONUtils.h
Normal file
11
dCommon/JSONUtils.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef _JSONUTILS_H_
|
||||
#define _JSONUTILS_H_
|
||||
|
||||
#include "json_fwd.hpp"
|
||||
|
||||
namespace JSONUtils {
|
||||
// check required fields in json data
|
||||
std::string CheckRequiredData(const nlohmann::json& data, const std::vector<std::string>& requiredData);
|
||||
}
|
||||
|
||||
#endif // _JSONUTILS_H_
|
||||
@@ -83,6 +83,12 @@ public:
|
||||
this->value = value;
|
||||
}
|
||||
|
||||
//! Initializer
|
||||
LDFData(const std::string& key, const T& value) {
|
||||
this->key = GeneralUtils::ASCIIToUTF16(key);
|
||||
this->value = value;
|
||||
}
|
||||
|
||||
//! Destructor
|
||||
~LDFData(void) override {}
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
#ifndef __DCOMMONVARS__H__
|
||||
#define __DCOMMONVARS__H__
|
||||
|
||||
#include <compare>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include "BitStream.h"
|
||||
#include "eConnectionType.h"
|
||||
#include "MessageType/Client.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "MessageType/Client.h"
|
||||
#include "eConnectionType.h"
|
||||
|
||||
#pragma warning (disable:4251) //Disables SQL warnings
|
||||
|
||||
@@ -99,6 +100,7 @@ public:
|
||||
constexpr LWOZONEID(const LWOMAPID& mapID, const LWOINSTANCEID& instanceID, const LWOCLONEID& cloneID) noexcept { m_MapID = mapID; m_InstanceID = instanceID; m_CloneID = cloneID; }
|
||||
constexpr LWOZONEID(const LWOZONEID& replacement) noexcept { *this = replacement; }
|
||||
constexpr bool operator==(const LWOZONEID&) const = default;
|
||||
constexpr auto operator<=>(const LWOZONEID&) const = default;
|
||||
|
||||
private:
|
||||
LWOMAPID m_MapID = LWOMAPID_INVALID; //1000 for VE, 1100 for AG, etc...
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef __EHTTPMETHODS__H__
|
||||
#define __EHTTPMETHODS__H__
|
||||
|
||||
#include "dPlatforms.h"
|
||||
|
||||
#ifdef DARKFLAME_PLATFORM_WIN32
|
||||
#pragma push_macro("DELETE")
|
||||
#undef DELETE
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
enum class eActivityType : uint32_t {
|
||||
PlayerLoggedIn,
|
||||
PlayerLoggedOut,
|
||||
PlayerChangedZone
|
||||
};
|
||||
|
||||
class IActivityLog {
|
||||
|
||||
@@ -1994,6 +1994,38 @@ void Entity::SetRotation(const NiQuaternion& rotation) {
|
||||
Game::entityManager->SerializeEntity(this);
|
||||
}
|
||||
|
||||
void Entity::SetVelocity(const NiPoint3& velocity) {
|
||||
auto* controllable = GetComponent<ControllablePhysicsComponent>();
|
||||
|
||||
if (controllable != nullptr) {
|
||||
controllable->SetVelocity(velocity);
|
||||
}
|
||||
|
||||
auto* simple = GetComponent<SimplePhysicsComponent>();
|
||||
|
||||
if (simple != nullptr) {
|
||||
simple->SetVelocity(velocity);
|
||||
}
|
||||
|
||||
Game::entityManager->SerializeEntity(this);
|
||||
}
|
||||
|
||||
const NiPoint3& Entity::GetVelocity() const {
|
||||
auto* controllable = GetComponent<ControllablePhysicsComponent>();
|
||||
|
||||
if (controllable != nullptr) {
|
||||
return controllable->GetVelocity();
|
||||
}
|
||||
|
||||
auto* simple = GetComponent<SimplePhysicsComponent>();
|
||||
|
||||
if (simple != nullptr) {
|
||||
return simple->GetVelocity();
|
||||
}
|
||||
|
||||
return NiPoint3Constant::ZERO;
|
||||
}
|
||||
|
||||
bool Entity::GetBoolean(const std::u16string& name) const {
|
||||
return GetVar<bool>(name);
|
||||
}
|
||||
|
||||
@@ -124,6 +124,8 @@ public:
|
||||
// then return the collision group from that.
|
||||
int32_t GetCollisionGroup() const;
|
||||
|
||||
const NiPoint3& GetVelocity() const;
|
||||
|
||||
/**
|
||||
* Setters
|
||||
*/
|
||||
@@ -148,6 +150,8 @@ public:
|
||||
|
||||
void SetRespawnRot(const NiQuaternion& rotation);
|
||||
|
||||
void SetVelocity(const NiPoint3& velocity);
|
||||
|
||||
/**
|
||||
* Component management
|
||||
*/
|
||||
@@ -329,7 +333,7 @@ public:
|
||||
* @brief The observable for player entity position updates.
|
||||
*/
|
||||
static Observable<Entity*, const PositionUpdate&> OnPlayerPositionUpdate;
|
||||
|
||||
|
||||
protected:
|
||||
LWOOBJID m_ObjectID;
|
||||
|
||||
@@ -435,7 +439,7 @@ const T& Entity::GetVar(const std::u16string& name) const {
|
||||
template<typename T>
|
||||
T Entity::GetVarAs(const std::u16string& name) const {
|
||||
const auto data = GetVarAsString(name);
|
||||
|
||||
|
||||
return GeneralUtils::TryParse<T>(data).value_or(LDFData<T>::Default);
|
||||
}
|
||||
|
||||
|
||||
@@ -320,7 +320,7 @@ const std::unordered_map<std::string, LWOOBJID>& EntityManager::GetSpawnPointEnt
|
||||
return m_SpawnPoints;
|
||||
}
|
||||
|
||||
void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr, const bool skipChecks) {
|
||||
void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr) {
|
||||
if (!entity) {
|
||||
LOG("Attempted to construct null entity");
|
||||
return;
|
||||
@@ -363,16 +363,14 @@ void EntityManager::ConstructEntity(Entity* entity, const SystemAddress& sysAddr
|
||||
entity->WriteComponents(stream, eReplicaPacketType::CONSTRUCTION);
|
||||
|
||||
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) {
|
||||
if (skipChecks) {
|
||||
Game::server->Send(stream, UNASSIGNED_SYSTEM_ADDRESS, true);
|
||||
} else {
|
||||
for (auto* player : PlayerManager::GetAllPlayers()) {
|
||||
if (player->GetPlayerReadyForUpdates()) {
|
||||
Game::server->Send(stream, player->GetSystemAddress(), false);
|
||||
} else {
|
||||
auto* ghostComponent = player->GetComponent<GhostComponent>();
|
||||
if (ghostComponent) ghostComponent->AddLimboConstruction(entity->GetObjectID());
|
||||
}
|
||||
for (auto* player : PlayerManager::GetAllPlayers()) {
|
||||
// Don't need to construct the player to themselves
|
||||
if (entity->GetObjectID() == player->GetObjectID()) continue;
|
||||
if (player->GetPlayerReadyForUpdates()) {
|
||||
Game::server->Send(stream, player->GetSystemAddress(), false);
|
||||
} else {
|
||||
auto* ghostComponent = player->GetComponent<GhostComponent>();
|
||||
if (ghostComponent) ghostComponent->AddLimboConstruction(entity->GetObjectID());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
const std::unordered_map<LWOOBJID, Entity*> GetAllEntities() const { return m_Entities; }
|
||||
#endif
|
||||
|
||||
void ConstructEntity(Entity* entity, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS, bool skipChecks = false);
|
||||
void ConstructEntity(Entity* entity, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
|
||||
void DestructEntity(Entity* entity, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS);
|
||||
void SerializeEntity(Entity* entity);
|
||||
void SerializeEntity(const Entity& entity);
|
||||
|
||||
@@ -24,12 +24,13 @@ namespace LeaderboardManager {
|
||||
std::map<GameID, Leaderboard::Type> leaderboardCache;
|
||||
}
|
||||
|
||||
Leaderboard::Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const Leaderboard::Type leaderboardType) {
|
||||
Leaderboard::Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const uint32_t numResults, const Leaderboard::Type leaderboardType) {
|
||||
this->gameID = gameID;
|
||||
this->weekly = weekly;
|
||||
this->infoType = infoType;
|
||||
this->leaderboardType = leaderboardType;
|
||||
this->relatedPlayer = relatedPlayer;
|
||||
this->numResults = numResults;
|
||||
}
|
||||
|
||||
Leaderboard::~Leaderboard() {
|
||||
@@ -144,7 +145,7 @@ void QueryToLdf(Leaderboard& leaderboard, const std::vector<ILeaderboard::Entry>
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ILeaderboard::Entry> FilterTo10(const std::vector<ILeaderboard::Entry>& leaderboard, const uint32_t relatedPlayer, const Leaderboard::InfoType infoType) {
|
||||
std::vector<ILeaderboard::Entry> FilterToNumResults(const std::vector<ILeaderboard::Entry>& leaderboard, const uint32_t relatedPlayer, const Leaderboard::InfoType infoType, const uint32_t numResults) {
|
||||
std::vector<ILeaderboard::Entry> toReturn;
|
||||
|
||||
int32_t index = 0;
|
||||
@@ -155,18 +156,19 @@ std::vector<ILeaderboard::Entry> FilterTo10(const std::vector<ILeaderboard::Entr
|
||||
}
|
||||
}
|
||||
|
||||
if (leaderboard.size() < 10) {
|
||||
if (leaderboard.size() < numResults) {
|
||||
toReturn.assign(leaderboard.begin(), leaderboard.end());
|
||||
index = 0;
|
||||
} else if (index < 10) {
|
||||
toReturn.assign(leaderboard.begin(), leaderboard.begin() + 10); // get the top 10 since we are in the top 10
|
||||
} else if (index < numResults) {
|
||||
toReturn.assign(leaderboard.begin(), leaderboard.begin() + numResults); // get the top 10 since we are in the top 10
|
||||
index = 0;
|
||||
} else if (index > leaderboard.size() - 10) {
|
||||
toReturn.assign(leaderboard.end() - 10, leaderboard.end()); // get the bottom 10 since we are in the bottom 10
|
||||
index = leaderboard.size() - 10;
|
||||
} else if (index > leaderboard.size() - numResults) {
|
||||
toReturn.assign(leaderboard.end() - numResults, leaderboard.end()); // get the bottom 10 since we are in the bottom 10
|
||||
index = leaderboard.size() - numResults;
|
||||
} else {
|
||||
toReturn.assign(leaderboard.begin() + index - 5, leaderboard.begin() + index + 5); // get the 5 above and below
|
||||
index -= 5;
|
||||
auto half = numResults / 2;
|
||||
toReturn.assign(leaderboard.begin() + index - half, leaderboard.begin() + index + half); // get the 5 above and below
|
||||
index -= half;
|
||||
}
|
||||
|
||||
int32_t i = index;
|
||||
@@ -178,14 +180,16 @@ std::vector<ILeaderboard::Entry> FilterTo10(const std::vector<ILeaderboard::Entr
|
||||
}
|
||||
|
||||
std::vector<ILeaderboard::Entry> FilterWeeklies(const std::vector<ILeaderboard::Entry>& leaderboard) {
|
||||
using namespace std::chrono;
|
||||
// Filter the leaderboard to only include entries from the last week
|
||||
const auto currentTime = std::chrono::system_clock::now();
|
||||
auto epochTime = currentTime.time_since_epoch().count();
|
||||
constexpr auto SECONDS_IN_A_WEEK = 60 * 60 * 24 * 7; // if you think im taking leap seconds into account thats cute.
|
||||
const auto epochTime = system_clock::now();
|
||||
constexpr auto oneWeek = weeks(1);
|
||||
|
||||
std::vector<ILeaderboard::Entry> weeklyLeaderboard;
|
||||
for (const auto& entry : leaderboard) {
|
||||
if (epochTime - entry.lastPlayedTimestamp < SECONDS_IN_A_WEEK) {
|
||||
const sys_time<seconds> asSysTime(seconds(entry.lastPlayedTimestamp));
|
||||
const auto timeDiff = epochTime - asSysTime;
|
||||
if (timeDiff < oneWeek) {
|
||||
weeklyLeaderboard.push_back(entry);
|
||||
}
|
||||
}
|
||||
@@ -213,14 +217,15 @@ std::vector<ILeaderboard::Entry> ProcessLeaderboard(
|
||||
const std::vector<ILeaderboard::Entry>& leaderboard,
|
||||
const bool weekly,
|
||||
const Leaderboard::InfoType infoType,
|
||||
const uint32_t relatedPlayer) {
|
||||
const uint32_t relatedPlayer,
|
||||
const uint32_t numResults) {
|
||||
std::vector<ILeaderboard::Entry> toReturn;
|
||||
|
||||
if (infoType == Leaderboard::InfoType::Friends) {
|
||||
const auto friendsLeaderboard = FilterFriends(leaderboard, relatedPlayer);
|
||||
toReturn = FilterTo10(weekly ? FilterWeeklies(friendsLeaderboard) : friendsLeaderboard, relatedPlayer, infoType);
|
||||
toReturn = FilterToNumResults(weekly ? FilterWeeklies(friendsLeaderboard) : friendsLeaderboard, relatedPlayer, infoType, numResults);
|
||||
} else {
|
||||
toReturn = FilterTo10(weekly ? FilterWeeklies(leaderboard) : leaderboard, relatedPlayer, infoType);
|
||||
toReturn = FilterToNumResults(weekly ? FilterWeeklies(leaderboard) : leaderboard, relatedPlayer, infoType, numResults);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
@@ -255,7 +260,7 @@ void Leaderboard::SetupLeaderboard(bool weekly) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto processedLeaderboard = ProcessLeaderboard(leaderboardRes, weekly, infoType, relatedPlayer);
|
||||
const auto processedLeaderboard = ProcessLeaderboard(leaderboardRes, weekly, infoType, relatedPlayer, numResults);
|
||||
|
||||
QueryToLdf(*this, processedLeaderboard);
|
||||
}
|
||||
@@ -301,8 +306,8 @@ void LeaderboardManager::SaveScore(const LWOOBJID& playerID, const GameID activi
|
||||
}
|
||||
}
|
||||
|
||||
void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID) {
|
||||
Leaderboard leaderboard(gameID, infoType, weekly, playerID, GetLeaderboardType(gameID));
|
||||
void LeaderboardManager::SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t numResults) {
|
||||
Leaderboard leaderboard(gameID, infoType, weekly, playerID, numResults, GetLeaderboardType(gameID));
|
||||
leaderboard.SetupLeaderboard(weekly);
|
||||
leaderboard.Send(targetID);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public:
|
||||
None
|
||||
};
|
||||
Leaderboard() = delete;
|
||||
Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const Leaderboard::Type = None);
|
||||
Leaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, LWOOBJID relatedPlayer, const uint32_t numResults, const Leaderboard::Type = None);
|
||||
|
||||
~Leaderboard();
|
||||
|
||||
@@ -79,6 +79,7 @@ private:
|
||||
InfoType infoType;
|
||||
Leaderboard::Type leaderboardType;
|
||||
bool weekly;
|
||||
uint32_t numResults;
|
||||
public:
|
||||
LeaderboardEntry& PushBackEntry() {
|
||||
return entries.emplace_back();
|
||||
@@ -90,7 +91,7 @@ public:
|
||||
};
|
||||
|
||||
namespace LeaderboardManager {
|
||||
void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID);
|
||||
void SendLeaderboard(const GameID gameID, const Leaderboard::InfoType infoType, const bool weekly, const LWOOBJID playerID, const LWOOBJID targetID, const uint32_t numResults);
|
||||
|
||||
void SaveScore(const LWOOBJID& playerID, const GameID activityId, const float primaryScore, const float secondaryScore = 0, const float tertiaryScore = 0);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "MessageType/Chat.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "CheatDetection.h"
|
||||
#include "CharacterComponent.h"
|
||||
|
||||
UserManager* UserManager::m_Address = nullptr;
|
||||
|
||||
@@ -340,7 +341,10 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
|
||||
|
||||
xml << "<char acct=\"" << u->GetAccountID() << "\" cc=\"0\" gm=\"0\" ft=\"0\" llog=\"" << time(NULL) << "\" ";
|
||||
xml << "ls=\"0\" lzx=\"-626.5847\" lzy=\"613.3515\" lzz=\"-28.6374\" lzrx=\"0.0\" lzry=\"0.7015\" lzrz=\"0.0\" lzrw=\"0.7126\" ";
|
||||
xml << "stt=\"0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;\"></char>";
|
||||
xml << "stt=\"0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;\">";
|
||||
xml << "<vl><l id=\"1000\" cid=\"0\"/></vl>";
|
||||
|
||||
xml << "</char>";
|
||||
|
||||
xml << "<dest hm=\"4\" hc=\"4\" im=\"0\" ic=\"0\" am=\"0\" ac=\"0\" d=\"0\"/>";
|
||||
|
||||
@@ -522,6 +526,13 @@ void UserManager::LoginCharacter(const SystemAddress& sysAddr, uint32_t playerID
|
||||
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneID, character->GetZoneClone(), false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
|
||||
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
|
||||
if (character) {
|
||||
auto* entity = character->GetEntity();
|
||||
if (entity) {
|
||||
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) {
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
}
|
||||
}
|
||||
character->SetZoneID(zoneID);
|
||||
character->SetZoneInstance(zoneInstance);
|
||||
character->SetZoneClone(zoneClone);
|
||||
|
||||
@@ -42,10 +42,15 @@ void PropertyTeleportBehavior::Handle(BehaviorContext* context, RakNet::BitStrea
|
||||
|
||||
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
|
||||
if (entity->GetCharacter()) {
|
||||
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) {
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
characterComponent->SetLastRocketConfig(u"");
|
||||
}
|
||||
|
||||
entity->GetCharacter()->SetZoneID(zoneID);
|
||||
entity->GetCharacter()->SetZoneInstance(zoneInstance);
|
||||
entity->GetCharacter()->SetZoneClone(zoneClone);
|
||||
entity->GetComponent<CharacterComponent>()->SetLastRocketConfig(u"");
|
||||
}
|
||||
|
||||
entity->GetCharacter()->SaveXMLToDatabase();
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "CDActivityRewardsTable.h"
|
||||
#include "CDActivitiesTable.h"
|
||||
#include "LeaderboardManager.h"
|
||||
#include "CharacterComponent.h"
|
||||
|
||||
ActivityComponent::ActivityComponent(Entity* parent, int32_t activityID) : Component(parent) {
|
||||
/*
|
||||
@@ -333,7 +334,7 @@ bool ActivityComponent::IsPlayedBy(LWOOBJID playerID) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ActivityComponent::TakeCost(Entity* player) const {
|
||||
bool ActivityComponent::CheckCost(Entity* player) const {
|
||||
if (m_ActivityInfo.optionalCostLOT <= 0 || m_ActivityInfo.optionalCostCount <= 0)
|
||||
return true;
|
||||
|
||||
@@ -344,11 +345,19 @@ bool ActivityComponent::TakeCost(Entity* player) const {
|
||||
if (inventoryComponent->GetLotCount(m_ActivityInfo.optionalCostLOT) < m_ActivityInfo.optionalCostCount)
|
||||
return false;
|
||||
|
||||
inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ActivityComponent::TakeCost(Entity* player) const{
|
||||
|
||||
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
||||
if (CheckCost(player)) {
|
||||
inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount);
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
void ActivityComponent::PlayerReady(Entity* player, bool bReady) {
|
||||
for (Lobby* lobby : m_Queue) {
|
||||
for (LobbyPlayer* lobbyPlayer : lobby->players) {
|
||||
@@ -381,7 +390,7 @@ ActivityInstance* ActivityComponent::NewInstance() {
|
||||
void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector<LobbyPlayer*>& lobby) const {
|
||||
for (LobbyPlayer* player : lobby) {
|
||||
auto* entity = player->GetEntity();
|
||||
if (entity == nullptr || !TakeCost(entity)) {
|
||||
if (entity == nullptr || !CheckCost(entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -526,6 +535,11 @@ void ActivityInstance::StartZone() {
|
||||
|
||||
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", player->GetCharacter()->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
|
||||
if (player->GetCharacter()) {
|
||||
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) {
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
}
|
||||
|
||||
player->GetCharacter()->SetZoneID(zoneID);
|
||||
player->GetCharacter()->SetZoneInstance(zoneInstance);
|
||||
player->GetCharacter()->SetZoneClone(zoneClone);
|
||||
|
||||
@@ -234,10 +234,17 @@ public:
|
||||
*/
|
||||
bool IsPlayedBy(LWOOBJID playerID) const;
|
||||
|
||||
/**
|
||||
* Checks if the entity has enough cost to play this activity
|
||||
* @param player the entity to check
|
||||
* @return true if the entity has enough cost to play this activity, false otherwise
|
||||
*/
|
||||
bool CheckCost(Entity* player) const;
|
||||
|
||||
/**
|
||||
* Removes the cost of the activity (e.g. green imaginate) for the entity that plays this activity
|
||||
* @param player the entity to take cost for
|
||||
* @return true if the cost was successfully deducted, false otherwise
|
||||
* @return true if the cost was taken, false otherwise
|
||||
*/
|
||||
bool TakeCost(Entity* player) const;
|
||||
|
||||
|
||||
@@ -245,6 +245,8 @@ void CharacterComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
|
||||
SetReputation(0);
|
||||
}
|
||||
|
||||
auto* vl = character->FirstChildElement("vl");
|
||||
if (vl) LoadVisitedLevelsXml(*vl);
|
||||
character->QueryUnsigned64Attribute("co", &m_ClaimCodes[0]);
|
||||
character->QueryUnsigned64Attribute("co1", &m_ClaimCodes[1]);
|
||||
character->QueryUnsigned64Attribute("co2", &m_ClaimCodes[2]);
|
||||
@@ -374,6 +376,10 @@ void CharacterComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* vl = character->FirstChildElement("vl");
|
||||
if (!vl) vl = character->InsertNewChildElement("vl");
|
||||
UpdateVisitedLevelsXml(*vl);
|
||||
|
||||
if (m_ClaimCodes[0] != 0) character->SetAttribute("co", m_ClaimCodes[0]);
|
||||
if (m_ClaimCodes[1] != 0) character->SetAttribute("co1", m_ClaimCodes[1]);
|
||||
if (m_ClaimCodes[2] != 0) character->SetAttribute("co2", m_ClaimCodes[2]);
|
||||
@@ -855,8 +861,9 @@ void CharacterComponent::SendToZone(LWOMAPID zoneId, LWOCLONEID cloneId) const {
|
||||
character->SetZoneID(zoneID);
|
||||
character->SetZoneInstance(zoneInstance);
|
||||
character->SetZoneClone(zoneClone);
|
||||
|
||||
|
||||
characterComponent->SetLastRocketConfig(u"");
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
|
||||
character->SaveXMLToDatabase();
|
||||
}
|
||||
@@ -883,3 +890,30 @@ void CharacterComponent::SetRespawnPos(const NiPoint3& position) {
|
||||
void CharacterComponent::SetRespawnRot(const NiQuaternion& rotation) {
|
||||
m_respawnRot = rotation;
|
||||
}
|
||||
|
||||
void CharacterComponent::AddVisitedLevel(const LWOZONEID zoneID) {
|
||||
LWOZONEID toInsert(zoneID.GetMapID(), LWOINSTANCEID_INVALID, zoneID.GetCloneID());
|
||||
m_VisitedLevels.insert(toInsert);
|
||||
}
|
||||
|
||||
void CharacterComponent::UpdateVisitedLevelsXml(tinyxml2::XMLElement& vl) {
|
||||
vl.DeleteChildren();
|
||||
// <vl>
|
||||
for (const auto zoneID : m_VisitedLevels) {
|
||||
// <l id=\"1100\" cid=\"0\"/>
|
||||
auto* l = vl.InsertNewChildElement("l");
|
||||
l->SetAttribute("id", zoneID.GetMapID());
|
||||
l->SetAttribute("cid", zoneID.GetCloneID());
|
||||
}
|
||||
// </vl>
|
||||
}
|
||||
|
||||
void CharacterComponent::LoadVisitedLevelsXml(const tinyxml2::XMLElement& vl) {
|
||||
// <vl>
|
||||
for (const auto* l = vl.FirstChildElement("l"); l != nullptr; l = l->NextSiblingElement("l")) {
|
||||
// <l id=\"1100\" cid=\"0\"/>
|
||||
LWOZONEID toInsert(l->IntAttribute("id"), LWOINSTANCEID_INVALID, l->IntAttribute("cid"));
|
||||
m_VisitedLevels.insert(toInsert);
|
||||
}
|
||||
// </vl>
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "tinyxml2.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
#include <array>
|
||||
#include <set>
|
||||
#include "Loot.h"
|
||||
|
||||
enum class eGameActivity : uint32_t;
|
||||
@@ -321,6 +322,13 @@ public:
|
||||
* Character info regarding this character, including clothing styles, etc.
|
||||
*/
|
||||
Character* m_Character;
|
||||
|
||||
/* Saves the provided zoneID as a visited level. Ignores InstanceID */
|
||||
void AddVisitedLevel(const LWOZONEID zoneID);
|
||||
/* Updates the VisitedLevels (vl) node of the charxml */
|
||||
void UpdateVisitedLevelsXml(tinyxml2::XMLElement& doc);
|
||||
/* Reads the VisitedLevels (vl) node of the charxml */
|
||||
void LoadVisitedLevelsXml(const tinyxml2::XMLElement& doc);
|
||||
private:
|
||||
|
||||
bool OnRequestServerObjectInfo(GameMessages::GameMsg& msg);
|
||||
@@ -619,6 +627,8 @@ private:
|
||||
std::map<LWOOBJID, Loot::Info> m_DroppedLoot;
|
||||
|
||||
uint64_t m_DroppedCoins = 0;
|
||||
|
||||
std::set<LWOZONEID> m_VisitedLevels;
|
||||
};
|
||||
|
||||
#endif // CHARACTERCOMPONENT_H
|
||||
|
||||
@@ -30,10 +30,16 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::GameMsg& msg) {
|
||||
unsmash.target = GetParent()->GetObjectID();
|
||||
unsmash.duration = 0.0f;
|
||||
unsmash.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
|
||||
m_Parent->SetPosition(m_OriginalPosition);
|
||||
m_Parent->SetRotation(m_OriginalRotation);
|
||||
m_Parent->SetVelocity(NiPoint3Constant::ZERO);
|
||||
|
||||
m_NumListeningInteract = 0;
|
||||
m_NumActiveUnSmash = 0;
|
||||
m_Dirty = true;
|
||||
Game::entityManager->SerializeEntity(GetParent());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -203,3 +209,31 @@ void ModelComponent::RemoveUnSmash() {
|
||||
LOG_DEBUG("Removing UnSmash %i", m_NumActiveUnSmash);
|
||||
m_NumActiveUnSmash--;
|
||||
}
|
||||
|
||||
bool ModelComponent::TrySetVelocity(const NiPoint3& velocity) const {
|
||||
auto currentVelocity = m_Parent->GetVelocity();
|
||||
|
||||
// If we're currently moving on an axis, prevent the move so only 1 behavior can have control over an axis
|
||||
if (velocity != NiPoint3Constant::ZERO) {
|
||||
const auto [x, y, z] = velocity;
|
||||
if (x != 0.0f) {
|
||||
if (currentVelocity.x != 0.0f) return false;
|
||||
currentVelocity.x = x;
|
||||
} else if (y != 0.0f) {
|
||||
if (currentVelocity.y != 0.0f) return false;
|
||||
currentVelocity.y = y;
|
||||
} else if (z != 0.0f) {
|
||||
if (currentVelocity.z != 0.0f) return false;
|
||||
currentVelocity.z = z;
|
||||
}
|
||||
} else {
|
||||
currentVelocity = velocity;
|
||||
}
|
||||
|
||||
m_Parent->SetVelocity(currentVelocity);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModelComponent::SetVelocity(const NiPoint3& velocity) const {
|
||||
m_Parent->SetVelocity(velocity);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
* Returns the original position of the model
|
||||
* @return the original position of the model
|
||||
*/
|
||||
const NiPoint3& GetPosition() { return m_OriginalPosition; }
|
||||
const NiPoint3& GetOriginalPosition() { return m_OriginalPosition; }
|
||||
|
||||
/**
|
||||
* Sets the original position of the model
|
||||
@@ -53,7 +53,7 @@ public:
|
||||
* Returns the original rotation of the model
|
||||
* @return the original rotation of the model
|
||||
*/
|
||||
const NiQuaternion& GetRotation() { return m_OriginalRotation; }
|
||||
const NiQuaternion& GetOriginalRotation() { return m_OriginalRotation; }
|
||||
|
||||
/**
|
||||
* Sets the original rotation of the model
|
||||
@@ -130,6 +130,14 @@ public:
|
||||
bool IsUnSmashing() const { return m_NumActiveUnSmash != 0; }
|
||||
|
||||
void Resume();
|
||||
|
||||
// Attempts to set the velocity of an axis for movement.
|
||||
// If the axis currently has a velocity of zero, returns true.
|
||||
// If the axis is currently controlled by a behavior, returns false.
|
||||
bool TrySetVelocity(const NiPoint3& velocity) const;
|
||||
|
||||
// Force sets the velocity to a value.
|
||||
void SetVelocity(const NiPoint3& velocity) const;
|
||||
private:
|
||||
// Number of Actions that are awaiting an UnSmash to finish.
|
||||
uint32_t m_NumActiveUnSmash{};
|
||||
|
||||
@@ -54,6 +54,7 @@ MovementAIComponent::MovementAIComponent(Entity* parent, MovementAIInfo info) :
|
||||
m_SourcePosition = m_Parent->GetPosition();
|
||||
m_Paused = false;
|
||||
m_SavedVelocity = NiPoint3Constant::ZERO;
|
||||
m_IsBounced = false;
|
||||
|
||||
if (!m_Parent->GetComponent<BaseCombatAIComponent>()) SetPath(m_Parent->GetVarAsString(u"attached_path"));
|
||||
}
|
||||
@@ -65,6 +66,67 @@ void MovementAIComponent::SetPath(const std::string pathName) {
|
||||
SetMaxSpeed(1);
|
||||
SetCurrentSpeed(m_BaseSpeed);
|
||||
SetPath(m_Path->pathWaypoints);
|
||||
|
||||
auto start_index = m_Parent->GetVarAs<uint32_t>(u"attached_path_start");
|
||||
if (start_index >= m_Path->waypointCount) start_index = 0;
|
||||
|
||||
SetDestination(m_Path->pathWaypoints[m_PathIndex].position);
|
||||
m_PathIndex = start_index;
|
||||
|
||||
}
|
||||
|
||||
void MovementAIComponent::SetPath(const std::string pathName, uint32_t startIndex, bool reverse) {
|
||||
m_Path = Game::zoneManager->GetZone()->GetPath(pathName);
|
||||
if (!pathName.empty()) LOG("WARNING: %s path %s", m_Path ? "Found" : "Failed to find", pathName.c_str());
|
||||
if (!m_Path) return;
|
||||
SetMaxSpeed(1);
|
||||
SetCurrentSpeed(m_BaseSpeed);
|
||||
|
||||
if (reverse) {
|
||||
m_IsBounced = !m_IsBounced;
|
||||
auto pathWaypoints = m_Path->pathWaypoints;
|
||||
std::reverse(pathWaypoints.begin(), pathWaypoints.end());
|
||||
SetPath(pathWaypoints);
|
||||
} else {
|
||||
SetPath(m_Path->pathWaypoints);
|
||||
}
|
||||
|
||||
SetDestination(m_Path->pathWaypoints[startIndex].position);
|
||||
m_PathIndex = startIndex;
|
||||
}
|
||||
|
||||
void MovementAIComponent::TurnAroundOnPath() {
|
||||
if (m_Path) {
|
||||
auto currentIndex = m_PathIndex;
|
||||
m_IsBounced = !m_IsBounced;
|
||||
std::vector<PathWaypoint> waypoints = m_Path->pathWaypoints;
|
||||
if (m_IsBounced) std::reverse(waypoints.begin(), waypoints.end());
|
||||
SetPath(waypoints);
|
||||
SetDestination(waypoints[currentIndex].position);
|
||||
m_PathIndex = currentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void MovementAIComponent::GoForwardOnPath() {
|
||||
if (m_Path) {
|
||||
auto currentIndex = m_PathIndex;
|
||||
m_IsBounced = false;
|
||||
SetPath(m_Path->pathWaypoints);
|
||||
SetDestination(m_Path->pathWaypoints[currentIndex].position);
|
||||
m_PathIndex = currentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void MovementAIComponent::GoBackwardOnPath() {
|
||||
if (m_Path) {
|
||||
auto currentIndex = m_PathIndex;
|
||||
m_IsBounced = true;
|
||||
std::vector<PathWaypoint> waypoints = m_Path->pathWaypoints;
|
||||
std::reverse(waypoints.begin(), waypoints.end());
|
||||
SetPath(waypoints);
|
||||
SetDestination(waypoints[currentIndex].position);
|
||||
m_PathIndex = currentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void MovementAIComponent::Pause() {
|
||||
@@ -158,8 +220,9 @@ void MovementAIComponent::Update(const float deltaTime) {
|
||||
if (m_Path->pathBehavior == PathBehavior::Loop) {
|
||||
SetPath(m_Path->pathWaypoints);
|
||||
} else if (m_Path->pathBehavior == PathBehavior::Bounce) {
|
||||
m_IsBounced = !m_IsBounced;
|
||||
std::vector<PathWaypoint> waypoints = m_Path->pathWaypoints;
|
||||
std::reverse(waypoints.begin(), waypoints.end());
|
||||
if (m_IsBounced) std::reverse(waypoints.begin(), waypoints.end());
|
||||
SetPath(waypoints);
|
||||
} else if (m_Path->pathBehavior == PathBehavior::Once) {
|
||||
Stop();
|
||||
@@ -186,7 +249,7 @@ bool MovementAIComponent::AdvanceWaypointIndex() {
|
||||
if (m_PathIndex >= m_InterpolatedWaypoints.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Parent->GetScript()->OnWaypointReached(m_Parent, m_PathIndex);
|
||||
m_PathIndex++;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -65,6 +65,10 @@ public:
|
||||
MovementAIComponent(Entity* parentEntity, MovementAIInfo info);
|
||||
|
||||
void SetPath(const std::string pathName);
|
||||
void SetPath(const std::string pathName, uint32_t startIndex, bool reverse);
|
||||
void TurnAroundOnPath();
|
||||
void GoForwardOnPath();
|
||||
void GoBackwardOnPath();
|
||||
|
||||
void Update(float deltaTime) override;
|
||||
|
||||
@@ -321,6 +325,8 @@ private:
|
||||
bool m_Paused;
|
||||
|
||||
NiPoint3 m_SavedVelocity;
|
||||
|
||||
bool m_IsBounced{};
|
||||
};
|
||||
|
||||
#endif // MOVEMENTAICOMPONENT_H
|
||||
|
||||
@@ -42,35 +42,6 @@ std::unordered_map<LWOOBJID, LWOOBJID> PetComponent::activePets{};
|
||||
* Maps all the pet lots to a flag indicating that the player has caught it. All basic pets have been guessed by ObjID
|
||||
* while the faction ones could be checked using their respective missions.
|
||||
*/
|
||||
const std::map<LOT, int32_t> PetComponent::petFlags{
|
||||
{ 3050, 801 }, // Elephant
|
||||
{ 3054, 803 }, // Cat
|
||||
{ 3195, 806 }, // Triceratops
|
||||
{ 3254, 807 }, // Terrier
|
||||
{ 3261, 811 }, // Skunk
|
||||
{ 3672, 813 }, // Bunny
|
||||
{ 3994, 814 }, // Crocodile
|
||||
{ 5635, 815 }, // Doberman
|
||||
{ 5636, 816 }, // Buffalo
|
||||
{ 5637, 818 }, // Robot Dog
|
||||
{ 5639, 819 }, // Red Dragon
|
||||
{ 5640, 820 }, // Tortoise
|
||||
{ 5641, 821 }, // Green Dragon
|
||||
{ 5643, 822 }, // Panda, see mission 786
|
||||
{ 5642, 823 }, // Mantis
|
||||
{ 6720, 824 }, // Warthog
|
||||
{ 3520, 825 }, // Lion, see mission 1318
|
||||
{ 7638, 826 }, // Goat
|
||||
{ 7694, 827 }, // Crab
|
||||
{ 12294, 829 }, // Reindeer
|
||||
{ 12431, 830 }, // Stegosaurus, see mission 1386
|
||||
{ 12432, 831 }, // Saber cat, see mission 1389
|
||||
{ 12433, 832 }, // Gryphon, see mission 1392
|
||||
{ 12434, 833 }, // Alien, see mission 1188
|
||||
// 834: unknown?, see mission 506, 688
|
||||
{ 16210, 836 }, // Ninjago Earth Dragon, see mission 1836
|
||||
{ 13067, 838 }, // Skeleton dragon
|
||||
};
|
||||
|
||||
PetComponent::PetComponent(Entity* parentEntity, uint32_t componentId) : Component{ parentEntity } {
|
||||
m_PetInfo = CDClientManager::GetTable<CDPetComponentTable>()->GetByID(componentId); // TODO: Make reference when safe
|
||||
@@ -556,9 +527,8 @@ void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) {
|
||||
);
|
||||
|
||||
// Triggers the catch a pet missions
|
||||
if (petFlags.find(m_Parent->GetLOT()) != petFlags.end()) {
|
||||
tamer->GetCharacter()->SetPlayerFlag(petFlags.at(m_Parent->GetLOT()), true);
|
||||
}
|
||||
constexpr auto PET_FLAG_BASE = 800;
|
||||
tamer->GetCharacter()->SetPlayerFlag(PET_FLAG_BASE + m_ComponentId, true);
|
||||
|
||||
auto* missionComponent = tamer->GetComponent<MissionComponent>();
|
||||
|
||||
@@ -942,6 +912,11 @@ void PetComponent::Command(const NiPoint3& position, const LWOOBJID source, cons
|
||||
if (commandType == 1) {
|
||||
// Emotes
|
||||
GameMessages::SendPlayEmote(m_Parent->GetObjectID(), typeId, owner->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS);
|
||||
GameMessages::EmotePlayed msg;
|
||||
msg.target = owner->GetObjectID();
|
||||
msg.emoteID = typeId;
|
||||
msg.targetID = 0; // Or set to the intended target entity's ID, or 0 if no target
|
||||
msg.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
} else if (commandType == 3) {
|
||||
// Follow me, ???
|
||||
} else if (commandType == 6) {
|
||||
|
||||
@@ -250,11 +250,6 @@ private:
|
||||
*/
|
||||
static std::unordered_map<LWOOBJID, LWOOBJID> currentActivities;
|
||||
|
||||
/**
|
||||
* Flags that indicate that a player has tamed a pet, indexed by the LOT of the pet
|
||||
*/
|
||||
static const std::map<LOT, int32_t> petFlags;
|
||||
|
||||
/**
|
||||
* The ID of the component in the pet component table
|
||||
*/
|
||||
|
||||
@@ -272,6 +272,10 @@ void PropertyManagementComponent::OnStartBuilding() {
|
||||
model->HandleMsg(reset);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto* const entity : Game::entityManager->GetEntitiesInGroup("SpawnedPropertyEnemies")) {
|
||||
if (entity) entity->Smash();
|
||||
}
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::OnFinishBuilding() {
|
||||
@@ -296,6 +300,10 @@ void PropertyManagementComponent::OnFinishBuilding() {
|
||||
model->HandleMsg(reset);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto* const entity : Game::entityManager->GetEntitiesInGroup("SpawnedPropertyEnemies")) {
|
||||
if (entity) entity->Smash();
|
||||
}
|
||||
}
|
||||
|
||||
void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) {
|
||||
@@ -696,8 +704,9 @@ void PropertyManagementComponent::Save() {
|
||||
Database::Get()->AddBehavior(info);
|
||||
}
|
||||
|
||||
const auto position = entity->GetPosition();
|
||||
const auto rotation = entity->GetRotation();
|
||||
// Always save the original position so we can move the model freely
|
||||
const auto& position = modelComponent->GetOriginalPosition();
|
||||
const auto& rotation = modelComponent->GetOriginalRotation();
|
||||
|
||||
if (std::find(present.begin(), present.end(), id) == present.end()) {
|
||||
IPropertyContents::Model model;
|
||||
|
||||
@@ -33,6 +33,13 @@ SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, int32_t component
|
||||
SimplePhysicsComponent::~SimplePhysicsComponent() {
|
||||
}
|
||||
|
||||
void SimplePhysicsComponent::Update(const float deltaTime) {
|
||||
if (m_Velocity == NiPoint3Constant::ZERO) return;
|
||||
m_Position += m_Velocity * deltaTime;
|
||||
m_DirtyPosition = true;
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
}
|
||||
|
||||
void SimplePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
||||
if (bIsInitialUpdate) {
|
||||
outBitStream.Write(m_ClimbableType != eClimbableType::CLIMBABLE_TYPE_NOT);
|
||||
|
||||
@@ -33,6 +33,8 @@ public:
|
||||
SimplePhysicsComponent(Entity* parent, int32_t componentID);
|
||||
~SimplePhysicsComponent() override;
|
||||
|
||||
void Update(const float deltaTime) override;
|
||||
|
||||
void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override;
|
||||
|
||||
/**
|
||||
|
||||
@@ -457,40 +457,43 @@ void TriggerComponent::HandleActivatePhysics(Entity* targetEntity, std::string a
|
||||
void TriggerComponent::HandleSetPath(Entity* targetEntity, std::vector<std::string> argArray) {
|
||||
auto* movementAIComponent = targetEntity->GetComponent<MovementAIComponent>();
|
||||
if (!movementAIComponent) return;
|
||||
// movementAIComponent->SetupPath(argArray.at(0));
|
||||
uint32_t start_index = 0;
|
||||
bool reverse = false;
|
||||
if (argArray.size() >= 2) {
|
||||
auto index = GeneralUtils::TryParse<int32_t>(argArray.at(1));
|
||||
if (!index) return;
|
||||
// movementAIComponent->SetPathStartingWaypointIndex(index.value());
|
||||
if (index) {
|
||||
start_index = index.value();
|
||||
}
|
||||
}
|
||||
if (argArray.size() >= 3 && argArray.at(2) == "1") {
|
||||
// movementAIComponent->ReversePath();
|
||||
reverse = true;
|
||||
}
|
||||
movementAIComponent->SetPath(argArray.at(0), start_index, reverse);
|
||||
}
|
||||
|
||||
void TriggerComponent::HandleTurnAroundOnPath(Entity* targetEntity) {
|
||||
auto* movementAIComponent = targetEntity->GetComponent<MovementAIComponent>();
|
||||
// if (movementAIComponent) movementAIComponent->ReversePath();
|
||||
if (movementAIComponent) movementAIComponent->TurnAroundOnPath();
|
||||
}
|
||||
|
||||
void TriggerComponent::HandleGoForwardOnPath(Entity* targetEntity) {
|
||||
auto* movementAIComponent = targetEntity->GetComponent<MovementAIComponent>();
|
||||
if (!movementAIComponent) return;
|
||||
// if (movementAIComponent->GetIsInReverse()) movementAIComponent->ReversePath();
|
||||
movementAIComponent->GoForwardOnPath();
|
||||
}
|
||||
|
||||
void TriggerComponent::HandleGoBackwardOnPath(Entity* targetEntity) {
|
||||
auto* movementAIComponent = targetEntity->GetComponent<MovementAIComponent>();
|
||||
if (!movementAIComponent) return;
|
||||
// if (!movementAIComponent->GetIsInReverse()) movementAIComponent->ReversePath();
|
||||
movementAIComponent->GoBackwardOnPath();
|
||||
}
|
||||
|
||||
void TriggerComponent::HandleStopPathing(Entity* targetEntity) {
|
||||
auto* movementAIComponent = targetEntity->GetComponent<MovementAIComponent>();
|
||||
// if (movementAIComponent) movementAIComponent->Pause();
|
||||
if (movementAIComponent) movementAIComponent->Pause();
|
||||
}
|
||||
|
||||
void TriggerComponent::HandleStartPathing(Entity* targetEntity) {
|
||||
auto* movementAIComponent = targetEntity->GetComponent<MovementAIComponent>();
|
||||
// if (movementAIComponent) movementAIComponent->Resume();
|
||||
if (movementAIComponent) movementAIComponent->Resume();
|
||||
}
|
||||
|
||||
@@ -1698,7 +1698,8 @@ void GameMessages::HandleRequestActivitySummaryLeaderboardData(RakNet::BitStream
|
||||
|
||||
bool weekly = inStream.ReadBit();
|
||||
|
||||
LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID());
|
||||
// The client won't accept more than 10 results even if we wanted it to
|
||||
LeaderboardManager::SendLeaderboard(gameID, queryType, weekly, entity->GetObjectID(), entity->GetObjectID(), 10);
|
||||
}
|
||||
|
||||
void GameMessages::HandleActivityStateChangeRequest(RakNet::BitStream& inStream, Entity* entity) {
|
||||
@@ -4925,6 +4926,11 @@ void GameMessages::HandleFireEventServerSide(RakNet::BitStream& inStream, Entity
|
||||
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
|
||||
|
||||
if (character) {
|
||||
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) {
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
}
|
||||
|
||||
character->SetZoneID(zoneID);
|
||||
character->SetZoneInstance(zoneInstance);
|
||||
character->SetZoneClone(zoneClone);
|
||||
@@ -4957,7 +4963,7 @@ void GameMessages::HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* e
|
||||
}
|
||||
|
||||
void GameMessages::HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity) {
|
||||
int emoteID;
|
||||
int32_t emoteID;
|
||||
LWOOBJID targetID;
|
||||
|
||||
inStream.Read(emoteID);
|
||||
@@ -4976,7 +4982,11 @@ void GameMessages::HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity)
|
||||
if (emote) sAnimationName = emote->animationName;
|
||||
}
|
||||
|
||||
RenderComponent::PlayAnimation(entity, sAnimationName);
|
||||
GameMessages::EmotePlayed msg;
|
||||
msg.target = entity->GetObjectID();
|
||||
msg.emoteID = emoteID;
|
||||
msg.targetID = targetID; // The emote’s target entity or 0 if none
|
||||
msg.Send(UNASSIGNED_SYSTEM_ADDRESS); // Broadcast to all clients
|
||||
|
||||
MissionComponent* missionComponent = entity->GetComponent<MissionComponent>();
|
||||
if (!missionComponent) return;
|
||||
@@ -5197,7 +5207,7 @@ void GameMessages::HandlePickupCurrency(RakNet::BitStream& inStream, Entity* ent
|
||||
if (currency == 0) return;
|
||||
|
||||
auto* ch = entity->GetCharacter();
|
||||
if (entity->CanPickupCoins(currency)) {
|
||||
if (ch && entity->CanPickupCoins(currency)) {
|
||||
ch->SetCoins(ch->GetCoins() + currency, eLootSourceType::PICKUP);
|
||||
}
|
||||
}
|
||||
@@ -6463,4 +6473,9 @@ namespace GameMessages {
|
||||
stream.Write(soundID != -1);
|
||||
if (soundID != -1) stream.Write(soundID);
|
||||
}
|
||||
|
||||
void EmotePlayed::Serialize(RakNet::BitStream& stream) const {
|
||||
stream.Write(emoteID);
|
||||
stream.Write(targetID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,6 +833,15 @@ namespace GameMessages {
|
||||
struct ResetModelToDefaults : public GameMsg {
|
||||
ResetModelToDefaults() : GameMsg(MessageType::Game::RESET_MODEL_TO_DEFAULTS) {}
|
||||
};
|
||||
};
|
||||
|
||||
struct EmotePlayed : public GameMsg {
|
||||
EmotePlayed() : GameMsg(MessageType::Game::EMOTE_PLAYED), emoteID(0), targetID(0) {}
|
||||
|
||||
void Serialize(RakNet::BitStream& stream) const override;
|
||||
|
||||
int32_t emoteID;
|
||||
LWOOBJID targetID;
|
||||
};
|
||||
|
||||
};
|
||||
#endif // GAMEMESSAGES_H
|
||||
|
||||
@@ -84,9 +84,11 @@ void Strip::HandleMsg(MigrateActionsMessage& msg) {
|
||||
|
||||
template<>
|
||||
void Strip::HandleMsg(GameMessages::RequestUse& msg) {
|
||||
if (m_PausedTime > 0.0f) return;
|
||||
if (m_PausedTime > 0.0f || !HasMinimumActions()) return;
|
||||
|
||||
if (m_Actions[m_NextActionIndex].GetType() == "OnInteract") {
|
||||
auto& nextAction = GetNextAction();
|
||||
|
||||
if (nextAction.GetType() == "OnInteract") {
|
||||
IncrementAction();
|
||||
m_WaitingForAction = false;
|
||||
}
|
||||
@@ -97,6 +99,8 @@ void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) {
|
||||
m_WaitingForAction = false;
|
||||
m_PausedTime = 0.0f;
|
||||
m_NextActionIndex = 0;
|
||||
m_InActionMove = NiPoint3Constant::ZERO;
|
||||
m_PreviousFramePosition = NiPoint3Constant::ZERO;
|
||||
}
|
||||
|
||||
void Strip::IncrementAction() {
|
||||
@@ -111,7 +115,9 @@ void Strip::Spawn(LOT lot, Entity& entity) {
|
||||
info.pos = entity.GetPosition();
|
||||
info.rot = NiQuaternionConstant::IDENTITY;
|
||||
info.spawnerID = entity.GetObjectID();
|
||||
Game::entityManager->ConstructEntity(Game::entityManager->CreateEntity(info, nullptr, &entity));
|
||||
auto* const spawnedEntity = Game::entityManager->CreateEntity(info, nullptr, &entity);
|
||||
spawnedEntity->AddToGroup("SpawnedPropertyEnemies");
|
||||
Game::entityManager->ConstructEntity(spawnedEntity);
|
||||
}
|
||||
|
||||
// Spawns a specific drop for all
|
||||
@@ -127,19 +133,39 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
|
||||
auto number = nextAction.GetValueParameterDouble();
|
||||
auto numberAsInt = static_cast<int32_t>(number);
|
||||
auto nextActionType = GetNextAction().GetType();
|
||||
if (nextActionType == "SpawnStromling") {
|
||||
Spawn(10495, entity); // Stromling property
|
||||
} else if (nextActionType == "SpawnPirate") {
|
||||
Spawn(10497, entity); // Maelstrom Pirate property
|
||||
} else if (nextActionType == "SpawnRonin") {
|
||||
Spawn(10498, entity); // Dark Ronin property
|
||||
} else if (nextActionType == "DropImagination") {
|
||||
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(935, entity); // 1 Imagination powerup
|
||||
} else if (nextActionType == "DropHealth") {
|
||||
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(177, entity); // 1 Life powerup
|
||||
} else if (nextActionType == "DropArmor") {
|
||||
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup
|
||||
} else if (nextActionType == "Smash") {
|
||||
|
||||
// TODO replace with switch case and nextActionType with enum
|
||||
/* BEGIN Move */
|
||||
if (nextActionType == "MoveRight" || nextActionType == "MoveLeft") {
|
||||
// X axis
|
||||
bool isMoveLeft = nextActionType == "MoveLeft";
|
||||
// Default velocity is 3 units per second.
|
||||
if (modelComponent.TrySetVelocity(NiPoint3{ isMoveLeft ? -3.0f : 3.0f, 0.0f, 0.0f })) {
|
||||
m_PreviousFramePosition = entity.GetPosition();
|
||||
m_InActionMove.x = isMoveLeft ? -number : number;
|
||||
}
|
||||
} else if (nextActionType == "FlyUp" || nextActionType == "FlyDown") {
|
||||
// Y axis
|
||||
bool isFlyDown = nextActionType == "FlyDown";
|
||||
// Default velocity is 3 units per second.
|
||||
if (modelComponent.TrySetVelocity(NiPoint3{ 0.0f, isFlyDown ? -3.0f : 3.0f, 0.0f })) {
|
||||
m_PreviousFramePosition = entity.GetPosition();
|
||||
m_InActionMove.y = isFlyDown ? -number : number;
|
||||
}
|
||||
|
||||
} else if (nextActionType == "MoveForward" || nextActionType == "MoveBackward") {
|
||||
// Z axis
|
||||
bool isMoveBackward = nextActionType == "MoveBackward";
|
||||
// Default velocity is 3 units per second.
|
||||
if (modelComponent.TrySetVelocity(NiPoint3{ 0.0f, 0.0f, isMoveBackward ? -3.0f : 3.0f })) {
|
||||
m_PreviousFramePosition = entity.GetPosition();
|
||||
m_InActionMove.z = isMoveBackward ? -number : number;
|
||||
}
|
||||
}
|
||||
/* END Move */
|
||||
|
||||
/* BEGIN Action */
|
||||
else if (nextActionType == "Smash") {
|
||||
if (!modelComponent.IsUnSmashing()) {
|
||||
GameMessages::Smash smash{};
|
||||
smash.target = entity.GetObjectID();
|
||||
@@ -162,13 +188,29 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent) {
|
||||
sound.target = modelComponent.GetParent()->GetObjectID();
|
||||
sound.soundID = numberAsInt;
|
||||
sound.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
||||
} else {
|
||||
}
|
||||
/* END Action */
|
||||
/* BEGIN Gameplay */
|
||||
else if (nextActionType == "SpawnStromling") {
|
||||
Spawn(10495, entity); // Stromling property
|
||||
} else if (nextActionType == "SpawnPirate") {
|
||||
Spawn(10497, entity); // Maelstrom Pirate property
|
||||
} else if (nextActionType == "SpawnRonin") {
|
||||
Spawn(10498, entity); // Dark Ronin property
|
||||
} else if (nextActionType == "DropImagination") {
|
||||
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(935, entity); // 1 Imagination powerup
|
||||
} else if (nextActionType == "DropHealth") {
|
||||
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(177, entity); // 1 Life powerup
|
||||
} else if (nextActionType == "DropArmor") {
|
||||
for (; numberAsInt > 0; numberAsInt--) SpawnDrop(6431, entity); // 1 Armor powerup
|
||||
}
|
||||
/* END Gameplay */
|
||||
else {
|
||||
static std::set<std::string> g_WarnedActions;
|
||||
if (!g_WarnedActions.contains(nextActionType.data())) {
|
||||
LOG("Tried to play action (%s) which is not supported.", nextActionType.data());
|
||||
g_WarnedActions.insert(nextActionType.data());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
IncrementAction();
|
||||
@@ -187,12 +229,67 @@ void Strip::RemoveStates(ModelComponent& modelComponent) const {
|
||||
}
|
||||
}
|
||||
|
||||
bool Strip::CheckMovement(float deltaTime, ModelComponent& modelComponent) {
|
||||
auto& entity = *modelComponent.GetParent();
|
||||
const auto& currentPos = entity.GetPosition();
|
||||
const auto diff = currentPos - m_PreviousFramePosition;
|
||||
const auto [moveX, moveY, moveZ] = m_InActionMove;
|
||||
m_PreviousFramePosition = currentPos;
|
||||
|
||||
// Only want to subtract from the move if one is being performed.
|
||||
// Starts at true because we may not be doing a move at all.
|
||||
// If one is being done, then one of the move_ variables will be non-zero
|
||||
bool moveFinished = true;
|
||||
NiPoint3 finalPositionAdjustment = NiPoint3Constant::ZERO;
|
||||
if (moveX != 0.0f) {
|
||||
m_InActionMove.x -= diff.x;
|
||||
// If the sign bit is different between the two numbers, then we have finished our move.
|
||||
moveFinished = std::signbit(m_InActionMove.x) != std::signbit(moveX);
|
||||
finalPositionAdjustment.x = m_InActionMove.x;
|
||||
} else if (moveY != 0.0f) {
|
||||
m_InActionMove.y -= diff.y;
|
||||
// If the sign bit is different between the two numbers, then we have finished our move.
|
||||
moveFinished = std::signbit(m_InActionMove.y) != std::signbit(moveY);
|
||||
finalPositionAdjustment.y = m_InActionMove.y;
|
||||
} else if (moveZ != 0.0f) {
|
||||
m_InActionMove.z -= diff.z;
|
||||
// If the sign bit is different between the two numbers, then we have finished our move.
|
||||
moveFinished = std::signbit(m_InActionMove.z) != std::signbit(moveZ);
|
||||
finalPositionAdjustment.z = m_InActionMove.z;
|
||||
}
|
||||
|
||||
// Once done, set the in action move & velocity to zero
|
||||
if (moveFinished && m_InActionMove != NiPoint3Constant::ZERO) {
|
||||
auto entityVelocity = entity.GetVelocity();
|
||||
// Zero out only the velocity that was acted on
|
||||
if (moveX != 0.0f) entityVelocity.x = 0.0f;
|
||||
else if (moveY != 0.0f) entityVelocity.y = 0.0f;
|
||||
else if (moveZ != 0.0f) entityVelocity.z = 0.0f;
|
||||
modelComponent.SetVelocity(entityVelocity);
|
||||
|
||||
// Do the final adjustment so we will have moved exactly the requested units
|
||||
entity.SetPosition(entity.GetPosition() + finalPositionAdjustment);
|
||||
m_InActionMove = NiPoint3Constant::ZERO;
|
||||
}
|
||||
|
||||
return moveFinished;
|
||||
}
|
||||
|
||||
void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
|
||||
// No point in running a strip with only one action.
|
||||
// Strips are also designed to have 2 actions or more to run.
|
||||
if (!HasMinimumActions()) return;
|
||||
|
||||
// Return if this strip has an active movement action
|
||||
if (!CheckMovement(deltaTime, modelComponent)) return;
|
||||
|
||||
// Don't run this strip if we're paused.
|
||||
m_PausedTime -= deltaTime;
|
||||
if (m_PausedTime > 0.0f) return;
|
||||
|
||||
m_PausedTime = 0.0f;
|
||||
|
||||
// Return here if we're waiting for external interactions to continue.
|
||||
if (m_WaitingForAction) return;
|
||||
|
||||
auto& entity = *modelComponent.GetParent();
|
||||
@@ -200,7 +297,7 @@ void Strip::Update(float deltaTime, ModelComponent& modelComponent) {
|
||||
|
||||
RemoveStates(modelComponent);
|
||||
|
||||
// Check for starting blocks and if not a starting block proc this blocks action
|
||||
// Check for trigger blocks and if not a trigger block proc this blocks action
|
||||
if (m_NextActionIndex == 0) {
|
||||
if (nextAction.GetType() == "OnInteract") {
|
||||
modelComponent.AddInteract();
|
||||
|
||||
@@ -29,15 +29,23 @@ public:
|
||||
|
||||
void IncrementAction();
|
||||
void Spawn(LOT object, Entity& entity);
|
||||
|
||||
// Checks the movement logic for whether or not to proceed
|
||||
// Returns true if the movement can continue, false if it needs to wait more.
|
||||
bool CheckMovement(float deltaTime, ModelComponent& modelComponent);
|
||||
void Update(float deltaTime, ModelComponent& modelComponent);
|
||||
void SpawnDrop(LOT dropLOT, Entity& entity);
|
||||
void ProcNormalAction(float deltaTime, ModelComponent& modelComponent);
|
||||
void RemoveStates(ModelComponent& modelComponent) const;
|
||||
|
||||
// 2 actions are required for strips to work
|
||||
bool HasMinimumActions() const { return m_Actions.size() >= 2; }
|
||||
private:
|
||||
// Indicates this Strip is waiting for an action to be taken upon it to progress to its actions
|
||||
bool m_WaitingForAction{ false };
|
||||
|
||||
// The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0.
|
||||
// The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0.
|
||||
// Actions that do not use time do not use this (ex. positions).
|
||||
float m_PausedTime{ 0.0f };
|
||||
|
||||
// The index of the next action to be played. This should always be within range of [0, m_Actions.size()).
|
||||
@@ -48,6 +56,13 @@ private:
|
||||
|
||||
// The location of this strip on the UGBehaviorEditor UI
|
||||
StripUiPosition m_Position;
|
||||
|
||||
// The current actions remaining distance to the target
|
||||
// Only 1 of these vertexs' will be active at once for any given strip.
|
||||
NiPoint3 m_InActionMove{};
|
||||
|
||||
// The position of the parent model on the previous frame
|
||||
NiPoint3 m_PreviousFramePosition{};
|
||||
};
|
||||
|
||||
#endif //!__STRIP__H__
|
||||
|
||||
@@ -1049,6 +1049,10 @@ namespace DEVGMCommands {
|
||||
|
||||
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
|
||||
if (entity->GetCharacter()) {
|
||||
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) {
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
}
|
||||
entity->GetCharacter()->SetZoneID(zoneID);
|
||||
entity->GetCharacter()->SetZoneInstance(zoneInstance);
|
||||
entity->GetCharacter()->SetZoneClone(zoneClone);
|
||||
|
||||
@@ -168,6 +168,10 @@ namespace GMZeroCommands {
|
||||
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", entity->GetCharacter()->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
|
||||
|
||||
if (entity->GetCharacter()) {
|
||||
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) {
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
}
|
||||
entity->GetCharacter()->SetZoneID(zoneID);
|
||||
entity->GetCharacter()->SetZoneInstance(zoneInstance);
|
||||
entity->GetCharacter()->SetZoneClone(zoneClone);
|
||||
@@ -190,6 +194,10 @@ namespace GMZeroCommands {
|
||||
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);
|
||||
|
||||
if (entity->GetCharacter()) {
|
||||
auto* characterComponent = entity->GetComponent<CharacterComponent>();
|
||||
if (characterComponent) {
|
||||
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
||||
}
|
||||
entity->GetCharacter()->SetZoneID(zoneID);
|
||||
entity->GetCharacter()->SetZoneInstance(zoneInstance);
|
||||
entity->GetCharacter()->SetZoneClone(zoneClone);
|
||||
|
||||
@@ -61,6 +61,7 @@ struct LUBitStream {
|
||||
void WriteHeader(RakNet::BitStream& bitStream) const;
|
||||
bool ReadHeader(RakNet::BitStream& bitStream);
|
||||
void Send(const SystemAddress& sysAddr) const;
|
||||
void Broadcast() const { Send(UNASSIGNED_SYSTEM_ADDRESS); };
|
||||
|
||||
virtual void Serialize(RakNet::BitStream& bitStream) const {}
|
||||
virtual bool Deserialize(RakNet::BitStream& bitStream) { return true; }
|
||||
|
||||
@@ -98,14 +98,13 @@ void ChatPackets::SendMessageFail(const SystemAddress& sysAddr) {
|
||||
SEND_PACKET;
|
||||
}
|
||||
|
||||
void ChatPackets::Announcement::Send() {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_ANNOUNCE);
|
||||
bitStream.Write<uint32_t>(title.size());
|
||||
bitStream.Write(title);
|
||||
bitStream.Write<uint32_t>(message.size());
|
||||
bitStream.Write(message);
|
||||
SEND_PACKET_BROADCAST;
|
||||
namespace ChatPackets {
|
||||
void Announcement::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write<uint32_t>(title.size());
|
||||
bitStream.Write(title);
|
||||
bitStream.Write<uint32_t>(message.size());
|
||||
bitStream.Write(message);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatPackets::AchievementNotify::Serialize(RakNet::BitStream& bitstream) const {
|
||||
|
||||
@@ -30,10 +30,12 @@ struct FindPlayerRequest{
|
||||
|
||||
namespace ChatPackets {
|
||||
|
||||
struct Announcement {
|
||||
struct Announcement : public LUBitStream {
|
||||
std::string title;
|
||||
std::string message;
|
||||
void Send();
|
||||
|
||||
Announcement() : LUBitStream(eConnectionType::CHAT, MessageType::Chat::GM_ANNOUNCE) {};
|
||||
virtual void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
};
|
||||
|
||||
struct AchievementNotify : public LUBitStream {
|
||||
|
||||
@@ -134,7 +134,7 @@ void WorldPackets::SendCreateCharacter(const SystemAddress& sysAddr, int64_t rep
|
||||
LOG("Sent CreateCharacter for ID: %llu", player);
|
||||
}
|
||||
|
||||
void WorldPackets::SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::vector<std::pair<uint8_t, uint8_t>> unacceptedItems) {
|
||||
void WorldPackets::SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set<std::pair<uint8_t, uint8_t>> unacceptedItems) {
|
||||
CBITSTREAM;
|
||||
BitStreamUtils::WriteHeader(bitStream, eConnectionType::CLIENT, MessageType::Client::CHAT_MODERATION_STRING);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace WorldPackets {
|
||||
void SendTransferToWorld(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort, bool mythranShift);
|
||||
void SendServerState(const SystemAddress& sysAddr);
|
||||
void SendCreateCharacter(const SystemAddress& sysAddr, int64_t reputation, LWOOBJID player, const std::string& xmlData, const std::u16string& username, eGameMasterLevel gm);
|
||||
void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::vector<std::pair<uint8_t, uint8_t>> unacceptedItems);
|
||||
void SendChatModerationResponse(const SystemAddress& sysAddr, bool requestAccepted, uint32_t requestID, const std::string& receiver, std::set<std::pair<uint8_t, uint8_t>> unacceptedItems);
|
||||
void SendGMLevelChange(const SystemAddress& sysAddr, bool success, eGameMasterLevel highestLevel, eGameMasterLevel prevLevel, eGameMasterLevel newLevel);
|
||||
void SendHTTPMonitorInfo(const SystemAddress& sysAddr, const HTTPMonitorInfo& info);
|
||||
void SendDebugOuput(const SystemAddress& sysAddr, const std::string& data);
|
||||
|
||||
@@ -121,7 +121,7 @@ void ActivityManager::GetLeaderboardData(Entity* self, const LWOOBJID playerID,
|
||||
auto* sac = self->GetComponent<ScriptedActivityComponent>();
|
||||
uint32_t gameID = sac != nullptr ? sac->GetActivityID() : self->GetLOT();
|
||||
// Save the new score to the leaderboard and show the leaderboard to the player
|
||||
LeaderboardManager::SendLeaderboard(activityID, Leaderboard::InfoType::MyStanding, false, playerID, self->GetObjectID());
|
||||
LeaderboardManager::SendLeaderboard(activityID, Leaderboard::InfoType::MyStanding, false, playerID, self->GetObjectID(), numResults);
|
||||
}
|
||||
|
||||
void ActivityManager::ActivityTimerStart(Entity* self, const std::string& timerName, const float_t updateInterval,
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
#include "NsRaceServer.h"
|
||||
#include "TrialFactionArmorServer.h"
|
||||
#include "ImaginationBackPack.h"
|
||||
#include "NsWinterRaceServer.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
@@ -656,6 +657,7 @@ namespace {
|
||||
//FB
|
||||
{"scripts\\ai\\NS\\WH\\L_ROCKHYDRANT_BROKEN.lua", []() {return new RockHydrantBroken();}},
|
||||
{"scripts\\ai\\NS\\L_NS_WH_FANS.lua", []() {return new WhFans();}},
|
||||
{"scripts\\ai\\RACING\\TRACK_NS_WINTER\\NS_WINTER_RACE_SERVER.lua", []() {return new NsWinterRaceServer();}},
|
||||
|
||||
//WBL
|
||||
{"scripts\\zone\\LUPs\\WBL_generic_zone.lua", []() {return new WblGenericZone();}},
|
||||
|
||||
@@ -342,7 +342,8 @@ void SGCannon::StartGame(Entity* self) {
|
||||
|
||||
auto* player = Game::entityManager->GetEntity(self->GetVar<LWOOBJID>(PlayerIDVariable));
|
||||
if (player != nullptr) {
|
||||
GetLeaderboardData(self, player->GetObjectID(), GetActivityID(self), 1);
|
||||
// The client cant accept more than 10 results.
|
||||
GetLeaderboardData(self, player->GetObjectID(), GetConstants().activityID, 10);
|
||||
LOG("Sending ActivityStart");
|
||||
GameMessages::SendActivityStart(self->GetObjectID(), player->GetSystemAddress());
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ foreach(file ${DSCRIPTS_SOURCES_AI_RACING_TRACK_NS})
|
||||
set(DSCRIPTS_SOURCES_AI_RACING ${DSCRIPTS_SOURCES_AI_RACING} "TRACK_NS/${file}")
|
||||
endforeach()
|
||||
|
||||
add_subdirectory(TRACK_NS_WINTER)
|
||||
|
||||
foreach(file ${DSCRIPTS_SOURCES_AI_RACING_TRACK_NS_WINTER})
|
||||
set(DSCRIPTS_SOURCES_AI_RACING ${DSCRIPTS_SOURCES_AI_RACING} "TRACK_NS_WINTER/${file}")
|
||||
endforeach()
|
||||
|
||||
add_subdirectory(TRACK_GF)
|
||||
|
||||
foreach(file ${DSCRIPTS_SOURCES_AI_RACING_TRACK_GF})
|
||||
@@ -26,5 +32,5 @@ foreach(file ${DSCRIPTS_SOURCES_AI_RACING_TRACK_FV})
|
||||
endforeach()
|
||||
|
||||
add_library(dScriptsAiRacing OBJECT ${DSCRIPTS_SOURCES_AI_RACING})
|
||||
target_include_directories(dScriptsAiRacing PUBLIC "." "OBJECTS" "TRACK_NS" "TRACK_GF" "TRACK_FV")
|
||||
target_include_directories(dScriptsAiRacing PUBLIC "." "OBJECTS" "TRACK_NS" "TRACK_NS_WINTER" "TRACK_GF" "TRACK_FV")
|
||||
target_precompile_headers(dScriptsAiRacing REUSE_FROM dScriptsBase)
|
||||
|
||||
3
dScripts/ai/RACING/TRACK_NS_WINTER/CMakeLists.txt
Normal file
3
dScripts/ai/RACING/TRACK_NS_WINTER/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
set(DSCRIPTS_SOURCES_AI_RACING_TRACK_NS_WINTER
|
||||
"NsWinterRaceServer.cpp"
|
||||
PARENT_SCOPE)
|
||||
53
dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp
Normal file
53
dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "NsWinterRaceServer.h"
|
||||
#include "GameMessages.h"
|
||||
#include "RacingControlComponent.h"
|
||||
|
||||
using std::unique_ptr;
|
||||
using std::make_unique;
|
||||
|
||||
void NsWinterRaceServer::OnStartup(Entity* self) {
|
||||
GameMessages::ConfigureRacingControl config;
|
||||
auto& raceSet = config.racingSettings;
|
||||
|
||||
raceSet.push_back(make_unique<LDFData<std::u16string>>("GameType", u"Racing"));
|
||||
raceSet.push_back(make_unique<LDFData<std::u16string>>("GameState", u"Starting"));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Number_Of_PlayersPerTeam", 6));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Minimum_Players_to_Start", 2));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Minimum_Players_for_Group_Achievments", 2));
|
||||
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Car_Object", 7703));
|
||||
raceSet.push_back(make_unique<LDFData<std::u16string>>("Race_PathName", u"MainPath"));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Current_Lap", 1));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Number_of_Laps", 3));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("activityID", 42));
|
||||
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Place_1", 100));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Place_2", 90));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Place_3", 80));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Place_4", 70));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Place_5", 60));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Place_6", 50));
|
||||
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_1", 15));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_2", 25));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_3", 50));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_4", 85));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_5", 90));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Num_of_Players_6", 100));
|
||||
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Number_of_Spawn_Groups", 1));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Spawners", 4847));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Spawners", 4848));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Flag", 4850));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Flag", 4851));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Point", 4846));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Point", 4845));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Red_Mark", 4844));
|
||||
raceSet.push_back(make_unique<LDFData<int32_t>>("Blue_Mark", 4843));
|
||||
|
||||
const auto racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL);
|
||||
for (auto* const racingController : racingControllers) {
|
||||
auto* const racingComponent = racingController->GetComponent<RacingControlComponent>();
|
||||
if (racingComponent) racingComponent->MsgConfigureRacingControl(config);
|
||||
}
|
||||
}
|
||||
15
dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.h
Normal file
15
dScripts/ai/RACING/TRACK_NS_WINTER/NsWinterRaceServer.h
Normal file
@@ -0,0 +1,15 @@
|
||||
// Darkflame Universe
|
||||
// Copyright 2025
|
||||
|
||||
#ifndef NSWINTERRACESERVER_H
|
||||
#define NSWINTERRACESERVER_H
|
||||
|
||||
#include "CppScripts.h"
|
||||
#include "RaceImaginationServer.h"
|
||||
|
||||
class NsWinterRaceServer : public RaceImaginationServer {
|
||||
public:
|
||||
void OnStartup(Entity* self) override;
|
||||
};
|
||||
|
||||
#endif //!NSWINTERRACESERVER_H
|
||||
7
dWeb/CMakeLists.txt
Normal file
7
dWeb/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
set(DWEB_SOURCES
|
||||
"Web.cpp")
|
||||
|
||||
add_library(dWeb STATIC ${DWEB_SOURCES})
|
||||
|
||||
target_include_directories(dWeb PUBLIC ".")
|
||||
target_link_libraries(dWeb dCommon mongoose)
|
||||
301
dWeb/Web.cpp
Normal file
301
dWeb/Web.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#include "Web.h"
|
||||
#include "Game.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "json.hpp"
|
||||
#include "Logger.h"
|
||||
#include "eHTTPMethod.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "JSONUtils.h"
|
||||
#include <ranges>
|
||||
|
||||
namespace Game {
|
||||
Web web;
|
||||
}
|
||||
|
||||
namespace {
|
||||
const char* jsonContentType = "Content-Type: application/json\r\n";
|
||||
const std::string wsSubscribed = "{\"status\":\"subscribed\"}";
|
||||
const std::string wsUnsubscribed = "{\"status\":\"unsubscribed\"}";
|
||||
std::map<std::pair<eHTTPMethod, std::string>, HTTPRoute> g_HTTPRoutes;
|
||||
std::map<std::string, WSEvent> g_WSEvents;
|
||||
std::vector<std::string> g_WSSubscriptions;
|
||||
}
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
bool ValidateAuthentication(const mg_http_message* http_msg) {
|
||||
// TO DO: This is just a placeholder for now
|
||||
// use tokens or something at a later point if we want to implement authentication
|
||||
// bit using the listen bind address to limit external access is good enough to start with
|
||||
return true;
|
||||
}
|
||||
|
||||
void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) {
|
||||
if (g_HTTPRoutes.empty()) return;
|
||||
|
||||
HTTPReply reply;
|
||||
|
||||
if (!http_msg) {
|
||||
reply.status = eHTTPStatusCode::BAD_REQUEST;
|
||||
reply.message = "{\"error\":\"Invalid Request\"}";
|
||||
} else if (ValidateAuthentication(http_msg)) {
|
||||
|
||||
// convert method from cstring to std string
|
||||
std::string method_string(http_msg->method.buf, http_msg->method.len);
|
||||
// get method from mg to enum
|
||||
const eHTTPMethod method = magic_enum::enum_cast<eHTTPMethod>(method_string).value_or(eHTTPMethod::INVALID);
|
||||
|
||||
// convert uri from cstring to std string
|
||||
std::string uri(http_msg->uri.buf, http_msg->uri.len);
|
||||
std::transform(uri.begin(), uri.end(), uri.begin(), ::tolower);
|
||||
|
||||
// convert body from cstring to std string
|
||||
std::string body(http_msg->body.buf, http_msg->body.len);
|
||||
|
||||
// Special case for websocket
|
||||
if (uri == "/ws" && method == eHTTPMethod::GET) {
|
||||
mg_ws_upgrade(connection, const_cast<mg_http_message*>(http_msg), NULL);
|
||||
LOG_DEBUG("Upgraded connection to websocket: %d.%d.%d.%d:%i", MG_IPADDR_PARTS(&connection->rem.ip), connection->rem.port);
|
||||
// return cause they are now a websocket
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle HTTP request
|
||||
const auto routeItr = g_HTTPRoutes.find({method, uri});
|
||||
if (routeItr != g_HTTPRoutes.end()) {
|
||||
const auto& [_, route] = *routeItr;
|
||||
route.handle(reply, body);
|
||||
} else {
|
||||
reply.status = eHTTPStatusCode::NOT_FOUND;
|
||||
reply.message = "{\"error\":\"Not Found\"}";
|
||||
}
|
||||
} else {
|
||||
reply.status = eHTTPStatusCode::UNAUTHORIZED;
|
||||
reply.message = "{\"error\":\"Unauthorized\"}";
|
||||
}
|
||||
mg_http_reply(connection, static_cast<int>(reply.status), jsonContentType, reply.message.c_str());
|
||||
}
|
||||
|
||||
|
||||
void HandleWSMessage(mg_connection* connection, const mg_ws_message* ws_msg) {
|
||||
if (!ws_msg) {
|
||||
LOG_DEBUG("Received invalid websocket message");
|
||||
return;
|
||||
} else {
|
||||
LOG_DEBUG("Received websocket message: %.*s", static_cast<uint32_t>(ws_msg->data.len), ws_msg->data.buf);
|
||||
auto data = GeneralUtils::TryParse<json>(std::string(ws_msg->data.buf, ws_msg->data.len));
|
||||
if (data) {
|
||||
const auto& good_data = data.value();
|
||||
auto check = JSONUtils::CheckRequiredData(good_data, { "event" });
|
||||
if (!check.empty()) {
|
||||
LOG_DEBUG("Received invalid websocket message: %s", check.c_str());
|
||||
} else {
|
||||
const auto event = good_data["event"].get<std::string>();
|
||||
const auto eventItr = g_WSEvents.find(event);
|
||||
if (eventItr != g_WSEvents.end()) {
|
||||
const auto& [_, event] = *eventItr;
|
||||
event.handle(connection, good_data);
|
||||
} else {
|
||||
LOG_DEBUG("Received invalid websocket event: %s", event.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Received invalid websocket message: %.*s", static_cast<uint32_t>(ws_msg->data.len), ws_msg->data.buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle websocket connection subscribing to an event
|
||||
void HandleWSSubscribe(mg_connection* connection, json data) {
|
||||
auto check = JSONUtils::CheckRequiredData(data, { "subscription" });
|
||||
if (!check.empty()) {
|
||||
LOG_DEBUG("Received invalid websocket message: %s", check.c_str());
|
||||
} else {
|
||||
const auto subscription = data["subscription"].get<std::string>();
|
||||
// check subscription vector
|
||||
auto subItr = std::ranges::find(g_WSSubscriptions, subscription);
|
||||
if (subItr != g_WSSubscriptions.end()) {
|
||||
// get index of subscription
|
||||
auto index = std::distance(g_WSSubscriptions.begin(), subItr);
|
||||
connection->data[index] = SubscriptionStatus::SUBSCRIBED;
|
||||
// send subscribe message
|
||||
mg_ws_send(connection, wsSubscribed.c_str(), wsSubscribed.size(), WEBSOCKET_OP_TEXT);
|
||||
LOG_DEBUG("subscription %s subscribed", subscription.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle websocket connection unsubscribing from an event
|
||||
void HandleWSUnsubscribe(mg_connection* connection, json data) {
|
||||
auto check = JSONUtils::CheckRequiredData(data, { "subscription" });
|
||||
if (!check.empty()) {
|
||||
LOG_DEBUG("Received invalid websocket message: %s", check.c_str());
|
||||
} else {
|
||||
const auto subscription = data["subscription"].get<std::string>();
|
||||
// check subscription vector
|
||||
auto subItr = std::ranges::find(g_WSSubscriptions, subscription);
|
||||
if (subItr != g_WSSubscriptions.end()) {
|
||||
// get index of subscription
|
||||
auto index = std::distance(g_WSSubscriptions.begin(), subItr);
|
||||
connection->data[index] = SubscriptionStatus::UNSUBSCRIBED;
|
||||
// send unsubscribe message
|
||||
mg_ws_send(connection, wsUnsubscribed.c_str(), wsUnsubscribed.size(), WEBSOCKET_OP_TEXT);
|
||||
LOG_DEBUG("subscription %s unsubscribed", subscription.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HandleWSGetSubscriptions(mg_connection* connection, json data) {
|
||||
// list subscribed and non subscribed subscriptions
|
||||
json response;
|
||||
// check subscription vector
|
||||
for (const auto& sub : g_WSSubscriptions) {
|
||||
auto subItr = std::ranges::find(g_WSSubscriptions, sub);
|
||||
if (subItr != g_WSSubscriptions.end()) {
|
||||
// get index of subscription
|
||||
auto index = std::distance(g_WSSubscriptions.begin(), subItr);
|
||||
if (connection->data[index] == SubscriptionStatus::SUBSCRIBED) {
|
||||
response["subscribed"].push_back(sub);
|
||||
} else {
|
||||
response["unsubscribed"].push_back(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
mg_ws_send(connection, response.dump().c_str(), response.dump().size(), WEBSOCKET_OP_TEXT);
|
||||
}
|
||||
|
||||
void HandleMessages(mg_connection* connection, int message, void* message_data) {
|
||||
if (!Game::web.IsEnabled()) return;
|
||||
switch (message) {
|
||||
case MG_EV_HTTP_MSG:
|
||||
HandleHTTPMessage(connection, static_cast<mg_http_message*>(message_data));
|
||||
break;
|
||||
case MG_EV_WS_MSG:
|
||||
HandleWSMessage(connection, static_cast<mg_ws_message*>(message_data));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect mongoose logs to our logger
|
||||
static void DLOG(char ch, void *param) {
|
||||
static char buf[256]{};
|
||||
static size_t len{};
|
||||
if (ch != '\n') buf[len++] = ch; // we provide the newline in our logger
|
||||
if (ch == '\n' || len >= sizeof(buf)) {
|
||||
LOG_DEBUG("%.*s", static_cast<int>(len), buf);
|
||||
len = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Web::RegisterHTTPRoute(HTTPRoute route) {
|
||||
if (!Game::web.enabled) {
|
||||
LOG_DEBUG("Failed to register HTTP route %s: web server not enabled", route.path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
auto [_, success] = g_HTTPRoutes.try_emplace({ route.method, route.path }, route);
|
||||
if (!success) {
|
||||
LOG_DEBUG("Failed to register HTTP route %s", route.path.c_str());
|
||||
} else {
|
||||
LOG_DEBUG("Registered HTTP route %s", route.path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Web::RegisterWSEvent(WSEvent event) {
|
||||
if (!Game::web.enabled) {
|
||||
LOG_DEBUG("Failed to register WS event %s: web server not enabled", event.name.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
auto [_, success] = g_WSEvents.try_emplace(event.name, event);
|
||||
if (!success) {
|
||||
LOG_DEBUG("Failed to register WS event %s", event.name.c_str());
|
||||
} else {
|
||||
LOG_DEBUG("Registered WS event %s", event.name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Web::RegisterWSSubscription(const std::string& subscription) {
|
||||
if (!Game::web.enabled) {
|
||||
LOG_DEBUG("Failed to register WS subscription %s: web server not enabled", subscription.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// check that subsction is not already in the vector
|
||||
auto subItr = std::ranges::find(g_WSSubscriptions, subscription);
|
||||
if (subItr != g_WSSubscriptions.end()) {
|
||||
LOG_DEBUG("Failed to register WS subscription %s: duplicate", subscription.c_str());
|
||||
} else {
|
||||
LOG_DEBUG("Registered WS subscription %s", subscription.c_str());
|
||||
g_WSSubscriptions.push_back(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
Web::Web() {
|
||||
mg_log_set_fn(DLOG, NULL); // Redirect logs to our logger
|
||||
mg_log_set(MG_LL_DEBUG);
|
||||
mg_mgr_init(&mgr); // Initialize event manager
|
||||
}
|
||||
|
||||
Web::~Web() {
|
||||
mg_mgr_free(&mgr);
|
||||
}
|
||||
|
||||
bool Web::Startup(const std::string& listen_ip, const uint32_t listen_port) {
|
||||
|
||||
// Make listen address
|
||||
const std::string listen_address = "http://" + listen_ip + ":" + std::to_string(listen_port);
|
||||
LOG("Starting web server on %s", listen_address.c_str());
|
||||
|
||||
// Create HTTP listener
|
||||
if (!mg_http_listen(&mgr, listen_address.c_str(), HandleMessages, NULL)) {
|
||||
LOG("Failed to create web server listener on %s", listen_address.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set enabled flag
|
||||
Game::web.enabled = true;
|
||||
|
||||
// Core WebSocket Events
|
||||
Game::web.RegisterWSEvent({
|
||||
.name = "subscribe",
|
||||
.handle = HandleWSSubscribe
|
||||
});
|
||||
|
||||
Game::web.RegisterWSEvent({
|
||||
.name = "unsubscribe",
|
||||
.handle = HandleWSUnsubscribe
|
||||
});
|
||||
|
||||
Game::web.RegisterWSEvent({
|
||||
.name = "getSubscriptions",
|
||||
.handle = HandleWSGetSubscriptions
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Web::ReceiveRequests() {
|
||||
mg_mgr_poll(&mgr, 15);
|
||||
}
|
||||
|
||||
void Web::SendWSMessage(const std::string subscription, json& data) {
|
||||
if (!Game::web.enabled) return; // don't attempt to send if web is not enabled
|
||||
|
||||
// find subscription
|
||||
auto subItr = std::ranges::find(g_WSSubscriptions, subscription);
|
||||
if (subItr == g_WSSubscriptions.end()) {
|
||||
LOG_DEBUG("Failed to send WS message: subscription %s not found", subscription.c_str());
|
||||
return;
|
||||
}
|
||||
// tell it the event type
|
||||
data["event"] = subscription;
|
||||
auto index = std::distance(g_WSSubscriptions.begin(), subItr);
|
||||
for (auto *wc = Game::web.mgr.conns; wc != NULL; wc = wc->next) {
|
||||
if (wc->is_websocket && wc->data[index] == SubscriptionStatus::SUBSCRIBED) {
|
||||
mg_ws_send(wc, data.dump().c_str(), data.dump().size(), WEBSOCKET_OP_TEXT);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
dWeb/Web.h
Normal file
82
dWeb/Web.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#ifndef __WEB_H__
|
||||
#define __WEB_H__
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include "mongoose.h"
|
||||
#include "json_fwd.hpp"
|
||||
#include "eHTTPStatusCode.h"
|
||||
|
||||
// Forward declarations for game namespace
|
||||
// so that we can access the data anywhere
|
||||
class Web;
|
||||
namespace Game {
|
||||
extern Web web;
|
||||
}
|
||||
|
||||
enum class eHTTPMethod;
|
||||
|
||||
// Forward declaration for mongoose manager
|
||||
typedef struct mg_mgr mg_mgr;
|
||||
|
||||
// For passing HTTP messages between functions
|
||||
struct HTTPReply {
|
||||
eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND;
|
||||
std::string message = "{\"error\":\"Not Found\"}";
|
||||
};
|
||||
|
||||
// HTTP route structure
|
||||
// This structure is used to register HTTP routes
|
||||
// with the server. Each route has a path, method, and a handler function
|
||||
// that will be called when the route is matched.
|
||||
struct HTTPRoute {
|
||||
std::string path;
|
||||
eHTTPMethod method;
|
||||
std::function<void(HTTPReply&, const std::string&)> handle;
|
||||
};
|
||||
|
||||
// WebSocket event structure
|
||||
// This structure is used to register WebSocket events
|
||||
// with the server. Each event has a name and a handler function
|
||||
// that will be called when the event is triggered.
|
||||
struct WSEvent {
|
||||
std::string name;
|
||||
std::function<void(mg_connection*, nlohmann::json)> handle;
|
||||
};
|
||||
|
||||
// Subscription status for WebSocket clients
|
||||
enum SubscriptionStatus {
|
||||
UNSUBSCRIBED = 0,
|
||||
SUBSCRIBED = 1
|
||||
};
|
||||
|
||||
class Web {
|
||||
public:
|
||||
// Constructor
|
||||
Web();
|
||||
// Destructor
|
||||
~Web();
|
||||
// Handle incoming messages
|
||||
void ReceiveRequests();
|
||||
// Start the web server
|
||||
// Returns true if the server started successfully
|
||||
bool Startup(const std::string& listen_ip, const uint32_t listen_port);
|
||||
// Register HTTP route to be handled by the server
|
||||
void RegisterHTTPRoute(HTTPRoute route);
|
||||
// Register WebSocket event to be handled by the server
|
||||
void RegisterWSEvent(WSEvent event);
|
||||
// Register WebSocket subscription to be handled by the server
|
||||
void RegisterWSSubscription(const std::string& subscription);
|
||||
// Returns if the web server is enabled
|
||||
bool IsEnabled() const { return enabled; };
|
||||
// Send a message to all connected WebSocket clients that are subscribed to the given topic
|
||||
void static SendWSMessage(std::string sub, nlohmann::json& message);
|
||||
private:
|
||||
// mongoose manager
|
||||
mg_mgr mgr;
|
||||
// If the web server is enabled
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
#endif // !__WEB_H__
|
||||
@@ -713,12 +713,12 @@ void HandleMasterPacket(Packet* packet) {
|
||||
//Create our user and send them in:
|
||||
UserManager::Instance()->CreateUser(it->second.sysAddr, username.GetAsString(), userHash);
|
||||
|
||||
auto zone = Game::zoneManager->GetZone();
|
||||
if (zone) {
|
||||
if (Game::zoneManager->HasZone()) {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
|
||||
auto zone = Game::zoneManager->GetZone();
|
||||
if (zone->GetZoneID().GetMapID() == 1100) {
|
||||
auto pos = zone->GetSpawnPos();
|
||||
x = pos.x;
|
||||
@@ -1045,7 +1045,7 @@ void HandlePacket(Packet* packet) {
|
||||
|
||||
const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID());
|
||||
|
||||
Game::entityManager->ConstructEntity(player, UNASSIGNED_SYSTEM_ADDRESS, true);
|
||||
Game::entityManager->ConstructEntity(player, UNASSIGNED_SYSTEM_ADDRESS);
|
||||
|
||||
if (respawnPoint != NiPoint3Constant::ZERO) {
|
||||
GameMessages::SendPlayerReachedRespawnCheckpoint(player, respawnPoint, NiQuaternionConstant::IDENTITY);
|
||||
@@ -1327,7 +1327,7 @@ void HandlePacket(Packet* packet) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<uint8_t, uint8_t>> segments = Game::chatFilter->IsSentenceOkay(request.message, entity->GetGMLevel(), !(isBestFriend && request.chatLevel == 1));
|
||||
const auto segments = Game::chatFilter->IsSentenceOkay(request.message, entity->GetGMLevel(), !(isBestFriend && request.chatLevel == 1));
|
||||
|
||||
bool bAllClean = segments.empty();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
/* Gets a pointer to the currently loaded zone. */
|
||||
Zone* GetZoneMut() const;
|
||||
const Zone* GetZone() const { return GetZoneMut(); };
|
||||
bool HasZone() const { return m_pZone != nullptr; };
|
||||
void LoadZone(const LWOZONEID& zoneID); //Discard the current zone (if any) and loads a new zone.
|
||||
|
||||
/* Adds a spawner to the zone with the specified ID. */
|
||||
|
||||
131
docs/ChatWSAPI.yaml
Normal file
131
docs/ChatWSAPI.yaml
Normal file
@@ -0,0 +1,131 @@
|
||||
asyncapi: 3.0.0
|
||||
info:
|
||||
title: Darkflame Chat Server WebSocket API
|
||||
version: 1.0.0
|
||||
description: API documentation for Darkflame Chat Server WebSocket endpoints
|
||||
servers:
|
||||
production:
|
||||
host: 'localhost:2005'
|
||||
pathname: /ws
|
||||
protocol: http
|
||||
description: Address of the websocket for the chat server
|
||||
channels:
|
||||
subscribe:
|
||||
address: subscribe
|
||||
messages:
|
||||
subscribe.message:
|
||||
title: Subscribe
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: '#/components/schemas/Subscription'
|
||||
unsubscribe:
|
||||
address: unsubscribe
|
||||
messages:
|
||||
unsubscribe.message:
|
||||
title: Unsubscribe
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: '#/components/schemas/Subscription'
|
||||
getSubscriptions:
|
||||
address: getSubscriptions
|
||||
messages:
|
||||
getSubscriptions.message:
|
||||
title: Get Subscriptions
|
||||
contentType: application/json
|
||||
payload:
|
||||
type: object
|
||||
properties:
|
||||
subscriptions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: player
|
||||
player:
|
||||
address: player
|
||||
messages:
|
||||
player.message:
|
||||
title: Player
|
||||
contentType: application/json
|
||||
payload:
|
||||
$ref: '#/components/schemas/PlayerUpdate'
|
||||
operations:
|
||||
subscribe:
|
||||
action: receive
|
||||
channel:
|
||||
$ref: '#/channels/subscribe'
|
||||
summary: Subscribe to an event
|
||||
messages:
|
||||
- $ref: '#/channels/subscribe/messages/subscribe.message'
|
||||
unsubscribe:
|
||||
action: receive
|
||||
channel:
|
||||
$ref: '#/channels/unsubscribe'
|
||||
summary: Unsubscribe from an event
|
||||
messages:
|
||||
- $ref: '#/channels/unsubscribe/messages/unsubscribe.message'
|
||||
getSubscriptions:
|
||||
action: receive
|
||||
channel:
|
||||
$ref: '#/channels/getSubscriptions'
|
||||
summary: Get the list of subscriptions
|
||||
messages:
|
||||
- $ref: '#/channels/getSubscriptions/messages/getSubscriptions.message'
|
||||
player:
|
||||
action: send
|
||||
channel:
|
||||
$ref: '#/channels/player'
|
||||
summary: Player event
|
||||
messages:
|
||||
- $ref: '#/channels/player/messages/player.message'
|
||||
components:
|
||||
schemas:
|
||||
PlayerUpdate:
|
||||
type: object
|
||||
properties:
|
||||
player_data:
|
||||
$ref: '#/components/schemas/Player'
|
||||
update_type:
|
||||
type: string
|
||||
example: JOIN
|
||||
Subscription:
|
||||
type: object
|
||||
required:
|
||||
- subscription
|
||||
properties:
|
||||
subscription:
|
||||
type: string
|
||||
example: player
|
||||
Player:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1152921508901824000
|
||||
gm_level:
|
||||
type: integer
|
||||
format: uint8
|
||||
example: 0
|
||||
name:
|
||||
type: string
|
||||
example: thisisatestname
|
||||
muted:
|
||||
type: boolean
|
||||
example: false
|
||||
zone_id:
|
||||
$ref: '#/components/schemas/ZoneID'
|
||||
ZoneID:
|
||||
type: object
|
||||
properties:
|
||||
map_id:
|
||||
type: integer
|
||||
format: uint16
|
||||
example: 1200
|
||||
instance_id:
|
||||
type: integer
|
||||
format: uint16
|
||||
example: 2
|
||||
clone_id:
|
||||
type: integer
|
||||
format: uint32
|
||||
example: 0
|
||||
Reference in New Issue
Block a user