diff --git a/data/sql/updates/pending_db_world/rev_1766254536399579700.sql b/data/sql/updates/pending_db_world/rev_1766254536399579700.sql new file mode 100644 index 000000000..f65621714 --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1766254536399579700.sql @@ -0,0 +1,10 @@ +-- +DELETE FROM `command` WHERE `name` LIKE 'pooltools%'; +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('pooltools', 3, 'Syntax: .pooltools $subcommand\nTools for creating gameobject pools ingame.\nTo use pooltools, uncomment Appender.Dev and Logger.sql.dev in the worldserver config.'), +('pooltools start', 3, 'Syntax: .pooltools start #description\nStarts a pooling session with the specified description.'), +('pooltools def', 3, 'Syntax: .pooltools def #GameobjectID #Chance #GameobjectID #Chance (...)\nDefines the Gameobject entries to be detected along with their associated chances.'), +('pooltools add', 3, 'Syntax: .pooltools add [radius]\nAdds nearby gameobjects to the pooling session. Default radius is 5y.'), +('pooltools remove', 3, 'Syntax: .pooltools remove\nRemoves the last group from the pooling session.'), +('pooltools end', 3, 'Syntax: .pooltools end\nLogs the current pooling session and clears it.'), +('pooltools clear', 3, 'Syntax: .pooltools clear\nClears the current pooling session.'); diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 92ed0e3af..c34645af1 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -680,6 +680,7 @@ Appender.Server=2,5,0,Server.log,w # Appender.GM=2,5,15,gm_%s.log Appender.Errors=2,2,0,Errors.log,w # Appender.DB=3,5,0 +# Appender.Dev=2,5,0,Dev.log,a # Logger config values: Given a logger "name" # Logger.name @@ -804,7 +805,7 @@ Logger.spells.scripts=2,Console Errors #Logger.spells.effect=4,Console Server #Logger.spells.scripts=4,Console Server #Logger.spells=4,Console Server -#Logger.sql.dev=4,Console Server +#Logger.sql.dev=4,Console Server Dev #Logger.sql.driver=4,Console Server #Logger.vehicles=4,Console Server #Logger.warden=4,Console Server diff --git a/src/server/scripts/Commands/cs_pooltools.cpp b/src/server/scripts/Commands/cs_pooltools.cpp new file mode 100644 index 000000000..4bb4d448f --- /dev/null +++ b/src/server/scripts/Commands/cs_pooltools.cpp @@ -0,0 +1,419 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "Cell.h" +#include "CellImpl.h" +#include "Chat.h" +#include "CommandScript.h" +#include "GameObject.h" +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" +#include "MapMgr.h" +#include "Player.h" +#include "ScriptMgr.h" +#include "Tokenize.h" + +using namespace Acore::ChatCommands; + +struct PoolTemplateItem +{ + uint32 Entry; + uint32 Chance; +}; + +// Represents one "spawn point" which might contain multiple GUIDs +struct NodeGroup +{ + float X = 0.0f; + float Y = 0.0f; + float Z = 0.0f; + std::vector> FoundObjects; +}; + +struct PoolSession +{ + std::string ZoneName; + std::vector CurrentTemplate; + std::vector CapturedGroups; +}; + +static std::map PoolSessions; + +class pooltools_commandscript : public CommandScript +{ +public: + pooltools_commandscript() : CommandScript("pooltools_commandscript") {} + + ChatCommandTable GetCommands() const override + { + static ChatCommandTable poolToolsCommandTable = + { + { "start", HandlePoolStart, SEC_ADMINISTRATOR, Console::No }, + { "def", HandlePoolDef, SEC_ADMINISTRATOR, Console::No }, + { "add", HandlePoolAdd, SEC_ADMINISTRATOR, Console::No }, + { "remove", HandlePoolRemove, SEC_ADMINISTRATOR, Console::No }, + { "end", HandlePoolEnd, SEC_ADMINISTRATOR, Console::No }, + { "clear", HandlePoolClear, SEC_ADMINISTRATOR, Console::No } + }; + + static ChatCommandTable commandTable = + { + { "pooltools", poolToolsCommandTable } + }; + return commandTable; + } + + static bool HandlePoolStart(ChatHandler* handler, std::string description) + { + ObjectGuid const playerGuid = handler->GetPlayer()->GetGUID(); + + if (PoolSessions.find(playerGuid) != PoolSessions.end()) + { + handler->SendErrorMessage("Session already active. Use .pooltools clear first."); + return false; + } + + PoolSession session; + session.ZoneName = description; + + PoolSessions[playerGuid] = session; + + handler->PSendSysMessage("|cff00ff00Pool Session Started.|r Description: {}", description); + return true; + } + + static bool HandlePoolDef(ChatHandler* handler, Tail args) + { + ObjectGuid playerGuid = handler->GetPlayer()->GetGUID(); + if (PoolSessions.find(playerGuid) == PoolSessions.end()) + { + handler->SendErrorMessage("No active session."); + return false; + } + + std::vector tokens = Acore::Tokenize(args, ' ', false); + + if (tokens.empty() || tokens.size() % 2 != 0) + { + handler->SendErrorMessage("Invalid syntax. Usage: .pooltools def [ID] [Chance] [ID] [Chance]..."); + return false; + } + + std::vector newTemplate; + for (size_t i = 0; i < tokens.size(); i += 2) + { + uint32 const entry = Acore::StringTo(tokens[i]).value_or(0); + uint32 const chance = Acore::StringTo(tokens[i + 1]).value_or(0); + + if (entry == 0) + continue; + + newTemplate.push_back({ entry, chance }); + } + + PoolSessions[playerGuid].CurrentTemplate = newTemplate; + handler->PSendSysMessage("Template Defined ({} items).", newTemplate.size()); + return true; + } + + static bool HandlePoolAdd(ChatHandler* handler, Optional radiusArg) + { + ObjectGuid playerGuid = handler->GetPlayer()->GetGUID(); + if (PoolSessions.find(playerGuid) == PoolSessions.end()) + return false; + + PoolSession& session = PoolSessions[playerGuid]; + if (session.CurrentTemplate.empty()) + { + handler->SendErrorMessage("Define a template first with .pooltools def"); + return false; + } + + Player* player = handler->GetPlayer(); + float const radius = radiusArg.value_or(5.0f); + + float searchX = player->GetPositionX(); + float searchY = player->GetPositionY(); + float searchZ = player->GetPositionZ(); + + GameObject* target = handler->GetNearbyGameObject(); + if (radius <= 10.0f && target) + { + searchX = target->GetPositionX(); + searchY = target->GetPositionY(); + searchZ = target->GetPositionZ(); + } + + std::list nearbyGOs; + Acore::GameObjectInRangeCheck check(searchX, searchY, searchZ, radius); + Acore::GameObjectListSearcher searcher(player, nearbyGOs, check); + Cell::VisitObjects(player, searcher, radius); + + int addedCount = 0; + int newGroupsCount = 0; + + for (GameObject* go : nearbyGOs) + { + if (go->GetDistance(searchX, searchY, searchZ) > radius) + continue; + + bool isTemplateMatch = false; + for (auto const& tpl : session.CurrentTemplate) + { + if (go->GetEntry() == tpl.Entry) + { + isTemplateMatch = true; + break; + } + } + if (!isTemplateMatch) + continue; + + uint32 const spawnId = go->GetSpawnId(); + + bool alreadyCaptured = false; + for (auto const& group : session.CapturedGroups) + { + for (auto const& obj : group.FoundObjects) + { + if (obj.second == spawnId) + { + alreadyCaptured = true; + break; + } + } + if (alreadyCaptured) + break; + } + if (alreadyCaptured) + continue; + + // Clustering + NodeGroup* existingGroup = nullptr; + for (auto& group : session.CapturedGroups) + { + if (go->GetDistance(group.X, group.Y, group.Z) < 0.1f) + { + existingGroup = &group; + break; + } + } + + if (existingGroup) + { + existingGroup->FoundObjects.push_back({ go->GetEntry(), spawnId }); + addedCount++; + } + else + { + NodeGroup newGroup; + newGroup.X = go->GetPositionX(); + newGroup.Y = go->GetPositionY(); + newGroup.Z = go->GetPositionZ(); + newGroup.FoundObjects.push_back({ go->GetEntry(), spawnId }); + + session.CapturedGroups.push_back(newGroup); + newGroupsCount++; + addedCount++; + } + } + + if (addedCount == 0) + { + handler->SendErrorMessage("No new matching objects found in {}y radius.", radius); + return false; + } + + handler->PSendSysMessage("|cff00ff00Scan Complete.|r Added {} objects into {} new groups.", addedCount, newGroupsCount); + + if (!session.CapturedGroups.empty()) + { + NodeGroup& lastGroup = session.CapturedGroups.back(); + for (auto& p : lastGroup.FoundObjects) + { + handler->PSendSysMessage(" - Entry {} (GUID: {})", p.first, p.second); + } + } + return true; + } + + static bool HandlePoolRemove(ChatHandler* handler) + { + ObjectGuid const playerGuid = handler->GetPlayer()->GetGUID(); + if (PoolSessions.find(playerGuid) == PoolSessions.end()) + { + handler->SendErrorMessage("No active session."); + return false; + } + + PoolSession& session = PoolSessions[playerGuid]; + + if (session.CapturedGroups.empty()) + { + handler->SendErrorMessage("No groups captured."); + return false; + } + + NodeGroup removed = session.CapturedGroups.back(); + session.CapturedGroups.pop_back(); + + handler->PSendSysMessage("|cffff0000Undo Successful.|r Removed last group containing {} objects.", removed.FoundObjects.size()); + return true; + } + + static bool HandlePoolEnd(ChatHandler* handler) + { + ObjectGuid playerGuid = handler->GetPlayer()->GetGUID(); + auto it = PoolSessions.find(playerGuid); + if (it == PoolSessions.end()) + return false; + + PoolSession& session = it->second; // Use reference from iterator + + auto EscapeSQL = [](std::string_view input) -> std::string { + std::string safe; + safe.reserve(input.size()); + for (char c : input) + { + if (c == '\'') + safe += "\\'"; + else + safe += c; + } + return safe; + }; + + bool const complexPool = (session.CurrentTemplate.size() > 1); + + // SQL Variables and Header + LOG_DEBUG("sql.dev", "-- Pool Dump: {}", session.ZoneName); + LOG_DEBUG("sql.dev", "SET @mother_pool := @mother_pool+1;"); + + if (complexPool) + LOG_DEBUG("sql.dev", "SET @pool_node := @pool_node+1;"); + + LOG_DEBUG("sql.dev", "SET @max_limit := {};", (session.CapturedGroups.size() + 3) / 4); + + // DELETEs section + if (!session.CapturedGroups.empty()) + { + LOG_DEBUG("sql.dev", "-- Cleanup specific object links"); + LOG_DEBUG("sql.dev", "DELETE FROM `pool_gameobject` WHERE `guid` IN ("); + + std::vector guidList; + for (auto const& group : session.CapturedGroups) + for (auto const& obj : group.FoundObjects) + guidList.push_back(std::to_string(obj.second)); + + LOG_DEBUG("sql.dev", fmt::format("{}", fmt::join(guidList, ", "))); + LOG_DEBUG("sql.dev", ");\n"); + } + + LOG_DEBUG("sql.dev", "DELETE FROM `pool_template` WHERE `entry`=@mother_pool;"); + LOG_DEBUG("sql.dev", "INSERT INTO `pool_template` (`entry`, `max_limit`, `description`) VALUES (@mother_pool, @max_limit, '{} - Mother Pool');", EscapeSQL(session.ZoneName)); + + int groupCounter = 0; + + // We can buffer the simple bulk inserts here + std::vector bulkInserts; + + for (auto const& group : session.CapturedGroups) + { + groupCounter++; + + // Generate Description + std::set uniqueNames; + for (auto const& obj : group.FoundObjects) + { + GameObjectTemplate const* goInfo = sObjectMgr->GetGameObjectTemplate(obj.first); + uniqueNames.insert(goInfo ? goInfo->name : std::to_string(obj.first)); + } + + std::string groupDesc = fmt::format("{}", fmt::join(uniqueNames, " / ")); + std::string safeGroupDesc = EscapeSQL(groupDesc); + + // Simple pooling + if (!complexPool) + { + for (auto const& obj : group.FoundObjects) + { + float chance = 0.0f; + for (auto const& tpl : session.CurrentTemplate) + if (tpl.Entry == obj.first) + chance = (float)tpl.Chance; + + bulkInserts.push_back(fmt::format("({}, @mother_pool, {}, '{} - {}')", + obj.second, chance, EscapeSQL(session.ZoneName), safeGroupDesc)); + } + } + // Pool_pool integration + else + { + LOG_DEBUG("sql.dev", "-- Group {}", groupCounter); + LOG_DEBUG("sql.dev", "SET @pool_node := @pool_node + 1;"); + + // Create the Sub-Pool Node + LOG_DEBUG("sql.dev", "INSERT INTO `pool_template` (`entry`, `max_limit`, `description`) VALUES (@pool_node, 1, '{} - Node {}');", EscapeSQL(session.ZoneName), groupCounter); + + // Link Node to Mother Pool + LOG_DEBUG("sql.dev", "INSERT INTO `pool_pool` (`pool_id`, `mother_pool`, `chance`, `description`) VALUES (@pool_node, @mother_pool, 0, '{} - {}');", EscapeSQL(session.ZoneName), safeGroupDesc); + + // Link Objects to Sub-Pool Node + LOG_DEBUG("sql.dev", "INSERT INTO `pool_gameobject` (`guid`, `pool_entry`, `chance`, `description`) VALUES"); + + std::vector nodeInserts; + for (auto const& obj : group.FoundObjects) + { + GameObjectTemplate const* goInfo = sObjectMgr->GetGameObjectTemplate(obj.first); + std::string objName = goInfo ? goInfo->name : "Unknown"; + + float chance = 0.0f; + for (auto const& tpl : session.CurrentTemplate) + if (tpl.Entry == obj.first) chance = (float)tpl.Chance; + + nodeInserts.push_back(fmt::format("({}, @pool_node, {}, '{} - {}')", + obj.second, chance, EscapeSQL(session.ZoneName), EscapeSQL(objName))); + } + LOG_DEBUG("sql.dev", "{};", fmt::join(nodeInserts, ",\n")); + } + } + + if (!complexPool && !bulkInserts.empty()) + { + LOG_DEBUG("sql.dev", "INSERT INTO `pool_gameobject` (`guid`, `pool_entry`, `chance`, `description`) VALUES"); + LOG_DEBUG("sql.dev", "{};", fmt::join(bulkInserts, ",\n")); + } + + handler->PSendSysMessage("Dumped {} groups.", groupCounter); + + // Cleanup + PoolSessions.erase(it); + return true; + } + + static bool HandlePoolClear(ChatHandler* handler) + { + PoolSessions.erase(handler->GetPlayer()->GetGUID()); + handler->PSendSysMessage("Session cleared."); + return true; + } +}; + +void AddSC_pooltools_commandscript() +{ + new pooltools_commandscript(); +} diff --git a/src/server/scripts/Commands/cs_script_loader.cpp b/src/server/scripts/Commands/cs_script_loader.cpp index d07ab959f..7e667a66c 100644 --- a/src/server/scripts/Commands/cs_script_loader.cpp +++ b/src/server/scripts/Commands/cs_script_loader.cpp @@ -49,6 +49,7 @@ void AddSC_modify_commandscript(); void AddSC_npc_commandscript(); void AddSC_pet_commandscript(); void AddSC_player_commandscript(); +void AddSC_pooltools_commandscript(); void AddSC_quest_commandscript(); void AddSC_reload_commandscript(); void AddSC_reset_commandscript(); @@ -101,6 +102,7 @@ void AddCommandsScripts() AddSC_npc_commandscript(); AddSC_pet_commandscript(); AddSC_player_commandscript(); + AddSC_pooltools_commandscript(); AddSC_quest_commandscript(); AddSC_reload_commandscript(); AddSC_reset_commandscript();