mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2024-11-28 00:17:23 +00:00
fee0238e79
Tested with logs that queries to get soft and hard cap actually succeed now Logs about slash command handler command registration and vanity NPC creation in mis matched worlds are now removed.
429 lines
13 KiB
C++
429 lines
13 KiB
C++
#include "VanityUtilities.h"
|
|
|
|
#include "DestroyableComponent.h"
|
|
#include "EntityManager.h"
|
|
#include "GameMessages.h"
|
|
#include "InventoryComponent.h"
|
|
#include "PhantomPhysicsComponent.h"
|
|
#include "ProximityMonitorComponent.h"
|
|
#include "ScriptComponent.h"
|
|
#include "dCommonVars.h"
|
|
#include "dConfig.h"
|
|
#include "dServer.h"
|
|
#include "tinyxml2.h"
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "BinaryPathFinder.h"
|
|
#include "EntityInfo.h"
|
|
#include "Spawner.h"
|
|
#include "dZoneManager.h"
|
|
#include "ObjectIDManager.h"
|
|
#include "Level.h"
|
|
|
|
#include <fstream>
|
|
|
|
|
|
namespace {
|
|
std::vector<VanityObject> objects;
|
|
std::set<std::string> loadedFiles;
|
|
}
|
|
|
|
void SetupNPCTalk(Entity* npc);
|
|
void NPCTalk(Entity* npc);
|
|
void ParseXml(const std::string& file);
|
|
LWOOBJID SpawnSpawner(const VanityObject& object, const VanityObjectLocation& location);
|
|
Entity* SpawnObject(const VanityObject& object, const VanityObjectLocation& location);
|
|
VanityObject* GetObject(const std::string& name);
|
|
|
|
void VanityUtilities::SpawnVanity() {
|
|
const uint32_t zoneID = Game::server->GetZoneID();
|
|
|
|
if (zoneID == 1200) {
|
|
{
|
|
EntityInfo info;
|
|
info.lot = 8139;
|
|
info.pos = { 259.5f, 246.4f, -705.2f };
|
|
info.rot = { 0.0f, 0.0f, 1.0f, 0.0f };
|
|
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
|
|
info.settings = {
|
|
new LDFData<bool>(u"hasCustomText", true),
|
|
new LDFData<std::string>(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string()))
|
|
};
|
|
|
|
auto* entity = Game::entityManager->CreateEntity(info);
|
|
Game::entityManager->ConstructEntity(entity);
|
|
}
|
|
}
|
|
|
|
if (Game::config->GetValue("disable_vanity") == "1") return;
|
|
|
|
for (const auto& npc : objects) {
|
|
if (npc.m_ID == LWOOBJID_EMPTY) continue;
|
|
if (npc.m_LOT == 176){
|
|
Game::zoneManager->RemoveSpawner(npc.m_ID);
|
|
} else{
|
|
auto* entity = Game::entityManager->GetEntity(npc.m_ID);
|
|
if (!entity) continue;
|
|
entity->Smash(LWOOBJID_EMPTY, eKillType::VIOLENT);
|
|
}
|
|
}
|
|
|
|
objects.clear();
|
|
loadedFiles.clear();
|
|
|
|
ParseXml((BinaryPathFinder::GetBinaryDir() / "vanity/root.xml").string());
|
|
|
|
// Loop through all objects
|
|
for (auto& object : objects) {
|
|
if (object.m_Locations.find(Game::server->GetZoneID()) == object.m_Locations.end()) continue;
|
|
|
|
const std::vector<VanityObjectLocation>& locations = object.m_Locations.at(Game::server->GetZoneID());
|
|
|
|
// Pick a random location
|
|
const auto& location = locations[GeneralUtils::GenerateRandomNumber<int>(
|
|
static_cast<size_t>(0), static_cast<size_t>(locations.size() - 1))];
|
|
|
|
float rate = GeneralUtils::GenerateRandomNumber<float>(0, 1);
|
|
if (location.m_Chance < rate) continue;
|
|
|
|
if (object.m_LOT == 176){
|
|
object.m_ID = SpawnSpawner(object, location);
|
|
} else {
|
|
// Spawn the NPC
|
|
auto* objectEntity = SpawnObject(object, location);
|
|
if (!objectEntity) continue;
|
|
object.m_ID = objectEntity->GetObjectID();
|
|
if (!object.m_Phrases.empty()){
|
|
objectEntity->SetVar<std::vector<std::string>>(u"chats", object.m_Phrases);
|
|
SetupNPCTalk(objectEntity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LWOOBJID SpawnSpawner(const VanityObject& object, const VanityObjectLocation& location) {
|
|
SceneObject obj;
|
|
obj.lot = object.m_LOT;
|
|
// guratantee we have no collisions
|
|
do {
|
|
obj.id = ObjectIDManager::GenerateObjectID();
|
|
} while(Game::zoneManager->GetSpawner(obj.id));
|
|
obj.position = location.m_Position;
|
|
obj.rotation = location.m_Rotation;
|
|
obj.settings = object.m_Config;
|
|
Level::MakeSpawner(obj);
|
|
return obj.id;
|
|
}
|
|
|
|
Entity* SpawnObject(const VanityObject& object, const VanityObjectLocation& location) {
|
|
EntityInfo info;
|
|
info.lot = object.m_LOT;
|
|
info.pos = location.m_Position;
|
|
info.rot = location.m_Rotation;
|
|
info.scale = location.m_Scale;
|
|
info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
|
|
info.settings = object.m_Config;
|
|
|
|
auto* entity = Game::entityManager->CreateEntity(info);
|
|
if (!object.m_Name.empty()) entity->SetVar(u"npcName", object.m_Name);
|
|
if (entity->GetVar<bool>(u"noGhosting")) entity->SetIsGhostingCandidate(false);
|
|
|
|
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
|
|
if (inventoryComponent && !object.m_Equipment.empty()) {
|
|
inventoryComponent->SetNPCItems(object.m_Equipment);
|
|
}
|
|
|
|
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
|
if (destroyableComponent) {
|
|
destroyableComponent->SetIsGMImmune(true);
|
|
destroyableComponent->SetMaxHealth(0);
|
|
destroyableComponent->SetHealth(0);
|
|
}
|
|
|
|
Game::entityManager->ConstructEntity(entity);
|
|
|
|
return entity;
|
|
}
|
|
|
|
void ParseXml(const std::string& file) {
|
|
if (loadedFiles.contains(file)){
|
|
LOG("Trying to load vanity file %s twice!!!", file.c_str());
|
|
return;
|
|
}
|
|
loadedFiles.insert(file);
|
|
// Read the entire file
|
|
std::ifstream xmlFile(file);
|
|
std::string xml((std::istreambuf_iterator<char>(xmlFile)), std::istreambuf_iterator<char>());
|
|
|
|
// Parse the XML
|
|
tinyxml2::XMLDocument doc;
|
|
doc.Parse(xml.c_str(), xml.size());
|
|
|
|
// Read the objects
|
|
auto* files = doc.FirstChildElement("files");
|
|
if (files) {
|
|
for (auto* file = files->FirstChildElement("file"); file != nullptr; file = file->NextSiblingElement("file")) {
|
|
std::string enabled = file->Attribute("enabled");
|
|
std::string filename = file->Attribute("name");
|
|
if (enabled != "1") {
|
|
continue;
|
|
}
|
|
ParseXml((BinaryPathFinder::GetBinaryDir() / "vanity" / filename).string());
|
|
}
|
|
}
|
|
|
|
// Read the objects
|
|
auto* objectsElement = doc.FirstChildElement("objects");
|
|
const uint32_t currentZoneID = Game::server->GetZoneID();
|
|
if (objectsElement) {
|
|
for (auto* object = objectsElement->FirstChildElement("object"); object != nullptr; object = object->NextSiblingElement("object")) {
|
|
// for use later when adding to the vector of VanityObjects
|
|
bool useLocationsAsRandomSpawnPoint = false;
|
|
// Get the NPC name
|
|
auto* name = object->Attribute("name");
|
|
|
|
if (!name) name = "";
|
|
|
|
// Get the NPC lot
|
|
auto lot = GeneralUtils::TryParse<LOT>(object->Attribute("lot")).value_or(LOT_NULL);
|
|
|
|
if (lot == LOT_NULL) {
|
|
LOG("Failed to parse object lot");
|
|
continue;
|
|
}
|
|
|
|
// Get the equipment
|
|
auto* equipment = object->FirstChildElement("equipment");
|
|
std::vector<LOT> inventory;
|
|
|
|
if (equipment) {
|
|
auto* text = equipment->GetText();
|
|
|
|
if (text != nullptr) {
|
|
std::string equipmentString(text);
|
|
|
|
std::vector<std::string> splitEquipment = GeneralUtils::SplitString(equipmentString, ',');
|
|
|
|
for (auto& item : splitEquipment) {
|
|
// remove spaces for tryParse to work
|
|
item.erase(remove_if(item.begin(), item.end(), isspace), item.end());
|
|
auto itemInt = GeneralUtils::TryParse<uint32_t>(item);
|
|
if (itemInt) inventory.push_back(itemInt.value());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the phrases
|
|
auto* phrases = object->FirstChildElement("phrases");
|
|
std::vector<std::string> phraseList = {};
|
|
if (phrases) {
|
|
for (auto* phrase = phrases->FirstChildElement("phrase"); phrase != nullptr;
|
|
phrase = phrase->NextSiblingElement("phrase")) {
|
|
// Get the phrase
|
|
auto* text = phrase->GetText();
|
|
if (text == nullptr) {
|
|
LOG("Failed to parse NPC phrase");
|
|
continue;
|
|
}
|
|
phraseList.push_back(text);
|
|
}
|
|
}
|
|
|
|
auto* configElement = object->FirstChildElement("config");
|
|
std::vector<std::u16string> keys = {};
|
|
std::vector<LDFBaseData*> config = {};
|
|
if(configElement) {
|
|
for (auto* key = configElement->FirstChildElement("key"); key != nullptr;
|
|
key = key->NextSiblingElement("key")) {
|
|
// Get the config data
|
|
auto* data = key->GetText();
|
|
if (!data) continue;
|
|
|
|
LDFBaseData* configData = LDFBaseData::DataFromString(data);
|
|
if (configData->GetKey() == u"useLocationsAsRandomSpawnPoint" && configData->GetValueType() == eLDFType::LDF_TYPE_BOOLEAN){
|
|
useLocationsAsRandomSpawnPoint = static_cast<bool>(configData);
|
|
continue;
|
|
}
|
|
keys.push_back(configData->GetKey());
|
|
config.push_back(configData);
|
|
}
|
|
}
|
|
if (!keys.empty()) config.push_back(new LDFData<std::vector<std::u16string>>(u"syncLDF", keys));
|
|
|
|
VanityObject objectData {
|
|
.m_Name = name,
|
|
.m_LOT = lot,
|
|
.m_Equipment = inventory,
|
|
.m_Phrases = phraseList,
|
|
.m_Config = config
|
|
};
|
|
|
|
// Get the locations
|
|
auto* locations = object->FirstChildElement("locations");
|
|
|
|
if (locations == nullptr) {
|
|
LOG("Failed to parse NPC locations");
|
|
continue;
|
|
}
|
|
|
|
for (auto* location = locations->FirstChildElement("location"); location != nullptr;
|
|
location = location->NextSiblingElement("location")) {
|
|
|
|
// Get the location data
|
|
auto zoneID = GeneralUtils::TryParse<uint32_t>(location->Attribute("zone"));
|
|
auto x = GeneralUtils::TryParse<float>(location->Attribute("x"));
|
|
auto y = GeneralUtils::TryParse<float>(location->Attribute("y"));
|
|
auto z = GeneralUtils::TryParse<float>(location->Attribute("z"));
|
|
auto rw = GeneralUtils::TryParse<float>(location->Attribute("rw"));
|
|
auto rx = GeneralUtils::TryParse<float>(location->Attribute("rx"));
|
|
auto ry = GeneralUtils::TryParse<float>(location->Attribute("ry"));
|
|
auto rz = GeneralUtils::TryParse<float>(location->Attribute("rz"));
|
|
|
|
if (!zoneID || !x || !y || !z || !rw || !rx || !ry || !rz) {
|
|
LOG("Failed to parse NPC location data");
|
|
continue;
|
|
}
|
|
|
|
if (zoneID.value() != currentZoneID) {
|
|
continue;
|
|
}
|
|
|
|
VanityObjectLocation locationData {
|
|
.m_Position = { x.value(), y.value(), z.value() },
|
|
.m_Rotation = { rw.value(), rx.value(), ry.value(), rz.value() },
|
|
};
|
|
|
|
if (location->Attribute("chance")) {
|
|
locationData.m_Chance = GeneralUtils::TryParse<float>(location->Attribute("chance")).value_or(1.0f);
|
|
}
|
|
|
|
if (location->Attribute("scale")) {
|
|
locationData.m_Scale = GeneralUtils::TryParse<float>(location->Attribute("scale")).value_or(1.0f);
|
|
}
|
|
|
|
const auto& it = objectData.m_Locations.find(zoneID.value());
|
|
|
|
if (it != objectData.m_Locations.end()) {
|
|
it->second.push_back(locationData);
|
|
} else {
|
|
std::vector<VanityObjectLocation> locations;
|
|
locations.push_back(locationData);
|
|
objectData.m_Locations.insert(std::make_pair(zoneID.value(), locations));
|
|
}
|
|
|
|
if (!useLocationsAsRandomSpawnPoint) {
|
|
objects.push_back(objectData);
|
|
objectData.m_Locations.clear();
|
|
}
|
|
}
|
|
|
|
if (useLocationsAsRandomSpawnPoint && !objectData.m_Locations.empty()) {
|
|
objects.push_back(objectData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
VanityObject* VanityUtilities::GetObject(const std::string& name) {
|
|
for (size_t i = 0; i < objects.size(); i++) {
|
|
if (objects[i].m_Name == name) {
|
|
return &objects[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::string VanityUtilities::ParseMarkdown(const std::string& file) {
|
|
// This function will read the file and return the content formatted as ASCII text.
|
|
|
|
// Read the file into a string
|
|
std::ifstream t(file);
|
|
std::stringstream output;
|
|
// If the file does not exist, return a useful error.
|
|
if (!t.good()) {
|
|
output << "File ";
|
|
output << file.substr(file.rfind("/") + 1);
|
|
output << " not found!\nContact your DarkflameServer admin\nor find the server source at https://github.com/DarkflameUniverse/DarkflameServer";
|
|
return output.str();
|
|
}
|
|
|
|
std::stringstream buffer;
|
|
buffer << t.rdbuf();
|
|
std::string fileContents = buffer.str();
|
|
|
|
// Loop through all lines in the file.
|
|
// Replace all instances of the markdown syntax with the corresponding HTML.
|
|
// Only care about headers
|
|
std::string line;
|
|
std::stringstream ss;
|
|
ss << fileContents;
|
|
while (std::getline(ss, line)) {
|
|
|
|
#define TOSTRING(x) #x
|
|
|
|
#ifndef STRINGIFY
|
|
#define STRINGIFY(x) TOSTRING(x)
|
|
#endif
|
|
// Replace "__TIMESTAMP__" with the __TIMESTAMP__
|
|
GeneralUtils::ReplaceInString(line, "__TIMESTAMP__", __TIMESTAMP__);
|
|
// Replace "__VERSION__" with the PROJECT_VERSION
|
|
GeneralUtils::ReplaceInString(line, "__VERSION__", Game::projectVersion);
|
|
// Replace "__SOURCE__" with SOURCE
|
|
GeneralUtils::ReplaceInString(line, "__SOURCE__", Game::config->GetValue("source"));
|
|
// Replace "__LICENSE__" with LICENSE
|
|
GeneralUtils::ReplaceInString(line, "__LICENSE__", "AGPL-3.0");
|
|
|
|
if (line.find("##") != std::string::npos) {
|
|
// Add "<font size='18' color='#000000'>" before the header
|
|
output << "<font size=\"14\" color=\"#000000\">";
|
|
// Add the header without the markdown syntax
|
|
output << line.substr(3);
|
|
|
|
output << "</font>";
|
|
} else if (line.find("#") != std::string::npos) {
|
|
// Add "<font size='18' color='#000000'>" before the header
|
|
output << "<font size=\"18\" color=\"#000000\">";
|
|
// Add the header without the markdown syntax
|
|
output << line.substr(2);
|
|
|
|
output << "</font>";
|
|
} else {
|
|
output << line;
|
|
}
|
|
|
|
output << "\n";
|
|
}
|
|
|
|
return output.str();
|
|
}
|
|
|
|
void SetupNPCTalk(Entity* npc) {
|
|
npc->AddCallbackTimer(15.0f, [npc]() { NPCTalk(npc); });
|
|
|
|
npc->SetProximityRadius(20.0f, "talk");
|
|
}
|
|
|
|
void NPCTalk(Entity* npc) {
|
|
auto* proximityMonitorComponent = npc->GetComponent<ProximityMonitorComponent>();
|
|
|
|
if (!proximityMonitorComponent->GetProximityObjects("talk").empty()) {
|
|
const auto& chats = npc->GetVar<std::vector<std::string>>(u"chats");
|
|
|
|
if (chats.empty()) {
|
|
return;
|
|
}
|
|
|
|
const auto& selected
|
|
= chats[GeneralUtils::GenerateRandomNumber<int32_t>(0, static_cast<int32_t>(chats.size() - 1))];
|
|
|
|
GameMessages::SendNotifyClientZoneObject(
|
|
npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
Game::entityManager->SerializeEntity(npc);
|
|
|
|
const float nextTime = GeneralUtils::GenerateRandomNumber<float>(15, 60);
|
|
|
|
npc->AddCallbackTimer(nextTime, [npc]() { NPCTalk(npc); });
|
|
}
|