feat(Scripts/Commands): Add .debug loot command (#25164)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew 2026-03-24 19:13:26 -03:00 committed by GitHub
parent 9d49639da1
commit 6ee7b5e3ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 266 additions and 1 deletions

View file

@ -0,0 +1,18 @@
--
DELETE FROM `command` WHERE `name` = 'debug loot';
INSERT INTO `command` (`name`, `security`, `help`) VALUES
('debug loot', 2, 'Syntax: .debug loot <type> <id> [count]\nSimulates loot generation for the given loot type and ID, outputting the results to chat without creating items.\nOptional count (1-100) repeats the simulation and shows aggregated drop rates.\nValid types: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player');
DELETE FROM `acore_string` WHERE `entry` IN (30099, 30100, 30101, 30102, 30103, 30104, 30105, 30106, 30107, 30108, 30109);
INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES
(30099, 'Simulating loot for {} "{}" (ID: {})', '루팅 시뮬레이션: {} "{}" (ID: {})', 'Simulation du butin pour {} "{}" (ID: {})', 'Beutesimulation für {} "{}" (ID: {})', '模拟 {} "{}" 的战利品 (ID: {})', '模擬 {} "{}" 的戰利品 (ID: {})', 'Simulando botín para {} "{}" (ID: {})', 'Simulando botín para {} "{}" (ID: {})', 'Симуляция добычи для {} "{}" (ID: {})'),
(30100, ' [{}] x{} - {} ({}, RandProp: {}, RandSuffix: {})', ' [{}] x{} - {} ({}, 랜덤속성: {}, 랜덤접미사: {})', ' [{}] x{} - {} ({}, PropAléa: {}, SuffAléa: {})', ' [{}] x{} - {} ({}, ZufEig: {}, ZufSuf: {})', ' [{}] x{} - {} ({}, 随机属性: {}, 随机后缀: {})', ' [{}] x{} - {} ({}, 隨機屬性: {}, 隨機後綴: {})', ' [{}] x{} - {} ({}, PropAleat: {}, SufAleat: {})', ' [{}] x{} - {} ({}, PropAleat: {}, SufAleat: {})', ' [{}] x{} - {} ({}, СлСвойство: {}, СлСуффикс: {})'),
(30101, ' [{}] x{} - {} ({}) [Quest]', ' [{}] x{} - {} ({}) [퀘스트]', ' [{}] x{} - {} ({}) [Quête]', ' [{}] x{} - {} ({}) [Quest]', ' [{}] x{} - {} ({}) [任务]', ' [{}] x{} - {} ({}) [任務]', ' [{}] x{} - {} ({}) [Misión]', ' [{}] x{} - {} ({}) [Misión]', ' [{}] x{} - {} ({}) [Задание]'),
(30102, 'Gold: {} copper ({}g {}s {}c)', '골드: {} 코퍼 ({}금 {}은 {}동)', 'Or: {} cuivre ({}po {}pa {}pc)', 'Gold: {} Kupfer ({}G {}S {}K)', '金币: {} 铜 ({}金 {}银 {}铜)', '金幣: {} 銅 ({}金 {}銀 {}銅)', 'Oro: {} cobre ({}o {}p {}c)', 'Oro: {} cobre ({}o {}p {}c)', 'Золото: {} медь ({}зол {}сер {}мед)'),
(30103, 'No loot generated.', '생성된 전리품이 없습니다.', 'Aucun butin généré.', 'Keine Beute generiert.', '未生成战利品。', '未生成戰利品。', 'No se generó botín.', 'No se generó botín.', 'Добыча не сгенерирована.'),
(30104, 'Invalid loot type "{}". Valid: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', '잘못된 전리품 유형 "{}". 유효: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', 'Type de butin invalide "{}". Valides: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', 'Ungültiger Beutetyp "{}". Gültig: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', '无效的战利品类型 "{}"。有效: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', '無效的戰利品類型 "{}"。有效: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', 'Tipo de botín inválido "{}". Válidos: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', 'Tipo de botín inválido "{}". Válidos: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player', 'Неверный тип добычи "{}". Допустимые: creature, gameobject, fishing, item, pickpocketing, skinning, disenchant, prospecting, milling, spell, reference, mail, player'),
(30105, 'No loot template found for {} ID {}.', '{}의 전리품 템플릿을 찾을 수 없습니다 (ID: {}).', 'Aucun modèle de butin trouvé pour {} ID {}.', 'Keine Beutevorlage gefunden für {} ID {}.', '未找到 {} ID {} 的战利品模板。', '未找到 {} ID {} 的戰利品模板。', 'No se encontró plantilla de botín para {} ID {}.', 'No se encontró plantilla de botín para {} ID {}.', 'Шаблон добычи не найден для {} ID {}.'),
(30106, 'Simulating loot for {} "{}" (ID: {}) - {} iterations', '루팅 시뮬레이션: {} "{}" (ID: {}) - {} 반복', 'Simulation du butin pour {} "{}" (ID: {}) - {} itérations', 'Beutesimulation für {} "{}" (ID: {}) - {} Durchläufe', '模拟 {} "{}" 的战利品 (ID: {}) - {} 次迭代', '模擬 {} "{}" 的戰利品 (ID: {}) - {} 次迭代', 'Simulando botín para {} "{}" (ID: {}) - {} iteraciones', 'Simulando botín para {} "{}" (ID: {}) - {} iteraciones', 'Симуляция добычи для {} "{}" (ID: {}) - {} итераций'),
(30107, ' [{}] x{} - {} ({}) - dropped {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - 드롭 {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - obtenu {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - gefallen {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - 掉落 {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - 掉落 {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - obtenido {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - obtenido {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) - выпало {}/{} ({}.{}%)'),
(30108, ' [{}] x{} - {} ({}) [Quest] - dropped {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [퀘스트] - 드롭 {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [Quête] - obtenu {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [Quest] - gefallen {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [任务] - 掉落 {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [任務] - 掉落 {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [Misión] - obtenido {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [Misión] - obtenido {}/{} ({}.{}%)', ' [{}] x{} - {} ({}) [Задание] - выпало {}/{} ({}.{}%)'),
(30109, 'Gold avg: {} copper ({}g {}s {}c) over {} iterations', '골드 평균: {} 코퍼 ({}금 {}은 {}동) {} 반복', 'Or moy: {} cuivre ({}po {}pa {}pc) sur {} itérations', 'Gold Durchschn.: {} Kupfer ({}G {}S {}K) über {} Durchläufe', '金币均值: {} 铜 ({}金 {}银 {}铜) 共 {} 次迭代', '金幣均值: {} 銅 ({}金 {}銀 {}銅) 共 {} 次迭代', 'Oro prom: {} cobre ({}o {}p {}c) en {} iteraciones', 'Oro prom: {} cobre ({}o {}p {}c) en {} iteraciones', 'Золото сред.: {} медь ({}зол {}сер {}мед) за {} итераций');

View file

@ -1391,6 +1391,19 @@ enum AcoreStrings
LANG_DEBUG_LFG_ON = 30096,
LANG_DEBUG_LFG_OFF = 30097,
LANG_DEBUG_LFG_CONF = 30098
LANG_DEBUG_LFG_CONF = 30098,
// debug loot command
LANG_DEBUG_LOOT_HEADER = 30099,
LANG_DEBUG_LOOT_ITEM = 30100,
LANG_DEBUG_LOOT_ITEM_QUEST = 30101,
LANG_DEBUG_LOOT_GOLD = 30102,
LANG_DEBUG_LOOT_EMPTY = 30103,
LANG_DEBUG_LOOT_INVALID_TYPE = 30104,
LANG_DEBUG_LOOT_NO_TEMPLATE = 30105,
LANG_DEBUG_LOOT_HEADER_MULTI = 30106,
LANG_DEBUG_LOOT_ITEM_MULTI = 30107,
LANG_DEBUG_LOOT_ITEM_QUEST_MULTI = 30108,
LANG_DEBUG_LOOT_GOLD_MULTI = 30109
};
#endif

View file

@ -22,9 +22,11 @@
#include "Chat.h"
#include "CommandScript.h"
#include "GridNotifiersImpl.h"
#include "ItemTemplate.h"
#include "LFGMgr.h"
#include "Language.h"
#include "Log.h"
#include "LootMgr.h"
#include "M2Stores.h"
#include "MapMgr.h"
#include "ObjectAccessor.h"
@ -33,7 +35,9 @@
#include "ScriptMgr.h"
#include "Transport.h"
#include "Warden.h"
#include <algorithm>
#include <fstream>
#include <map>
#include <set>
using namespace Acore::ChatCommands;
@ -96,6 +100,7 @@ public:
{ "itemexpire", HandleDebugItemExpireCommand, SEC_ADMINISTRATOR, Console::No },
{ "areatriggers", HandleDebugAreaTriggersCommand, SEC_ADMINISTRATOR, Console::No },
{ "lfg", HandleDebugDungeonFinderCommand, SEC_ADMINISTRATOR, Console::Yes},
{ "loot", HandleDebugLootCommand, SEC_GAMEMASTER, Console::Yes},
{ "los", HandleDebugLoSCommand, SEC_ADMINISTRATOR, Console::No },
{ "moveflags", HandleDebugMoveflagsCommand, SEC_ADMINISTRATOR, Console::No },
{ "unitstate", HandleDebugUnitStateCommand, SEC_ADMINISTRATOR, Console::No },
@ -1607,6 +1612,235 @@ public:
handler->PSendSysMessage("Player count in zone {} ({}): {}.", zoneId, (zoneEntry ? zoneEntry->area_name[LOCALE_enUS] : "<unknown>"), player->GetMap()->GetPlayerCountInZone(zoneId));
return true;
}
static std::string GetLootSourceName(std::string const& type, uint32 lootId)
{
if (type == "creature" || type == "skinning" || type == "pickpocketing")
{
if (CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(lootId))
return ct->Name;
}
else if (type == "gameobject")
{
if (GameObjectTemplate const* gt = sObjectMgr->GetGameObjectTemplate(lootId))
return gt->name;
}
else if (type == "item" || type == "disenchant" || type == "prospecting" || type == "milling")
{
if (ItemTemplate const* it = sObjectMgr->GetItemTemplate(lootId))
return it->Name1;
}
else if (type == "fishing")
{
if (AreaTableEntry const* area = sAreaTableStore.LookupEntry(lootId))
return area->area_name[LOCALE_enUS];
}
return "";
}
static char const* GetItemQualityName(uint32 quality)
{
static char const* const qualityNames[MAX_ITEM_QUALITY] =
{
"Poor", "Normal", "Uncommon", "Rare",
"Epic", "Legendary", "Artifact", "Heirloom"
};
if (quality < MAX_ITEM_QUALITY)
return qualityNames[quality];
return "Unknown";
}
static void GenerateLoot(Loot& loot, LootTemplate const* tab,
LootStore const& store, Player* player, std::string const& type, uint32 lootId)
{
loot.clear();
loot.items.reserve(MAX_NR_LOOT_ITEMS);
loot.quest_items.reserve(MAX_NR_QUEST_ITEMS);
tab->Process(loot, store, LOOT_MODE_DEFAULT, player);
if (type == "creature")
{
if (CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(lootId))
loot.generateMoneyLoot(ct->mingold, ct->maxgold);
}
else if (type == "gameobject")
{
if (GameObjectTemplateAddon const* addon = sObjectMgr->GetGameObjectTemplateAddon(lootId))
loot.generateMoneyLoot(addon->mingold, addon->maxgold);
}
else if (type == "item")
{
if (ItemTemplate const* it = sObjectMgr->GetItemTemplate(lootId))
loot.generateMoneyLoot(it->MinMoneyLoot, it->MaxMoneyLoot);
}
}
static bool HandleDebugLootCommand(ChatHandler* handler, std::string type, uint32 lootId, Optional<uint32> count)
{
static std::unordered_map<std::string, LootStore*> const lootStoreMap =
{
{ "creature", &LootTemplates_Creature },
{ "gameobject", &LootTemplates_Gameobject },
{ "fishing", &LootTemplates_Fishing },
{ "item", &LootTemplates_Item },
{ "pickpocketing", &LootTemplates_Pickpocketing },
{ "skinning", &LootTemplates_Skinning },
{ "disenchant", &LootTemplates_Disenchant },
{ "prospecting", &LootTemplates_Prospecting },
{ "milling", &LootTemplates_Milling },
{ "spell", &LootTemplates_Spell },
{ "reference", &LootTemplates_Reference },
{ "mail", &LootTemplates_Mail },
{ "player", &LootTemplates_Player }
};
// Lowercase the type for case-insensitive matching
std::transform(type.begin(), type.end(), type.begin(), ::tolower);
auto itr = lootStoreMap.find(type);
if (itr == lootStoreMap.end())
{
handler->SendErrorMessage(LANG_DEBUG_LOOT_INVALID_TYPE, type);
return false;
}
LootStore const* store = itr->second;
LootTemplate const* tab = store->GetLootFor(lootId);
if (!tab)
{
handler->SendErrorMessage(LANG_DEBUG_LOOT_NO_TEMPLATE, type, lootId);
return false;
}
uint32 iterations = std::min(count.value_or(1), uint32(100));
if (iterations == 0)
iterations = 1;
Player* player = handler->GetPlayer();
std::string sourceName = GetLootSourceName(type, lootId);
// Single iteration - original behavior
if (iterations == 1)
{
Loot loot;
GenerateLoot(loot, tab, *store, player, type, lootId);
handler->PSendSysMessage(LANG_DEBUG_LOOT_HEADER, type, sourceName, lootId);
if (loot.items.empty() && loot.quest_items.empty() && loot.gold == 0)
{
handler->PSendSysMessage(LANG_DEBUG_LOOT_EMPTY);
return true;
}
for (LootItem const& li : loot.items)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(li.itemid);
std::string name = proto ? proto->Name1 : "Unknown";
char const* qualityName = GetItemQualityName(proto ? proto->Quality : 0);
handler->PSendSysMessage(LANG_DEBUG_LOOT_ITEM,
li.itemid, uint32(li.count), name, qualityName,
li.randomPropertyId, li.randomSuffix);
}
for (LootItem const& li : loot.quest_items)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(li.itemid);
std::string name = proto ? proto->Name1 : "Unknown";
char const* qualityName = GetItemQualityName(proto ? proto->Quality : 0);
handler->PSendSysMessage(LANG_DEBUG_LOOT_ITEM_QUEST,
li.itemid, uint32(li.count), name, qualityName);
}
if (loot.gold > 0)
{
uint32 gold = loot.gold / 10000;
uint32 silver = (loot.gold % 10000) / 100;
uint32 copper = loot.gold % 100;
handler->PSendSysMessage(LANG_DEBUG_LOOT_GOLD,
loot.gold, gold, silver, copper);
}
return true;
}
// Multi iteration - aggregate results
struct ItemStats
{
uint32 totalCount = 0;
uint32 timesDropped = 0;
bool questItem = false;
};
std::map<uint32, ItemStats> itemStats;
uint64 totalGold = 0;
for (uint32 i = 0; i < iterations; ++i)
{
Loot loot;
GenerateLoot(loot, tab, *store, player, type, lootId);
std::set<uint32> seenThisRun;
for (LootItem const& li : loot.items)
{
itemStats[li.itemid].totalCount += li.count;
if (seenThisRun.insert(li.itemid).second)
itemStats[li.itemid].timesDropped++;
}
for (LootItem const& li : loot.quest_items)
{
itemStats[li.itemid].totalCount += li.count;
itemStats[li.itemid].questItem = true;
if (seenThisRun.insert(li.itemid).second)
itemStats[li.itemid].timesDropped++;
}
totalGold += loot.gold;
}
handler->PSendSysMessage(LANG_DEBUG_LOOT_HEADER_MULTI,
type, sourceName, lootId, iterations);
if (itemStats.empty() && totalGold == 0)
{
handler->PSendSysMessage(LANG_DEBUG_LOOT_EMPTY);
return true;
}
for (auto const& [itemId, stats] : itemStats)
{
ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemId);
std::string name = proto ? proto->Name1 : "Unknown";
char const* qualityName = GetItemQualityName(proto ? proto->Quality : 0);
uint32 dropPct = (stats.timesDropped * 10000) / iterations;
if (stats.questItem)
handler->PSendSysMessage(LANG_DEBUG_LOOT_ITEM_QUEST_MULTI,
itemId, stats.totalCount, name, qualityName,
stats.timesDropped, iterations, dropPct / 100, dropPct % 100);
else
handler->PSendSysMessage(LANG_DEBUG_LOOT_ITEM_MULTI,
itemId, stats.totalCount, name, qualityName,
stats.timesDropped, iterations, dropPct / 100, dropPct % 100);
}
if (totalGold > 0)
{
uint32 avgGold = static_cast<uint32>(totalGold / iterations);
uint32 gold = avgGold / 10000;
uint32 silver = (avgGold % 10000) / 100;
uint32 copper = avgGold % 100;
handler->PSendSysMessage(LANG_DEBUG_LOOT_GOLD_MULTI,
avgGold, gold, silver, copper, iterations);
}
return true;
}
};
void AddSC_debug_commandscript()