Compare commits

...

9 Commits

Author SHA1 Message Date
d532a9b063 It works (kinda) now to actually implement things 2025-10-11 00:02:31 -05:00
5453d163a3 WIP 2025-10-06 19:15:55 -05:00
David Markowitz
62ac65c520 feat: Mission Component debug (#1901)
* feat: Mission Component debug

* Add player argument to inspect command

* Add completion details

* Remove unlocalized server string

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

* switched to and updated GetRandomElement

* Update config option check

* implement cached config values for mute settings and update handlers

* Address review

* Update dGame/dComponents/PetComponent.cpp

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

* Update dGame/dComponents/PetComponent.cpp

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

* reduce if argument chain

---------

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

hahahahahahahahahahwwwwwwwwwwwwwwww草

* Enhance hit detection with proximity object checks

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

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

Update EntityManager.cpp

* fix log msg
2025-10-04 17:25:23 -07:00
David Markowitz
69b1a694a6 fix: ignore foreign key checks more (#1895)
fixes an issue if you delete users in an earlier build of dlu.
2025-10-04 13:57:16 -05:00
36 changed files with 3701 additions and 96 deletions

View File

@@ -89,6 +89,7 @@ elseif(MSVC)
add_compile_options("/wd4267" "/utf-8" "/volatile:iso" "/Zc:inline")
elseif(WIN32)
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
add_compile_definitions(NOMINMAX)
endif()
# Our output dir
@@ -253,6 +254,7 @@ include_directories(
"thirdparty/MD5"
"thirdparty/nlohmann"
"thirdparty/mongoose"
"thirdparty/inja"
)
# Add system specfic includes for Apple, Windows and Other Unix OS' (including Linux)
@@ -322,6 +324,7 @@ endif()
add_subdirectory(dWorldServer)
add_subdirectory(dAuthServer)
add_subdirectory(dChatServer)
add_subdirectory(dDashboardServer)
add_subdirectory(dMasterServer) # Add MasterServer last so it can rely on the other binaries
target_precompile_headers(

View File

@@ -26,12 +26,14 @@ void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) {
const json data = Game::playerContainer;
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump();
reply.contentType = ContentType::JSON;
}
void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) {
const json data = TeamContainer::GetTeamContainer();
reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK;
reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump();
reply.contentType = ContentType::JSON;
}
void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
@@ -39,6 +41,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
if (!data) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = "{\"error\":\"Invalid JSON\"}";
reply.contentType = ContentType::JSON;
return;
}
@@ -47,6 +50,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
if (!check.empty()) {
reply.status = eHTTPStatusCode::BAD_REQUEST;
reply.message = check;
reply.contentType = ContentType::JSON;
} else {
ChatPackets::Announcement announcement;
@@ -56,6 +60,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) {
reply.status = eHTTPStatusCode::OK;
reply.message = "{\"status\":\"Announcement Sent\"}";
reply.contentType = ContentType::JSON;
}
}

View File

@@ -3,7 +3,7 @@
// C++
#include <charconv>
#include <cstdint>
#include <cmath>
#include <cmath>
#include <ctime>
#include <functional>
#include <optional>
@@ -19,6 +19,7 @@
#include "dPlatforms.h"
#include "Game.h"
#include "Logger.h"
#include "DluAssert.h"
#include <glm/ext/vector_float3.hpp>
@@ -305,7 +306,7 @@ namespace GeneralUtils {
template<typename Container>
inline Container::value_type GetRandomElement(const Container& container) {
DluAssert(!container.empty());
return container[GenerateRandomNumber<typename Container::value_type>(0, container.size() - 1)];
return container[GenerateRandomNumber<typename Container::size_type>(0, container.size() - 1)];
}
/**

View File

@@ -50,7 +50,10 @@ enum class eMissionState : int {
/**
* The mission has been completed before and has now been completed again. Used for daily missions.
*/
COMPLETE_READY_TO_COMPLETE = 12
COMPLETE_READY_TO_COMPLETE = 12,
// The mission is failed (don't know where this is used)
FAILED = 16,
};
#endif //!__MISSIONSTATE__H__

View File

@@ -0,0 +1,8 @@
set(DDASHBOARDSERVER_SOURCES
"DashboardWeb.cpp"
)
add_executable(DashboardServer "DashboardServer.cpp" "DashboardWeb.cpp")
target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dServer dWeb)
target_include_directories(DashboardServer PRIVATE ${PROJECT_SOURCE_DIR}/dServer ${PROJECT_SOURCE_DIR}/dWeb)
add_compile_definitions(DashboardServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}\"")

View File

@@ -0,0 +1,182 @@
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
//DLU Includes:
#include "dCommonVars.h"
#include "dServer.h"
#include "Logger.h"
#include "Database.h"
#include "dConfig.h"
#include "Diagnostics.h"
#include "AssetManager.h"
#include "BinaryPathFinder.h"
#include "ServiceType.h"
#include "StringifiedEnum.h"
#include "Game.h"
#include "Server.h"
//RakNet includes:
#include "RakNetDefines.h"
#include "MessageIdentifiers.h"
#include "DashboardWeb.h"
namespace Game {
Logger* logger = nullptr;
dServer* server = nullptr;
dConfig* config = nullptr;
AssetManager* assetManager = nullptr;
Game::signal_t lastSignal = 0;
std::mt19937 randomEngine;
}
int main(int argc, char** argv) {
constexpr uint32_t dashboardFramerate = mediumFramerate;
constexpr uint32_t dashboardFrameDelta = mediumFrameDelta;
Diagnostics::SetProcessName("Dashboard");
Diagnostics::SetProcessFileName(argv[0]);
Diagnostics::Initialize();
std::signal(SIGINT, Game::OnSignal);
std::signal(SIGTERM, Game::OnSignal);
Game::config = new dConfig("dashboardconfig.ini");
//Create all the objects we need to run our service:
Server::SetupLogger("DashboardServer");
if (!Game::logger) return EXIT_FAILURE;
Game::config->LogSettings();
//Read our config:
LOG("Starting Dashboard server...");
LOG("Version: %s", PROJECT_VERSION);
LOG("Compiled on: %s", __TIMESTAMP__);
try {
std::string clientPathStr = Game::config->GetValue("client_location");
if (clientPathStr.empty()) clientPathStr = "./res";
std::filesystem::path clientPath = std::filesystem::path(clientPathStr);
if (clientPath.is_relative()) {
clientPath = BinaryPathFinder::GetBinaryDir() / clientPath;
}
Game::assetManager = new AssetManager(clientPath);
} catch (std::runtime_error& ex) {
LOG("Got an error while setting up assets: %s", ex.what());
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
}
//Connect to the Database
try {
Database::Connect();
} catch (std::exception& ex) {
LOG("Got an error while connecting to the database: %s", ex.what());
Database::Destroy("DashboardServer");
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
}
// setup the chat api web server
const uint32_t web_server_port = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("web_server_port")).value_or(80);
if (!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("DashboardServer");
delete Game::logger;
delete Game::config;
return EXIT_FAILURE;
}
DashboardWeb::RegisterRoutes();
//Find out the master's IP:
std::string masterIP;
uint32_t masterPort = 1000;
std::string masterPassword;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
masterPassword = masterInfo->password;
}
//It's safe to pass 'localhost' here, as the IP is only used as the external IP.
std::string ourIP = "localhost";
const uint32_t maxClients = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("max_clients")).value_or(999);
const uint32_t ourPort = GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("dashboard_server_port")).value_or(2006);
const auto externalIPString = Game::config->GetValue("external_ip");
if (!externalIPString.empty()) ourIP = externalIPString;
Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServiceType::COMMON, Game::config, &Game::lastSignal, masterPassword);
Game::randomEngine = std::mt19937(time(0));
//Run it until server gets a kill message from Master:
auto t = std::chrono::high_resolution_clock::now();
Packet* packet = nullptr;
constexpr uint32_t logFlushTime = 30 * dashboardFramerate; // 30 seconds in frames
constexpr uint32_t sqlPingTime = 10 * 60 * dashboardFramerate; // 10 minutes in frames
uint32_t framesSinceLastFlush = 0;
uint32_t framesSinceMasterDisconnect = 0;
uint32_t framesSinceLastSQLPing = 0;
auto lastTime = std::chrono::high_resolution_clock::now();
Game::logger->Flush(); // once immediately before main loop
while (!Game::ShouldShutdown()) {
// Check if we're still connected to master:
if (!Game::server->GetIsConnectedToMaster()) {
framesSinceMasterDisconnect++;
if (framesSinceMasterDisconnect >= dashboardFramerate)
break; //Exit our loop, shut down.
} else framesSinceMasterDisconnect = 0;
const auto currentTime = std::chrono::high_resolution_clock::now();
const float deltaTime = std::chrono::duration<float>(currentTime - lastTime).count();
lastTime = currentTime;
// Check and handle web requests:
Game::web.ReceiveRequests();
//Push our log every 30s:
if (framesSinceLastFlush >= logFlushTime) {
Game::logger->Flush();
framesSinceLastFlush = 0;
} else framesSinceLastFlush++;
//Every 10 min we ping our sql server to keep it alive hopefully:
if (framesSinceLastSQLPing >= sqlPingTime) {
//Find out the master's IP for absolutely no reason:
std::string masterIP;
uint32_t masterPort;
auto masterInfo = Database::Get()->GetMasterInfo();
if (masterInfo) {
masterIP = masterInfo->ip;
masterPort = masterInfo->port;
}
framesSinceLastSQLPing = 0;
} else framesSinceLastSQLPing++;
//Sleep our thread since auth can afford to.
t += std::chrono::milliseconds(dashboardFrameDelta); //Chat can run at a lower "fps"
std::this_thread::sleep_until(t);
}
//Delete our objects here:
Database::Destroy("DashboardServer");
delete Game::server;
delete Game::logger;
delete Game::config;
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,59 @@
#include "DashboardWeb.h"
// thanks bill gates
#ifdef _WIN32
#undef min
#undef max
#endif
#include "inja.hpp"
#include "eHTTPMethod.h"
// simple home page with inja
void HandleHTTPHomeRequest(HTTPReply& reply, std::string body) {
try {
inja::Environment env;
env.set_trim_blocks(true);
env.set_lstrip_blocks(true);
nlohmann::json data;
data["title"] = "Darkflame Universe Dashboard";
data["header"] = "Welcome to the Darkflame Universe Dashboard";
data["message"] = "This is a simple dashboard page served using Inja templating engine.";
const std::string template_str = R"(
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>{{ header }}</h1>
<p>{{ message }}</p>
</body>
</html>
)";
std::string rendered = env.render(template_str, data);
reply.message = rendered;
reply.status = eHTTPStatusCode::OK;
reply.contentType = ContentType::HTML;
} catch (const std::exception& e) {
reply.status = eHTTPStatusCode::INTERNAL_SERVER_ERROR;
reply.message = "Internal Server Error";
reply.contentType = ContentType::PLAIN;
}
}
namespace DashboardWeb {
void RegisterRoutes() {
Game::web.RegisterHTTPRoute({
.path = "/",
.method = eHTTPMethod::GET,
.handle = HandleHTTPHomeRequest
});
}
}

View File

@@ -0,0 +1,11 @@
#ifndef __DASHBOARDWEB_H__
#define __DASHBOARDWEB_H__
#include "Web.h"
namespace DashboardWeb {
void RegisterRoutes();
};
#endif // __DASHBOARDWEB_H__

View File

@@ -2247,6 +2247,7 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
response.Insert("objectID", std::to_string(m_ObjectID));
response.Insert("serverInfo", true);
GameMessages::GetObjectReportInfo info{};
info.bVerbose = requestInfo.bVerbose;
info.info = response.InsertArray("data");
auto& objectInfo = info.info->PushDebug("Object Details");
auto* table = CDClientManager::GetTable<CDObjectsTable>();
@@ -2260,14 +2261,14 @@ bool Entity::MsgRequestServerObjectInfo(GameMessages::GameMsg& msg) {
auto& componentDetails = objectInfo.PushDebug("Component Information");
for (const auto [id, component] : m_Components) {
componentDetails.PushDebug<AMFStringValue>(StringifiedEnum::ToString(id)) = "";
componentDetails.PushDebug(StringifiedEnum::ToString(id));
}
auto& configData = objectInfo.PushDebug("Config Data");
for (const auto config : m_Settings) {
configData.PushDebug<AMFStringValue>(GeneralUtils::UTF16ToWTF8(config->GetKey())) = config->GetValueAsString();
}
HandleMsg(info);
auto* client = Game::entityManager->GetEntity(requestInfo.clientId);

View File

@@ -87,6 +87,8 @@ void EntityManager::ReloadConfig() {
auto hcXpReduction = Game::config->GetValue("hardcore_uscore_reduction");
m_HardcoreUscoreReduction = hcXpReduction.empty() ? 1.0f : GeneralUtils::TryParse<float>(hcXpReduction).value_or(1.0f);
m_HardcoreMode = GetHardcoreDisabledWorlds().contains(Game::zoneManager->GetZoneID().GetMapID()) ? false : m_HardcoreMode;
auto hcCoinKeep = Game::config->GetValue("hardcore_coin_keep");
m_HardcoreCoinKeep = hcCoinKeep.empty() ? false : GeneralUtils::TryParse<float>(hcCoinKeep).value_or(0.0f);
}
void EntityManager::Initialize() {

View File

@@ -81,6 +81,7 @@ public:
const std::set<LOT>& GetHardcoreUscoreReducedLots() const { return m_HardcoreUscoreReducedLots; };
const std::set<LOT>& GetHardcoreUscoreExcludedEnemies() const { return m_HardcoreUscoreExcludedEnemies; };
const std::set<LWOMAPID>& GetHardcoreDisabledWorlds() const { return m_HardcoreDisabledWorlds; };
float GetHardcoreCoinKeep() const { return m_HardcoreCoinKeep; }
// Messaging
bool SendMessage(GameMessages::GameMsg& msg) const;
@@ -125,6 +126,7 @@ private:
std::set<LOT> m_HardcoreUscoreReducedLots{};
std::set<LOT> m_HardcoreUscoreExcludedEnemies{};
std::set<LWOMAPID> m_HardcoreDisabledWorlds{};
float m_HardcoreCoinKeep{};
};
#endif // ENTITYMANAGER_H

View File

@@ -30,6 +30,7 @@
#include "BitStreamUtils.h"
#include "CheatDetection.h"
#include "CharacterComponent.h"
#include "dConfig.h"
#include "eCharacterVersion.h"
UserManager* UserManager::m_Address = nullptr;
@@ -92,6 +93,23 @@ void UserManager::Initialize() {
StripCR(line);
m_PreapprovedNames.push_back(line);
}
// Initialize cached config values and register a handler to update them on config reload
// This avoids repeated lookups into dConfig at runtime.
if (Game::config) {
m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
Game::config->AddConfigHandler([this]() {
this->m_MuteAutoRejectNames = (Game::config->GetValue("mute_auto_reject_names") == "1");
this->m_MuteRestrictTrade = (Game::config->GetValue("mute_restrict_trade") == "1");
this->m_MuteRestrictMail = (Game::config->GetValue("mute_restrict_mail") == "1");
});
}
else {
LOG("Warning: dConfig not initialized before UserManager. Cached config values will not be available.");
}
}
UserManager::~UserManager() {
@@ -301,7 +319,9 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
inStream.Read(eyes);
inStream.Read(mouth);
const auto name = LUWStringName.GetAsString();
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
const auto name = autoRejectNames ? "" : LUWStringName.GetAsString();
std::string predefinedName = GetPredefinedName(firstNameIndex, middleNameIndex, lastNameIndex);
LOT shirtLOT = FindCharShirtID(shirtColor, shirtStyle);
@@ -319,6 +339,10 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
return;
}
if (autoRejectNames) {
LOG("AccountID: %i is muted, forcing use of predefined name", u->GetAccountID());
}
if (name.empty()) {
LOG("AccountID: %i is creating a character with predefined name: %s", u->GetAccountID(), predefinedName.c_str());
} else {
@@ -369,6 +393,7 @@ void UserManager::CreateCharacter(const SystemAddress& sysAddr, Packet* packet)
//Check to see if our name was pre-approved:
bool nameOk = IsNamePreapproved(name);
if (!nameOk && u->GetMaxGMLevel() > eGameMasterLevel::FORUM_MODERATOR) nameOk = true;
// If predefined name is invalid, change it to be their object id
@@ -448,9 +473,10 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
LUWString LUWStringName;
inStream.Read(LUWStringName);
const auto newName = LUWStringName.GetAsString();
auto newName = LUWStringName.GetAsString();
Character* character = nullptr;
const bool autoRejectNames = this->GetMuteAutoRejectNames() && u->GetIsMuted();
//Check if this user has this character:
bool ownsCharacter = CheatDetection::VerifyLwoobjidIsSender(
@@ -471,13 +497,30 @@ void UserManager::RenameCharacter(const SystemAddress& sysAddr, Packet* packet)
if (!ownsCharacter || !character) {
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::UNKNOWN_ERROR);
} else if (ownsCharacter && character) {
if (autoRejectNames) {
// Create a random preapproved name (fallback to default if none available)
if (!m_FirstNames.empty() && !m_MiddleNames.empty() && !m_LastNames.empty()) {
std::string firstName = GeneralUtils::GetRandomElement(m_FirstNames);
std::string middleName = GeneralUtils::GetRandomElement(m_MiddleNames);
std::string lastName = GeneralUtils::GetRandomElement(m_LastNames);
newName = firstName + middleName + lastName;
} else {
newName = "character" + std::to_string(objectID);
}
}
if (newName == character->GetName()) {
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::NAME_UNAVAILABLE);
return;
}
if (!Database::Get()->GetCharacterInfo(newName)) {
if (IsNamePreapproved(newName)) {
if (autoRejectNames) {
Database::Get()->SetCharacterName(objectID, newName);
LOG("Character %s auto-renamed to preapproved name %s due to mute", character->GetName().c_str(), newName.c_str());
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);
UserManager::RequestCharacterList(sysAddr);
} else if (IsNamePreapproved(newName)) {
Database::Get()->SetCharacterName(objectID, newName);
LOG("Character %s now known as %s", character->GetName().c_str(), newName.c_str());
WorldPackets::SendCharacterRenameResponse(sysAddr, eRenameResponse::SUCCESS);

View File

@@ -41,6 +41,11 @@ public:
size_t GetUserCount() const { return m_Users.size(); }
// Access cached config values
bool GetMuteAutoRejectNames() const { return m_MuteAutoRejectNames; }
bool GetMuteRestrictTrade() const { return m_MuteRestrictTrade; }
bool GetMuteRestrictMail() const { return m_MuteRestrictMail; }
private:
static UserManager* m_Address; //Singleton
std::map<SystemAddress, User*> m_Users;
@@ -50,6 +55,11 @@ private:
std::vector<std::string> m_MiddleNames;
std::vector<std::string> m_LastNames;
std::vector<std::string> m_PreapprovedNames;
// Cached config values that can change on config reload
bool m_MuteAutoRejectNames = false;
bool m_MuteRestrictTrade = false;
bool m_MuteRestrictMail = false;
};
#endif // USERMANAGER_H

View File

@@ -70,7 +70,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
for (const auto zoneID : m_VisitedLevels) {
std::stringstream sstream;
sstream << "MapID: " << zoneID.GetMapID() << " CloneID: " << zoneID.GetCloneID();
vl.PushDebug<AMFStringValue>(sstream.str()) = "";
vl.PushDebug(sstream.str());
}
// visited locations
@@ -95,7 +95,7 @@ bool CharacterComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
const int32_t flagId = base + i;
std::stringstream stream;
stream << "Flag: " << flagId;
allFlags.PushDebug<AMFStringValue>(stream.str()) = "";
allFlags.PushDebug(stream.str());
}
flagChunkCopy >>= 1;
}

View File

@@ -786,7 +786,7 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType
}
} else {
//Check if this zone allows coin drops
if (Game::zoneManager->GetPlayerLoseCoinOnDeath()) {
if (Game::zoneManager->GetPlayerLoseCoinOnDeath() && !Game::entityManager->GetHardcoreMode()) {
auto* character = m_Parent->GetCharacter();
uint64_t coinsTotal = character->GetCoins();
const uint64_t minCoinsToLose = Game::zoneManager->GetWorldConfig().coinsLostOnDeathMin;
@@ -1012,13 +1012,23 @@ void DestroyableComponent::DoHardcoreModeDrops(const LWOOBJID source) {
//get character:
auto* chars = m_Parent->GetCharacter();
if (chars) {
auto coins = chars->GetCoins();
auto oldCoins = chars->GetCoins();
// Floor this so there arent coins generated from rounding
auto coins = static_cast<uint64_t>(oldCoins * Game::entityManager->GetHardcoreCoinKeep());
auto coinsToDrop = oldCoins - coins;
LOG("Player had %llu coins, will lose %i coins to have %i", oldCoins, coinsToDrop, coins);
//lose all coins:
chars->SetCoins(0, eLootSourceType::NONE);
chars->SetCoins(coins, eLootSourceType::NONE);
//drop all coins:
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, coins, m_Parent->GetPosition());
constexpr auto MAX_TO_DROP_PER_GM = 100'000;
while (coinsToDrop > MAX_TO_DROP_PER_GM) {
LOG("Dropping 100,000, %llu left", coinsToDrop);
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, MAX_TO_DROP_PER_GM, m_Parent->GetPosition());
coinsToDrop -= MAX_TO_DROP_PER_GM;
}
GameMessages::SendDropClientLoot(m_Parent, source, LOT_NULL, coinsToDrop, m_Parent->GetPosition());
}
return;
}

View File

@@ -27,7 +27,10 @@ std::unordered_map<AchievementCacheKey, std::vector<uint32_t>> MissionComponent:
//! Initializer
MissionComponent::MissionComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
using namespace GameMessages;
m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue();
RegisterMsg<GetObjectReportInfo>(this, &MissionComponent::OnGetObjectReportInfo);
}
//! Destructor
@@ -622,3 +625,111 @@ void MissionComponent::ResetMission(const int32_t missionId) {
m_Missions.erase(missionId);
GameMessages::SendResetMissions(m_Parent, m_Parent->GetSystemAddress(), missionId);
}
void PushMissions(const std::map<uint32_t, Mission*>& missions, AMFArrayValue& V, bool verbose) {
for (const auto& [id, mission] : missions) {
std::stringstream ss;
if (!mission) {
ss << "Mission ID: " << id;
V.PushDebug(ss.str());
} else if (!verbose) {
ss << "%[Missions_" << id << "_name]" << ", Mission ID";
V.PushDebug<AMFIntValue>(ss.str()) = id;
} else {
ss << "%[Missions_" << id << "_name]" << ", Mission ID: " << id;
auto& missionV = V.PushDebug(ss.str());
auto& missionInformation = missionV.PushDebug("Mission Information");
if (mission->IsComplete()) {
missionInformation.PushDebug<AMFStringValue>("Time mission last completed") = std::to_string(mission->GetTimestamp());
missionInformation.PushDebug<AMFIntValue>("Number of times completed") = mission->GetCompletions();
}
// Expensive to network this especially when its read from the client anyways
// missionInformation.PushDebug("Description").PushDebug("None");
// missionInformation.PushDebug("Text").PushDebug("None");
auto& statusInfo = missionInformation.PushDebug("Mission statuses for local player");
if (mission->IsAvalible()) statusInfo.PushDebug("Available");
if (mission->IsActive()) statusInfo.PushDebug("Active");
if (mission->IsReadyToComplete()) statusInfo.PushDebug("Ready To Complete");
if (mission->IsComplete()) statusInfo.PushDebug("Completed");
if (mission->IsFailed()) statusInfo.PushDebug("Failed");
const auto& clientInfo = mission->GetClientInfo();
statusInfo.PushDebug<AMFBoolValue>("Is an achievement mission") = mission->IsAchievement();
statusInfo.PushDebug<AMFBoolValue>("Is an timed mission") = clientInfo.time_limit > 0;
auto& taskInfo = statusInfo.PushDebug("Task Info");
taskInfo.PushDebug<AMFIntValue>("Number of tasks in this mission") = mission->GetTasks().size();
int32_t i = 0;
for (const auto* task : mission->GetTasks()) {
auto& thisTask = taskInfo.PushDebug("Task " + std::to_string(i));
// Expensive to network this especially when its read from the client anyways
// thisTask.PushDebug("Description").PushDebug("%[MissionTasks_" + taskUidStr + "_description]");
thisTask.PushDebug<AMFIntValue>("Number done") = std::min(task->GetProgress(), static_cast<uint32_t>(task->GetClientInfo().targetValue));
thisTask.PushDebug<AMFIntValue>("Number total needed") = task->GetClientInfo().targetValue;
thisTask.PushDebug<AMFIntValue>("Task Type") = task->GetClientInfo().taskType;
i++;
}
// auto& chatText = missionInformation.PushDebug("Chat Text for Mission States");
// Expensive to network this especially when its read from the client anyways
// chatText.PushDebug("Available Text").PushDebug("%[MissionText_" + idStr + "_chat_state_1]");
// chatText.PushDebug("Active Text").PushDebug("%[MissionText_" + idStr + "_chat_state_2]");
// chatText.PushDebug("Ready-to-Complete Text").PushDebug("%[MissionText_" + idStr + "_chat_state_3]");
// chatText.PushDebug("Complete Text").PushDebug("%[MissionText_" + idStr + "_chat_state_4]");
if (clientInfo.time_limit > 0) {
missionInformation.PushDebug<AMFIntValue>("Time Limit") = clientInfo.time_limit;
missionInformation.PushDebug<AMFDoubleValue>("Time Remaining") = 0;
}
if (clientInfo.offer_objectID != -1) {
missionInformation.PushDebug<AMFIntValue>("Offer Object LOT") = clientInfo.offer_objectID;
}
if (clientInfo.target_objectID != -1) {
missionInformation.PushDebug<AMFIntValue>("Complete Object LOT") = clientInfo.target_objectID;
}
if (!clientInfo.prereqMissionID.empty()) {
missionInformation.PushDebug<AMFStringValue>("Requirement Mission IDs") = clientInfo.prereqMissionID;
}
missionInformation.PushDebug<AMFBoolValue>("Is Repeatable") = clientInfo.repeatable;
const bool hasNoOfferer = clientInfo.offer_objectID == -1 || clientInfo.offer_objectID == 0;
const bool hasNoCompleter = clientInfo.target_objectID == -1 || clientInfo.target_objectID == 0;
missionInformation.PushDebug<AMFBoolValue>("Is Achievement") = hasNoOfferer && hasNoCompleter;
}
}
}
bool MissionComponent::OnGetObjectReportInfo(GameMessages::GameMsg& msg) {
auto& reportMsg = static_cast<GameMessages::GetObjectReportInfo&>(msg);
auto& missionInfo = reportMsg.info->PushDebug("Mission (Laggy)");
missionInfo.PushDebug<AMFIntValue>("Component ID") = GetComponentID();
// Sort the missions so they are easier to parse and present to the end user
std::map<uint32_t, Mission*> achievements;
std::map<uint32_t, Mission*> missions;
std::map<uint32_t, Mission*> doneMissions;
for (const auto [id, mission] : m_Missions) {
if (!mission) continue;
else if (mission->IsComplete()) doneMissions[id] = mission;
else if (mission->IsAchievement()) achievements[id] = mission;
else if (mission->IsMission()) missions[id] = mission;
}
// None of these should be empty, but if they are dont print the field
if (!achievements.empty() || !missions.empty()) {
auto& incompleteMissions = missionInfo.PushDebug("Incomplete Missions");
PushMissions(achievements, incompleteMissions, reportMsg.bVerbose);
PushMissions(missions, incompleteMissions, reportMsg.bVerbose);
}
if (!doneMissions.empty()) {
auto& completeMissions = missionInfo.PushDebug("Completed Missions");
PushMissions(doneMissions, completeMissions, reportMsg.bVerbose);
}
return true;
}

View File

@@ -171,6 +171,7 @@ public:
void ResetMission(const int32_t missionId);
private:
bool OnGetObjectReportInfo(GameMessages::GameMsg& msg);
/**
* All the missions owned by this entity, mapped by mission ID
*/

View File

@@ -10,9 +10,11 @@
#include "InventoryComponent.h"
#include "Item.h"
#include "MissionComponent.h"
#include "User.h"
#include "SwitchComponent.h"
#include "DestroyableComponent.h"
#include "dpWorld.h"
#include "UserManager.h"
#include "PetDigServer.h"
#include "ObjectIDManager.h"
#include "eUnequippableActiveType.h"
@@ -21,6 +23,7 @@
#include "eUseItemResponse.h"
#include "ePlayerFlag.h"
#include "GeneralUtils.h"
#include "Game.h"
#include "dConfig.h"
#include "dChatFilter.h"
@@ -553,18 +556,29 @@ void PetComponent::NotifyTamingBuildSuccess(NiPoint3 position) {
}
void PetComponent::RequestSetPetName(std::u16string name) {
const bool autoRejectNames = UserManager::Instance()->GetMuteAutoRejectNames();
if (m_Tamer == LWOOBJID_EMPTY) {
if (m_Owner != LWOOBJID_EMPTY) {
auto* owner = GetOwner();
m_ModerationStatus = 1; // Pending
m_Name = "";
// If auto reject names is on, and the user is muted, force use of predefined name
if (autoRejectNames && owner && owner->GetCharacter() && owner->GetCharacter()->GetParentUser()->GetIsMuted()) {
m_ModerationStatus = 2; // Approved
std::string forcedName = "Pet";
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus) });
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
} else {
m_ModerationStatus = 1; // Pending
m_Name = "";
//Save our pet's new name to the db:
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
//Save our pet's new name to the db:
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
GameMessages::SendSetPetName(m_Owner, GeneralUtils::UTF8ToUTF16(m_Name), m_DatabaseId, owner->GetSystemAddress());
GameMessages::SendSetPetNameModerated(m_Owner, m_DatabaseId, m_ModerationStatus, owner->GetSystemAddress());
}
}
return;
@@ -586,11 +600,21 @@ void PetComponent::RequestSetPetName(std::u16string name) {
return;
}
m_ModerationStatus = 1; // Pending
m_Name = "";
// If auto reject names is on, and the user is muted, force use of predefined name ELSE proceed with normal name check
if (autoRejectNames && tamer->GetCharacter() && tamer->GetCharacter()->GetParentUser()->GetIsMuted()) {
m_ModerationStatus = 2; // Approved
m_Name = "";
std::string forcedName = "Pet";
//Save our pet's new name to the db:
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast<int32_t>(m_ModerationStatus) });
LOG("AccountID: %i is muted, forcing use of predefined pet name", tamer->GetCharacter()->GetParentUser()->GetAccountID());
} else {
m_ModerationStatus = 1; // Pending
m_Name = "";
//Save our pet's new name to the db:
SetPetNameForModeration(GeneralUtils::UTF16ToWTF8(name));
}
Game::entityManager->SerializeEntity(m_Parent);

View File

@@ -3243,12 +3243,13 @@ void GameMessages::SendServerTradeUpdate(LWOOBJID objectId, uint64_t coins, cons
void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) {
// Check if the player has restricted trade access
auto* character = entity->GetCharacter();
const bool restrictTradeOnMute = UserManager::Instance()->GetMuteRestrictTrade();
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess)) {
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) {
// Send a message to the player
ChatPackets::SendSystemMessage(
sysAddr,
u"This character has restricted trade access."
u"Your character has restricted trade access."
);
return;
@@ -3264,7 +3265,7 @@ void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity*
if (invitee != nullptr && invitee->IsPlayer()) {
character = invitee->GetCharacter();
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess)) {
if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) {
// Send a message to the player
ChatPackets::SendSystemMessage(
sysAddr,
@@ -6414,6 +6415,7 @@ namespace GameMessages {
void RequestServerObjectInfo::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* handlingEntity = Game::entityManager->GetEntity(targetForReport);
if (handlingEntity) handlingEntity->HandleMsg(*this);
else LOG("Failed to find target %llu", targetForReport);
}
bool RequestUse::Deserialize(RakNet::BitStream& stream) {

View File

@@ -324,10 +324,14 @@ Inventory::~Inventory() {
void Inventory::RegenerateItemIDs() {
std::map<LWOOBJID, Item*> newItems{};
for (auto* const item : items | std::views::values) {
if (item->GetParent() != LWOOBJID_EMPTY) continue; // temp items dont need new ids
const bool equipped = item->IsEquipped();
if (equipped) item->UnEquip();
const auto oldID = item->GetId();
const auto newID = item->GenerateID();
LOG("Updating item ID from %llu to %llu", oldID, newID);
newItems.insert_or_assign(newID, item);
if (equipped) item->Equip();
}
// We don't want to delete the item pointers, we're just moving from map to map

View File

@@ -270,6 +270,12 @@ bool Mission::IsReadyToComplete() const {
return m_State == eMissionState::READY_TO_COMPLETE || m_State == eMissionState::COMPLETE_READY_TO_COMPLETE;
}
bool Mission::IsFailed() const {
const auto underlying = GeneralUtils::ToUnderlying(m_State);
const auto target = GeneralUtils::ToUnderlying(eMissionState::FAILED);
return (underlying & target) != 0;
}
void Mission::MakeReadyToComplete() {
SetMissionState(m_Completions == 0 ? eMissionState::READY_TO_COMPLETE : eMissionState::COMPLETE_READY_TO_COMPLETE);
}

View File

@@ -244,6 +244,8 @@ public:
const std::set<uint32_t>& GetTestedMissions() const;
bool IsFailed() const;
private:
/**
* Progresses all the newly accepted tasks for this mission after it has been accepted to reflect the state of the

View File

@@ -9,6 +9,7 @@
#include "GeneralUtils.h"
#include "Database.h"
#include "Game.h"
#include "dConfig.h"
#include "dServer.h"
#include "Entity.h"
#include "Character.h"
@@ -28,6 +29,7 @@
#include "ServiceType.h"
#include "User.h"
#include "StringifiedEnum.h"
#include "UserManager.h"
namespace {
const std::string DefaultSender = "%[MAIL_SYSTEM_NOTIFICATION]";
@@ -72,7 +74,10 @@ namespace Mail {
void SendRequest::Handle() {
SendResponse response;
auto* character = player->GetCharacter();
if (character && !(character->HasPermission(ePermissionMap::RestrictedMailAccess) || character->GetParentUser()->GetIsMuted())) {
const bool restrictMailOnMute = UserManager::Instance()->GetMuteRestrictMail() && character->GetParentUser()->GetIsMuted();
const bool restrictedMailAccess = character->HasPermission(ePermissionMap::RestrictedMailAccess);
if (character && !(restrictedMailAccess || restrictMailOnMute)) {
mailInfo.recipient = std::regex_replace(mailInfo.recipient, std::regex("[^0-9a-zA-Z]+"), "");
auto receiverID = Database::Get()->GetCharacterInfo(mailInfo.recipient);
@@ -83,7 +88,7 @@ namespace Mail {
} else {
uint32_t mailCost = Game::zoneManager->GetWorldConfig().mailBaseFee;
uint32_t stackSize = 0;
auto inventoryComponent = player->GetComponent<InventoryComponent>();
Item* item = nullptr;
@@ -123,7 +128,7 @@ namespace Mail {
Database::Get()->InsertNewMail(mailInfo);
response.status = eSendResponse::Success;
character->SaveXMLToDatabase();
character->SaveXMLToDatabase();
} else {
response.status = eSendResponse::AttachmentNotFound;
}
@@ -165,7 +170,7 @@ namespace Mail {
void DataResponse::Serialize(RakNet::BitStream& bitStream) const {
MailLUBitStream::Serialize(bitStream);
bitStream.Write(this->throttled);
bitStream.Write<uint16_t>(this->playerMail.size());
bitStream.Write<uint16_t>(0); // packing
for (const auto& mail : this->playerMail) {

View File

@@ -1475,50 +1475,55 @@ namespace DEVGMCommands {
if (splitArgs.empty()) return;
Entity* closest = nullptr;
float closestDistance = 0.0f;
std::u16string ldf;
bool isLDF = false;
auto component = GeneralUtils::TryParse<eReplicaComponentType>(splitArgs[0]);
if (!component) {
component = eReplicaComponentType::INVALID;
closest = PlayerManager::GetPlayer(splitArgs[0]);
if (!closest) {
auto component = GeneralUtils::TryParse<eReplicaComponentType>(splitArgs[0]);
if (!component) {
component = eReplicaComponentType::INVALID;
ldf = GeneralUtils::UTF8ToUTF16(splitArgs[0]);
ldf = GeneralUtils::UTF8ToUTF16(splitArgs[0]);
isLDF = true;
}
auto reference = entity->GetPosition();
auto closestDistance = 0.0f;
const auto candidates = Game::entityManager->GetEntitiesByComponent(component.value());
for (auto* candidate : candidates) {
if (candidate->GetLOT() == 1 || candidate->GetLOT() == 8092) {
continue;
isLDF = true;
}
if (isLDF && !candidate->HasVar(ldf)) {
continue;
}
if (!closest) {
closest = candidate;
closestDistance = NiPoint3::Distance(candidate->GetPosition(), reference);
continue;
}
const auto distance = NiPoint3::Distance(candidate->GetPosition(), reference);
if (distance < closestDistance) {
closest = candidate;
closestDistance = distance;
auto reference = entity->GetPosition();
const auto candidates = Game::entityManager->GetEntitiesByComponent(component.value());
for (auto* candidate : candidates) {
if (candidate->GetLOT() == 1 || candidate->GetLOT() == 8092) {
continue;
}
if (isLDF && !candidate->HasVar(ldf)) {
continue;
}
if (!closest) {
closest = candidate;
closestDistance = NiPoint3::Distance(candidate->GetPosition(), reference);
continue;
}
const auto distance = NiPoint3::Distance(candidate->GetPosition(), reference);
if (distance < closestDistance) {
closest = candidate;
closestDistance = distance;
}
}
} else {
closestDistance = NiPoint3::Distance(entity->GetPosition(), closest->GetPosition());
}
if (!closest) return;
@@ -1684,7 +1689,7 @@ namespace DEVGMCommands {
}
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
// Prevent execute command recursion by checking if this is already an execute command
for (const auto& arg : splitArgs) {
if (arg == "execute" || arg == "exec") {
@@ -1692,51 +1697,51 @@ namespace DEVGMCommands {
return;
}
}
// Context variables for execution
Entity* execEntity = entity; // Entity to execute as
NiPoint3 execPosition = entity->GetPosition(); // Position to execute from
bool positionOverridden = false;
std::string finalCommand;
// Parse subcommands
size_t i = 0;
while (i < splitArgs.size()) {
const std::string& subcommand = splitArgs[i];
if (subcommand == "as") {
if (i + 1 >= splitArgs.size()) {
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'as' requires a player name");
return;
}
const std::string& targetName = splitArgs[i + 1];
auto* targetPlayer = PlayerManager::GetPlayer(targetName);
if (!targetPlayer) {
ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found");
return;
}
execEntity = targetPlayer;
i += 2;
} else if (subcommand == "at") {
if (i + 1 >= splitArgs.size()) {
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'at' requires a player name");
return;
}
const std::string& targetName = splitArgs[i + 1];
auto* targetPlayer = PlayerManager::GetPlayer(targetName);
if (!targetPlayer) {
ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found");
return;
}
execPosition = targetPlayer->GetPosition();
positionOverridden = true;
i += 2;
} else if (subcommand == "positioned") {
if (i + 3 >= splitArgs.size()) {
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'positioned' requires x, y, z coordinates");
@@ -1754,69 +1759,69 @@ namespace DEVGMCommands {
execPosition = NiPoint3(xOpt.value(), yOpt.value(), zOpt.value());
positionOverridden = true;
i += 4;
} else if (subcommand == "run") {
// Everything after "run" is the command to execute
if (i + 1 >= splitArgs.size()) {
ChatPackets::SendSystemMessage(sysAddr, u"Error: 'run' requires a command");
return;
}
// Reconstruct the command from remaining args
for (size_t j = i + 1; j < splitArgs.size(); ++j) {
if (!finalCommand.empty()) finalCommand += " ";
finalCommand += splitArgs[j];
}
break;
} else {
ChatPackets::SendSystemMessage(sysAddr, u"Error: Unknown subcommand '" + GeneralUtils::ASCIIToUTF16(subcommand) + u"'");
ChatPackets::SendSystemMessage(sysAddr, u"Valid subcommands: as, at, positioned, run");
return;
}
}
if (finalCommand.empty()) {
ChatPackets::SendSystemMessage(sysAddr, u"Error: No command specified to run. Use 'run <command>' at the end.");
return;
}
// Validate that the command starts with a valid character
if (finalCommand.empty() || finalCommand[0] == '/') {
ChatPackets::SendSystemMessage(sysAddr, u"Error: Command should not start with '/'. Just specify the command name.");
return;
}
// Store original position if we need to restore it
NiPoint3 originalPosition;
bool needToRestore = false;
if (positionOverridden && execEntity == entity) {
// If we're executing as ourselves but from a different position,
// temporarily move the entity
originalPosition = entity->GetPosition();
needToRestore = true;
// Set the position temporarily for the command execution
auto* controllable = entity->GetComponent<ControllablePhysicsComponent>();
if (controllable) {
controllable->SetPosition(execPosition);
}
}
// Provide feedback about what we're executing
std::string execAsName = execEntity->GetCharacter() ? execEntity->GetCharacter()->GetName() : "Unknown";
ChatPackets::SendSystemMessage(sysAddr, u"[Execute] Running as '" + GeneralUtils::ASCIIToUTF16(execAsName) +
u"' from <" + GeneralUtils::to_u16string(execPosition.x) + u", " +
GeneralUtils::to_u16string(execPosition.y) + u", " +
GeneralUtils::to_u16string(execPosition.z) + u">: /" +
GeneralUtils::ASCIIToUTF16(finalCommand));
ChatPackets::SendSystemMessage(sysAddr, u"[Execute] Running as '" + GeneralUtils::ASCIIToUTF16(execAsName) +
u"' from <" + GeneralUtils::to_u16string(execPosition.x) + u", " +
GeneralUtils::to_u16string(execPosition.y) + u", " +
GeneralUtils::to_u16string(execPosition.z) + u">: /" +
GeneralUtils::ASCIIToUTF16(finalCommand));
// Execute the command through the slash command handler
SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16("/" + finalCommand), execEntity, sysAddr);
// Restore original position if needed
if (needToRestore) {
auto* controllable = entity->GetComponent<ControllablePhysicsComponent>();

View File

@@ -379,6 +379,7 @@ int main(int argc, char** argv) {
Game::im->GetInstance(0, false, 0);
Game::im->GetInstance(1000, false, 0);
StartAuthServer();
StartDashboardServer();
}
auto t = std::chrono::high_resolution_clock::now();

View File

@@ -147,3 +147,39 @@ uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInsta
LOG("WorldServer PID is %d", world_pid);
return world_pid;
}
uint32_t StartDashboardServer() {
if (Game::ShouldShutdown()) {
LOG("Currently shutting down. dashboard will not be restarted.");
return 0;
}
auto dashboard_path = BinaryPathFinder::GetBinaryDir() / "DashboardServer";
#ifdef _WIN32
dashboard_path.replace_extension(".exe");
auto dashboard_startup = startup;
auto dashboard_info = PROCESS_INFORMATION{};
if (!CreateProcessW(dashboard_path.wstring().data(), dashboard_path.wstring().data(),
nullptr, nullptr, false, 0, nullptr, nullptr,
&dashboard_startup, &dashboard_info))
{
LOG("Failed to launch DashboardServer");
return 0;
}
// get pid and close unused handles
auto dashboard_pid = dashboard_info.dwProcessId;
CloseHandle(dashboard_info.hProcess);
CloseHandle(dashboard_info.hThread);
#else // *nix systems
const auto dashboard_pid = fork();
if (dashboard_pid < 0) {
LOG("Failed to launch DashboardServer");
return 0;
} else if (dashboard_pid == 0) {
// We are the child process
execl(dashboard_path.string().c_str(), dashboard_path.string().c_str(), nullptr);
}
#endif
LOG("DashboardServer PID is %d", dashboard_pid);
return dashboard_pid;
}

View File

@@ -4,3 +4,4 @@
uint32_t StartAuthServer();
uint32_t StartChatServer();
uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID);
uint32_t StartDashboardServer();

View File

@@ -49,7 +49,7 @@ void ExplodingAsset::OnHit(Entity* self, Entity* attacker) {
if (!self->GetBoolean(u"bIsHit")) {
for (const auto objID : proximityComponent->GetProximityObjects("crateHitters")) {
auto* const entity = Game::entityManager->GetEntity(objID);
if (!entity) continue;
if (!entity || entity->GetObjectID() != attacker->GetObjectID()) continue;
auto* const destroyable = entity->GetComponent<DestroyableComponent>();
if (destroyable) destroyable->Smash(attacker->GetObjectID());

View File

@@ -13,7 +13,6 @@ namespace Game {
}
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;
@@ -73,7 +72,7 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms
reply.status = eHTTPStatusCode::UNAUTHORIZED;
reply.message = "{\"error\":\"Unauthorized\"}";
}
mg_http_reply(connection, static_cast<int>(reply.status), jsonContentType, reply.message.c_str());
mg_http_reply(connection, static_cast<int>(reply.status), reply.contentType.c_str(), reply.message.c_str());
}

View File

@@ -20,10 +20,36 @@ enum class eHTTPMethod;
// Forward declaration for mongoose manager
typedef struct mg_mgr mg_mgr;
namespace ContentType {
const std::string JSON = "Content-Type: application/json\r\n";
const std::string HTML = "Content-Type: text/html\r\n";
const std::string PLAIN = "Content-Type: text/plain\r\n";
const std::string CSS = "Content-Type: text/css\r\n";
const std::string JAVASCRIPT = "Content-Type: application/javascript\r\n";
const std::string ICO = "Content-Type: image/x-icon\r\n";
const std::string PNG = "Content-Type: image/png\r\n";
const std::string SVG = "Content-Type: image/svg+xml\r\n";
const std::string JPG = "Content-Type: image/jpeg\r\n";
const std::string GIF = "Content-Type: image/gif\r\n";
const std::string WEBP = "Content-Type: image/webp\r\n";
const std::string MP4 = "Content-Type: video/mp4\r\n";
const std::string OGG = "Content-Type: audio/ogg\r\n";
const std::string MP3 = "Content-Type: audio/mpeg\r\n";
const std::string BINARY = "Content-Type: application/octet-stream\r\n";
const std::string FORM = "Content-Type: application/x-www-form-urlencoded\r\n";
const std::string MULTIPART = "Content-Type: multipart/form-data\r\n";
const std::string ZIP = "Content-Type: application/zip\r\n";
const std::string PDF = "Content-Type: application/pdf\r\n";
const std::string XML = "Content-Type: application/xml\r\n";
const std::string CSV = "Content-Type: text/csv\r\n";
const std::string YAML = "Content-Type: application/x-yaml\r\n";
}
// For passing HTTP messages between functions
struct HTTPReply {
eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND;
std::string message = "{\"error\":\"Not Found\"}";
std::string contentType = ContentType::JSON;
};
// HTTP route structure

View File

@@ -80,7 +80,7 @@ These commands are primarily for development and testing. The usage of many of t
|getnavmeshheight|`/getnavmeshheight`|Displays the navmesh height at your current position.|8|
|giveuscore|`/giveuscore <uscore>`|Gives uscore.|8|
|gmadditem|`/gmadditem <id> (count)`|Adds the given item to your inventory by id.|8|
|inspect|`/inspect <component> (-m <waypoint> \| -a <animation> \| -s \| -p \| -f (faction) \| -t)`|Finds the closest entity with the given component or LDF variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8|
|inspect|`/inspect <component or ldf variable or player name> (-m <waypoint> \| -a <animation> \| -s \| -p \| -f (faction) \| -t)`|Finds the closest entity with the given component or LDF variable (ignoring players and racing cars), printing its ID, distance from the player, and whether it is sleeping, as well as the the IDs of all components the entity has. See [Detailed `/inspect` Usage](#detailed-inspect-usage) below.|8|
|list-spawns|`/list-spawns`|Lists all the character spawn points in the zone. Additionally, this command will display the current scene that plays when the character lands in the next zone, if there is one.|8|
|locrow|`/locrow`|Prints the your current position and rotation information to the console.|8|
|lookup|`/lookup <query>`|Searches through the Objects table in the client SQLite database for items whose display name, name, or description contains the query. Query can be multiple words delimited by spaces.|8|

View File

@@ -28,10 +28,12 @@ CREATE TABLE `properties_contents_2` (
`behavior_4` BIGINT DEFAULT 0,
`behavior_5` BIGINT DEFAULT 0
);
SET foreign_key_checks = 0;
INSERT INTO `ugc_2` SELECT `id`|0x1000000000000000,`account_id`,`character_id`,`is_optimized`,`lxfml`,`bake_ao`,`filename` FROM `ugc`;
INSERT INTO `properties_contents_2` SELECT `id`,`property_id`,`ugc_id`|0x1000000000000000,`lot`,`x`,`y`,`z`,`rx`,`ry`,`rz`,`rw`,`model_name`,`model_description`,`behavior_1`,`behavior_2`,`behavior_3`,`behavior_4`,`behavior_5` FROM `properties_contents`;
DROP TABLE `properties_contents`;
DROP TABLE `ugc`;
RENAME TABLE `properties_contents_2` TO `properties_contents`;
RENAME TABLE `ugc_2` TO `ugc`;
SET foreign_key_checks = 1;
COMMIT;

View File

View File

@@ -74,3 +74,9 @@ database_type=sqlite
# Skips the account creation check in master. Used for non-interactive setups.
skip_account_creation=0
# 0 or 1, restrict mail while account is muted
mute_restrict_mail=1
# 0 or 1, restrict trade while account is muted
mute_restrict_trade=1
# 0 or 1, automatically reject character and pet names submitted while muted
mute_auto_reject_names=1

View File

@@ -99,3 +99,6 @@ hardcore_uscore_excluded_enemies=
# Disables hardcore mode for specific worlds, if hardcore is enabled
hardcore_disabled_worlds=
# Keeps this percentage of a players' coins on death in hardcore
hardcore_coin_keep=

3031
thirdparty/inja/inja.hpp vendored Normal file

File diff suppressed because it is too large Load Diff