From 6ee7b5e3aeb43073802c01e689b7988ccf61a955 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:13:26 -0300 Subject: [PATCH] feat(Scripts/Commands): Add .debug loot command (#25164) Co-authored-by: Claude Opus 4.6 --- .../rev_1774115806392066400.sql | 18 ++ src/server/game/Miscellaneous/Language.h | 15 +- src/server/scripts/Commands/cs_debug.cpp | 234 ++++++++++++++++++ 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 data/sql/updates/pending_db_world/rev_1774115806392066400.sql diff --git a/data/sql/updates/pending_db_world/rev_1774115806392066400.sql b/data/sql/updates/pending_db_world/rev_1774115806392066400.sql new file mode 100644 index 000000000..10008e5ad --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1774115806392066400.sql @@ -0,0 +1,18 @@ +-- +DELETE FROM `command` WHERE `name` = 'debug loot'; +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('debug loot', 2, 'Syntax: .debug loot [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', 'Золото сред.: {} медь ({}зол {}сер {}мед) за {} итераций'); diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 4482dcb7b..4ee2eb3e7 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -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 diff --git a/src/server/scripts/Commands/cs_debug.cpp b/src/server/scripts/Commands/cs_debug.cpp index 156c0373a..f4531e9ed 100644 --- a/src/server/scripts/Commands/cs_debug.cpp +++ b/src/server/scripts/Commands/cs_debug.cpp @@ -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 #include +#include #include 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] : ""), 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 count) + { + static std::unordered_map 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 itemStats; + uint64 totalGold = 0; + + for (uint32 i = 0; i < iterations; ++i) + { + Loot loot; + GenerateLoot(loot, tab, *store, player, type, lootId); + + std::set 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(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()