#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 std::vector VanityUtilities::m_NPCs = {}; std::vector VanityUtilities::m_Parties = {}; std::vector VanityUtilities::m_PartyPhrases = {}; void VanityUtilities::SpawnVanity() { if (Game::config->GetValue("disable_vanity") == "1") { return; } const uint32_t zoneID = Game::server->GetZoneID(); ParseXML((BinaryPathFinder::GetBinaryDir() / "vanity/NPC.xml").string()); // Loop through all parties for (const auto& party : m_Parties) { const auto chance = party.m_Chance; const auto zone = party.m_Zone; if (zone != Game::server->GetZoneID()) { continue; } float rate = GeneralUtils::GenerateRandomNumber(0, 1); if (chance < rate) { continue; } // Copy m_NPCs into a new vector std::vector npcList = m_NPCs; std::vector taken = {}; LOG("Spawning party with %i locations", party.m_Locations.size()); // Loop through all locations for (const auto& location : party.m_Locations) { rate = GeneralUtils::GenerateRandomNumber(0, 1); if (0.75f < rate) { continue; } // Get a random NPC auto npcIndex = GeneralUtils::GenerateRandomNumber(0, npcList.size() - 1); while (std::find(taken.begin(), taken.end(), npcIndex) != taken.end()) { npcIndex = GeneralUtils::GenerateRandomNumber(0, npcList.size() - 1); } auto& npc = npcList[npcIndex]; taken.push_back(npcIndex); // Spawn the NPC LOG("ldf size is %i", npc.ldf.size()); if (npc.ldf.empty()) { npc.ldf = { new LDFData>(u"syncLDF", { u"custom_script_client" }), new LDFData(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua") }; } // Spawn the NPC auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf); if (!npc.m_Phrases.empty()) { npcEntity->SetVar>(u"chats", m_PartyPhrases); SetupNPCTalk(npcEntity); } } return; } // Loop through all NPCs for (auto& npc : m_NPCs) { if (npc.m_Locations.find(Game::server->GetZoneID()) == npc.m_Locations.end()) continue; const std::vector& locations = npc.m_Locations.at(Game::server->GetZoneID()); // Pick a random location const auto& location = locations[GeneralUtils::GenerateRandomNumber( static_cast(0), static_cast(locations.size() - 1))]; float rate = GeneralUtils::GenerateRandomNumber(0, 1); if (location.m_Chance < rate) { continue; } if (npc.ldf.empty()) { npc.ldf = { new LDFData>(u"syncLDF", { u"custom_script_client" }), new LDFData(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua") }; } // Spawn the NPC auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf); if (!npc.m_Phrases.empty()){ npcEntity->SetVar>(u"chats", npc.m_Phrases); SetupNPCTalk(npcEntity); } auto* scriptComponent = npcEntity->GetComponent(); Game::logger->Log("VanityUtilities", "Script: %s", npc.m_Script.c_str()); if (scriptComponent && !npc.m_Script.empty()) { scriptComponent->SetScript(npc.m_Script); scriptComponent->SetSerialized(false); Game::logger->Log("VanityUtilities", "Setting script to %s", npc.m_Script.c_str()); for (const auto& npc : npc.m_Flags) { npcEntity->SetVar(GeneralUtils::ASCIIToUTF16(npc.first), npc.second); } } } 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(u"hasCustomText", true), new LDFData(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string())) }; auto* entity = Game::entityManager->CreateEntity(info); Game::entityManager->ConstructEntity(entity); } } } Entity* VanityUtilities::SpawnNPC(LOT lot, const std::string& name, const NiPoint3& position, const NiQuaternion& rotation, const std::vector& inventory, const std::vector& ldf) { EntityInfo info; info.lot = lot; info.pos = position; info.rot = rotation; info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID(); info.settings = ldf; auto* entity = Game::entityManager->CreateEntity(info); entity->SetVar(u"npcName", name); if (entity->GetVar(u"noGhosting")) entity->SetIsGhostingCandidate(false); // Debug print Game::logger->Log("VanityUtilities", "Spawning NPC %s (%i) at %f, %f, %f", name.c_str(), lot, position.x, position.y, position.z); auto* inventoryComponent = entity->GetComponent(); if (inventoryComponent && !inventory.empty()) { inventoryComponent->SetNPCItems(inventory); } auto* destroyableComponent = entity->GetComponent(); if (destroyableComponent != nullptr) { destroyableComponent->SetIsGMImmune(true); destroyableComponent->SetMaxHealth(0); destroyableComponent->SetHealth(0); } auto* scriptComponent = entity->GetComponent(); if (scriptComponent == nullptr) { entity->AddComponent(eReplicaComponentType::SCRIPT, new ScriptComponent(entity, "", false)); } Game::logger->Log("VanityUtilities", "NPC has script component? %s", (entity->GetComponent() != nullptr) ? "true" : "false"); Game::entityManager->ConstructEntity(entity); return entity; } void VanityUtilities::ParseXML(const std::string& file) { // Read the entire file std::ifstream xmlFile(file); std::string xml((std::istreambuf_iterator(xmlFile)), std::istreambuf_iterator()); // Parse the XML tinyxml2::XMLDocument doc; doc.Parse(xml.c_str(), xml.size()); // Read the NPCs auto* npcs = doc.FirstChildElement("npcs"); if (npcs == nullptr) { LOG("Failed to parse NPCs"); return; } for (auto* party = npcs->FirstChildElement("party"); party != nullptr; party = party->NextSiblingElement("party")) { // Get 'zone' as uint32_t and 'chance' as float uint32_t zone = 0; float chance = 0.0f; if (party->Attribute("zone") != nullptr) { zone = std::stoul(party->Attribute("zone")); } if (party->Attribute("chance") != nullptr) { chance = std::stof(party->Attribute("chance")); } VanityParty partyInfo; partyInfo.m_Zone = zone; partyInfo.m_Chance = chance; auto* locations = party->FirstChildElement("locations"); if (locations == nullptr) { LOG("Failed to parse party locations"); continue; } for (auto* location = locations->FirstChildElement("location"); location != nullptr; location = location->NextSiblingElement("location")) { // Get the location data auto* x = location->Attribute("x"); auto* y = location->Attribute("y"); auto* z = location->Attribute("z"); auto* rw = location->Attribute("rw"); auto* rx = location->Attribute("rx"); auto* ry = location->Attribute("ry"); auto* rz = location->Attribute("rz"); if (x == nullptr || y == nullptr || z == nullptr || rw == nullptr || rx == nullptr || ry == nullptr || rz == nullptr) { LOG("Failed to parse party location data"); continue; } VanityNPCLocation locationData; locationData.m_Position = { std::stof(x), std::stof(y), std::stof(z) }; locationData.m_Rotation = { std::stof(rw), std::stof(rx), std::stof(ry), std::stof(rz) }; locationData.m_Chance = 1.0f; partyInfo.m_Locations.push_back(locationData); } m_Parties.push_back(partyInfo); } auto* partyPhrases = npcs->FirstChildElement("partyphrases"); if (partyPhrases == nullptr) { LOG("Failed to parse party phrases"); return; } for (auto* phrase = partyPhrases->FirstChildElement("phrase"); phrase != nullptr; phrase = phrase->NextSiblingElement("phrase")) { // Get the phrase auto* text = phrase->GetText(); if (text == nullptr) { LOG("Failed to parse party phrase"); continue; } m_PartyPhrases.push_back(text); } for (auto* npc = npcs->FirstChildElement("npc"); npc != nullptr; npc = npc->NextSiblingElement("npc")) { // Get the NPC name auto* name = npc->Attribute("name"); if (!name) name = ""; // Get the NPC lot auto* lot = npc->Attribute("lot"); if (lot == nullptr) { LOG("Failed to parse NPC lot"); continue; } // Get the equipment auto* equipment = npc->FirstChildElement("equipment"); std::vector inventory; if (equipment) { auto* text = equipment->GetText(); if (text != nullptr) { std::string equipmentString(text); std::vector splitEquipment = GeneralUtils::SplitString(equipmentString, ','); for (auto& item : splitEquipment) { inventory.push_back(std::stoi(item)); } } } // Get the phrases auto* phrases = npc->FirstChildElement("phrases"); std::vector 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); } } // Get the script auto* scriptElement = npc->FirstChildElement("script"); std::string scriptName = ""; if (scriptElement != nullptr) { auto* scriptNameAttribute = scriptElement->Attribute("name"); if (scriptNameAttribute) scriptName = scriptNameAttribute; } auto* ldfElement = npc->FirstChildElement("ldf"); std::vector keys = {}; std::vector ldf = {}; if(ldfElement) { for (auto* entry = ldfElement->FirstChildElement("entry"); entry != nullptr; entry = entry->NextSiblingElement("entry")) { // Get the ldf data auto* data = entry->Attribute("data"); if (!data) continue; LDFBaseData* ldfData = LDFBaseData::DataFromString(data); keys.push_back(ldfData->GetKey()); ldf.push_back(ldfData); } } if (!keys.empty()) ldf.push_back(new LDFData>(u"syncLDF", keys)); VanityNPC npcData; npcData.m_Name = name; npcData.m_LOT = std::stoi(lot); npcData.m_Equipment = inventory; npcData.m_Phrases = phraseList; npcData.m_Script = scriptName; npcData.ldf = ldf; // Get flags auto* flags = npc->FirstChildElement("flags"); if (flags != nullptr) { for (auto* flag = flags->FirstChildElement("flag"); flag != nullptr; flag = flag->NextSiblingElement("flag")) { // Get the flag name auto* name = flag->Attribute("name"); if (name == nullptr) { LOG("Failed to parse NPC flag name"); continue; } // Get the flag value auto* value = flag->Attribute("value"); if (value == nullptr) { LOG("Failed to parse NPC flag value"); continue; } npcData.m_Flags[name] = std::stoi(value); } } // Get the zones for (auto* zone = npc->FirstChildElement("zone"); zone != nullptr; zone = zone->NextSiblingElement("zone")) { // Get the zone ID auto* zoneID = zone->Attribute("id"); if (zoneID == nullptr) { LOG("Failed to parse NPC zone ID"); continue; } // Get the locations auto* locations = zone->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* x = location->Attribute("x"); auto* y = location->Attribute("y"); auto* z = location->Attribute("z"); auto* rw = location->Attribute("rw"); auto* rx = location->Attribute("rx"); auto* ry = location->Attribute("ry"); auto* rz = location->Attribute("rz"); if (x == nullptr || y == nullptr || z == nullptr || rw == nullptr || rx == nullptr || ry == nullptr || rz == nullptr) { LOG("Failed to parse NPC location data"); continue; } VanityNPCLocation locationData; locationData.m_Position = { std::stof(x), std::stof(y), std::stof(z) }; locationData.m_Rotation = { std::stof(rw), std::stof(rx), std::stof(ry), std::stof(rz) }; locationData.m_Chance = 1.0f; if (location->Attribute("chance") != nullptr) { locationData.m_Chance = std::stof(location->Attribute("chance")); } const auto& it = npcData.m_Locations.find(std::stoi(zoneID)); if (it != npcData.m_Locations.end()) { it->second.push_back(locationData); } else { std::vector locations; locations.push_back(locationData); npcData.m_Locations.insert(std::make_pair(std::stoi(zoneID), locations)); } } } m_NPCs.push_back(npcData); } } VanityNPC* VanityUtilities::GetNPC(const std::string& name) { for (size_t i = 0; i < m_NPCs.size(); i++) { if (m_NPCs[i].m_Name == name) { return &m_NPCs[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); // If the file does not exist, return an empty string. if (!t.good()) { return ""; } 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::stringstream output; 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__" wit'h the PROJECT_VERSION GeneralUtils::ReplaceInString(line, "__VERSION__", STRINGIFY(PROJECT_VERSION)); // Replace "__SOURCE__" with SOURCE GeneralUtils::ReplaceInString(line, "__SOURCE__", Game::config->GetValue("source")); // Replace "__LICENSE__" with LICENSE GeneralUtils::ReplaceInString(line, "__LICENSE__", STRINGIFY(LICENSE)); if (line.find("##") != std::string::npos) { // Add "<font size='18' color='#000000'>" before the header output << ""; // Add the header without the markdown syntax output << line.substr(3); output << ""; } else if (line.find("#") != std::string::npos) { // Add "<font size='18' color='#000000'>" before the header output << ""; // Add the header without the markdown syntax output << line.substr(2); output << ""; } else { output << line; } output << "\n"; } return output.str(); } void VanityUtilities::SetupNPCTalk(Entity* npc) { npc->AddCallbackTimer(15.0f, [npc]() { NPCTalk(npc); }); npc->SetProximityRadius(20.0f, "talk"); } void VanityUtilities::NPCTalk(Entity* npc) { auto* proximityMonitorComponent = npc->GetComponent(); if (!proximityMonitorComponent->GetProximityObjects("talk").empty()) { const auto& chats = npc->GetVar>(u"chats"); if (chats.empty()) { return; } const auto& selected = chats[GeneralUtils::GenerateRandomNumber(0, static_cast(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(15, 60); npc->AddCallbackTimer(nextTime, [npc]() { NPCTalk(npc); }); }