/*
* 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();
}