From 607871a7ac576833fd29d5ec066d9ec252959a9a Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sat, 20 Aug 2022 01:16:09 -0700 Subject: [PATCH] [Loot] Add #lootsim (Loot Simulator) command (#2375) * [Loot] Add #lootsim (Loot Simulator) command * Validation * Add global loot --- zone/command.cpp | 2 + zone/command.h | 1 + zone/gm_commands/lootsim.cpp | 198 +++++++++++++++++++++++++++++++++++ zone/loottables.cpp | 6 +- zone/npc.cpp | 33 ++++++ zone/npc.h | 17 ++- 6 files changed, 252 insertions(+), 5 deletions(-) create mode 100755 zone/gm_commands/lootsim.cpp diff --git a/zone/command.cpp b/zone/command.cpp index cd9647106..3773ad5dd 100755 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -198,6 +198,7 @@ int command_init(void) command_add("level", "[Level] - Set your target's level", AccountStatus::Steward, command_level) || command_add("list", "[npcs|players|corpses|doors|objects] [search] - Search entities", AccountStatus::ApprenticeGuide, command_list) || command_add("listpetition", "List petitions", AccountStatus::Guide, command_listpetition) || + command_add("lootsim", "[npc_type_id] [loottable_id] [iterations] - Runs benchmark simulations using real loot logic to report numbers and data", AccountStatus::GMImpossible, command_lootsim) || command_add("load_shared_memory", "[shared_memory_name] - Reloads shared memory and uses the input as output", AccountStatus::GMImpossible, command_load_shared_memory) || command_add("loc", "Print out your or your target's current location and heading", AccountStatus::Player, command_loc) || command_add("logs", "Manage anything to do with logs", AccountStatus::GMImpossible, command_logs) || @@ -1035,6 +1036,7 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/level.cpp" #include "gm_commands/list.cpp" #include "gm_commands/listpetition.cpp" +#include "gm_commands/lootsim.cpp" #include "gm_commands/loc.cpp" #include "gm_commands/logcommand.cpp" #include "gm_commands/logs.cpp" diff --git a/zone/command.h b/zone/command.h index 9c8dcf5d4..d0c39b3f0 100644 --- a/zone/command.h +++ b/zone/command.h @@ -137,6 +137,7 @@ void command_lastname(Client *c, const Seperator *sep); void command_level(Client *c, const Seperator *sep); void command_list(Client *c, const Seperator *sep); void command_listpetition(Client *c, const Seperator *sep); +void command_lootsim(Client *c, const Seperator *sep); void command_load_shared_memory(Client *c, const Seperator *sep); void command_loc(Client *c, const Seperator *sep); void command_logs(Client *c, const Seperator *sep); diff --git a/zone/gm_commands/lootsim.cpp b/zone/gm_commands/lootsim.cpp new file mode 100755 index 000000000..2213c12a1 --- /dev/null +++ b/zone/gm_commands/lootsim.cpp @@ -0,0 +1,198 @@ +#include "../client.h" + +void command_lootsim(Client *c, const Seperator *sep) +{ + int arguments = sep->argnum; + if (arguments < 3 || !sep->IsNumber(1) || !sep->IsNumber(2) || !sep->IsNumber(3)) { + c->Message(Chat::White, "Usage: #lootsim [npc_type_id] [loottable_id] [iterations] [loot_log_enabled=0]"); + return; + } + + auto npc_id = std::stoul(sep->arg[1]); + auto loottable_id = std::stoul(sep->arg[2]); + auto iterations = std::stoul(sep->arg[3]) > 1000 ? 1000 : std::stoul(sep->arg[3]); + auto log_enabled = arguments > 3 ? std::stoul(sep->arg[4]) : false; + + // temporarily disable loot logging unless set explicitly + LogSys.log_settings[Logs::Loot].log_to_console = log_enabled ? LogSys.log_settings[Logs::Loot].log_to_console : 0; + LogSys.log_settings[Logs::Loot].log_to_file = log_enabled ? LogSys.log_settings[Logs::Loot].log_to_file : 0; + LogSys.log_settings[Logs::Loot].log_to_gmsay = log_enabled ? LogSys.log_settings[Logs::Loot].log_to_gmsay : 0; + + auto npc_type = content_db.LoadNPCTypesData(npc_id); + if (npc_type) { + auto npc = new NPC(npc_type, nullptr, c->GetPosition(), GravityBehavior::Water); + if (npc) { + + BenchTimer benchmark; + + npc->SetRecordLootStats(true); + for (int i = 0; i < iterations; i++) { + npc->AddLootTable(loottable_id); + + for (auto &id: zone->GetGlobalLootTables(npc)) { + npc->AddLootTable(id); + } + } + + entity_list.AddNPC(npc); + + c->SendChatLineBreak(); + c->Message( + Chat::White, + fmt::format( + "# [Loot Simulator] NPC [{}] ({}) Loot Table ID [{}] Dropped Items [{}] iterations [{}]", + npc->GetCleanName(), + npc_id, + loottable_id, + npc->GetRolledItems().size(), + iterations + ).c_str() + ); + c->SendChatLineBreak(); + + // npc level loot table + auto loot_table = database.GetLootTable(loottable_id); + if (!loot_table) { + c->Message(Chat::Red, "Loot table not found"); + return; + } + + for (uint32 i = 0; i < loot_table->NumEntries; i++) { + auto le = loot_table->Entries[i]; + + c->Message( + Chat::White, + fmt::format( + "# Lootdrop ID [{}] drop_limit [{}] min_drop [{}] mult [{}] probability [{}]", + le.lootdrop_id, + le.droplimit, + le.mindrop, + le.multiplier, + le.probability + ).c_str() + ); + + auto loot_drop = database.GetLootDrop(le.lootdrop_id); + if (!loot_drop) { + continue; + } + + for (uint32 ei = 0; ei < loot_drop->NumEntries; ei++) { + auto e = loot_drop->Entries[ei]; + int rolled_count = npc->GetRolledItemCount(e.item_id); + const EQ::ItemData *item = database.GetItem(e.item_id); + + EQ::SayLinkEngine linker; + linker.SetLinkType(EQ::saylink::SayLinkItemData); + linker.SetItemData(item); + + auto rolled_percentage = (float) ((float) ((float) rolled_count / (float) iterations) * 100); + + c->Message( + Chat::White, + fmt::format( + "-- [{}] item_id [{}] chance [{}] rolled_count [{}] ({:.2f}%) name [{}]", + ei, + e.item_id, + e.chance, + rolled_count, + rolled_percentage, + linker.GenerateLink() + ).c_str() + ); + } + } + + + // global loot + auto tables = zone->GetGlobalLootTables(npc); + if (!tables.empty()) { + c->SendChatLineBreak(); + c->Message(Chat::White, "# [Loot Simulator] Global Loot"); + } + + for (auto &id: tables) { + c->SendChatLineBreak(); + c->Message(Chat::White, fmt::format("# Global Loot Table ID [{}]", id).c_str()); + c->SendChatLineBreak(); + + loot_table = database.GetLootTable(id); + if (!loot_table) { + c->Message(Chat::Red, fmt::format("Global Loot table not found [{}]", id).c_str()); + continue; + } + + for (uint32 i = 0; i < loot_table->NumEntries; i++) { + auto le = loot_table->Entries[i]; + + c->Message( + Chat::White, + fmt::format( + "# Lootdrop ID [{}] drop_limit [{}] min_drop [{}] mult [{}] probability [{}]", + le.lootdrop_id, + le.droplimit, + le.mindrop, + le.multiplier, + le.probability + ).c_str() + ); + + auto loot_drop = database.GetLootDrop(le.lootdrop_id); + if (!loot_drop) { + continue; + } + + for (uint32 ei = 0; ei < loot_drop->NumEntries; ei++) { + auto e = loot_drop->Entries[ei]; + int rolled_count = npc->GetRolledItemCount(e.item_id); + const EQ::ItemData *item = database.GetItem(e.item_id); + + EQ::SayLinkEngine linker; + linker.SetLinkType(EQ::saylink::SayLinkItemData); + linker.SetItemData(item); + + auto rolled_percentage = (float) ((float) ((float) rolled_count / (float) iterations) * 100); + + c->Message( + Chat::White, + fmt::format( + "-- [{}] item_id [{}] chance [{}] rolled_count [{}] ({:.2f}%) name [{}]", + ei, + e.item_id, + e.chance, + rolled_count, + rolled_percentage, + linker.GenerateLink() + ).c_str() + ); + } + } + } + + + c->SendChatLineBreak(); + c->Message( + Chat::White, + fmt::format( + "# Global Loot Benchmark End [{}] iterations took [{}](s)", + iterations, + benchmark.elapsed() + ).c_str() + ); + c->SendChatLineBreak(); + + LogSys.LoadLogDatabaseSettings(); + } + } + else { + c->Message( + Chat::White, + fmt::format( + "Failed to spawn NPC ID {}.", + npc_id + ).c_str() + ); + } +} + + diff --git a/zone/loottables.cpp b/zone/loottables.cpp index 46eda9989..792fde1fc 100644 --- a/zone/loottables.cpp +++ b/zone/loottables.cpp @@ -39,7 +39,7 @@ #endif // Queries the loottable: adds item & coin to the npc -void ZoneDatabase::AddLootTableToNPC(NPC* npc,uint32 loottable_id, ItemList* itemlist, uint32* copper, uint32* silver, uint32* gold, uint32* plat) { +void ZoneDatabase::AddLootTableToNPC(NPC* npc, uint32 loottable_id, ItemList* itemlist, uint32* copper, uint32* silver, uint32* gold, uint32* plat) { const LootTable_Struct* lts = nullptr; // global loot passes nullptr for these bool bGlobal = copper == nullptr && silver == nullptr && gold == nullptr && plat == nullptr; @@ -559,6 +559,10 @@ void NPC::AddLootDrop( } else safe_delete(item); + if (IsRecordLootStats()) { + m_rolled_items.emplace_back(item->item_id); + } + if (wear_change && outapp) { entity_list.QueueClients(this, outapp); safe_delete(outapp); diff --git a/zone/npc.cpp b/zone/npc.cpp index c6c55cb53..a7abca981 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -264,6 +264,7 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi HasAISpell = false; HasAISpellEffects = false; innate_proc_spell_id = 0; + m_record_loot_stats = false; if (GetClass() == MERCENARY_MASTER && RuleB(Mercs, AllowMercs)) { LoadMercTypes(); @@ -3689,3 +3690,35 @@ std::vector NPC::GetLootList() { } return npc_items; } + +bool NPC::IsRecordLootStats() const +{ + return m_record_loot_stats; +} + +void NPC::SetRecordLootStats(bool record_loot_stats) +{ + NPC::m_record_loot_stats = record_loot_stats; +} + +void NPC::FlushLootStats() +{ + m_rolled_items = {}; +} + +const std::vector &NPC::GetRolledItems() const +{ + return m_rolled_items; +} + +int NPC::GetRolledItemCount(uint32 item_id) +{ + int rolled_count = 0; + for (auto &e: m_rolled_items) { + if (item_id == e) { + rolled_count++; + } + } + + return rolled_count; +} diff --git a/zone/npc.h b/zone/npc.h index 8c0e84b96..7c86cc894 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -109,6 +109,13 @@ public: static bool SpawnZoneController(); static int8 GetAILevel(bool iForceReRead = false); + // loot recording / simulator + bool IsRecordLootStats() const; + void SetRecordLootStats(bool record_loot_stats); + void FlushLootStats(); + const std::vector &GetRolledItems() const; + int GetRolledItemCount(uint32 item_id); + NPC(const NPCType* npc_type_data, Spawn2* respawn, const glm::vec4& position, GravityBehavior iflymode, bool IsCorpse = false); virtual ~NPC(); @@ -678,10 +685,12 @@ protected: private: - uint32 loottable_id; - bool skip_global_loot; - bool skip_auto_scale; - bool p_depop; + uint32 loottable_id; + bool skip_global_loot; + bool skip_auto_scale; + bool p_depop; + bool m_record_loot_stats; + std::vector m_rolled_items = {}; }; #endif